napi/PLAN.md

5.6 KiB

Plan: Emacs Integration for napi — Remote File-Notify via TRAMP

Context

Fénix wants real-time awareness in Emacs of student deliveries on zzz (qu3v3d0.tech). The GNU Emacs manual confirms that file-notify-add-watch works on remote machines via TRAMP — it runs inotifywait on the remote host through SSH and streams events back in real-time. No polling.

This means: no modifications to watcher scripts on zzz, no reverse SSH tunnels, no Porthole/JSON-RPC. Pure Emacs, using built-in capabilities + a dedicated daemon.

Architecture

emacs --daemon=napi (local)
    │
    ├── TRAMP SSH → zzz:/home/*/python/    (ASIR1 — 21 dirs)
    │   └── inotifywait running on zzz (auto-started by TRAMP)
    │
    ├── TRAMP SSH → zzz:/home/*/html/      (DDAW2 — 19 dirs)
    │   └── inotifywait running on zzz (auto-started by TRAMP)
    │
    └── On file event → forward notification to `water` daemon
        ├── Desktop notification (D-Bus)
        ├── Sound alert (paplay)
        ├── *napi-log* buffer (persistent log)
        └── Minibuffer message

emacsclient --socket-name=napi --eval '(napi-dashboard)'   ← interact
emacsclient --socket-name=water --eval '(napi-log)'        ← view in main UI

Prerequisites

  • inotify-tools installed on zzz (provides inotifywait) — already present (watchers use it)
  • SSH key access from local → zzz — already working (ssh fenix@qu3v3d0.tech)
  • Emacs 30.1 on local — confirmed

Implementation Steps

Step 1: Create ~/napi/emacs/napi.el

The core elisp package. ~200 lines. Key components:

Component Purpose
napi-watch-start Sets up TRAMP file-notify watches on all student dirs
napi-watch-stop Removes all watches cleanly
napi-notify Handles a file event: log + desktop notification + sound
napi-log Interactive: open *napi-log* buffer
napi-open-student Interactive: completing-read → open student dir/notas.md
napi-dashboard Interactive: overview of all students + last delivery
C-c n prefix Keybindings for all interactive commands

Student name maps (username → full name) embedded in elisp, mirroring watcher scripts.

TRAMP watch setup — the core innovation:

;; For each ASIR1 student:
(file-notify-add-watch
  "/ssh:fenix@qu3v3d0.tech:/home/jara/python/"
  '(change)
  #'napi--handle-event)

;; 40 watches total (21 ASIR1 + 19 DDAW2)
;; Each spawns a persistent inotifywait on zzz via SSH

Event handler filters noise (Thumbs.db, pycache, .tmp, etc.) and forwards clean notifications to water daemon via:

(shell-command
  "emacsclient --socket-name=water --eval '(napi--show-notification ...)' &")

Reconnection: TRAMP sends a stopped event when SSH drops. The handler auto-re-establishes the watch after a delay.

Step 2: Create systemd user unit for napi daemon

File: ~/.config/systemd/user/emacs-napi.service

[Unit]
Description=Emacs daemon for napi student monitoring
After=network-online.target
Wants=network-online.target

[Service]
Type=forking
ExecStart=/usr/bin/emacs --daemon=napi -l ~/napi/emacs/napi-init.el
ExecStop=/usr/bin/emacsclient --socket-name=napi --eval "(kill-emacs)"
Restart=on-failure
RestartSec=30

[Install]
WantedBy=default.target

Step 3: Create ~/napi/emacs/napi-init.el

Minimal init file for the napi daemon (not the full ~/.emacs.d/init.el):

;; Loaded ONLY by emacs --daemon=napi
(load "~/napi/emacs/napi.el")
(napi-watch-start)
(message "napi: watching %d student directories on zzz" napi--watch-count)

Step 4: Also load napi.el in water daemon

Add to ~/.emacs.d/init.el (near line 1478, following funciones-art.el pattern):

(load "~/napi/emacs/napi.el")

This gives the water daemon the napi-log, napi-dashboard, napi-open-student commands and the C-c n keybindings — but no watches (those run in the napi daemon only).

Step 5: Verify inotifywait on zzz

ssh fenix@qu3v3d0.tech "which inotifywait && inotifywait --help | head -1"

Files to Create/Modify

File Action Location
emacs/napi.el CREATE ~/napi/emacs/napi.el
emacs/napi-init.el CREATE ~/napi/emacs/napi-init.el
emacs-napi.service CREATE ~/.config/systemd/user/emacs-napi.service
init.el MODIFY (1 line) ~/.emacs.d/init.el

No modifications to zzz — no watcher changes, no new SSH keys, no reverse tunnels.

Testing

  1. Unit test — load napi.el in water, call (napi-notify ...) manually
  2. TRAMP watch test — single watch on one student dir, touch a file on zzz
  3. Full integration — start napi daemon, simulate SFTP upload as student
  4. Stress test — confirm 40 concurrent TRAMP watches are stable
  5. Reconnection — kill SSH, verify watch auto-restores
  6. UI testC-c n l (log), C-c n s (student), C-c n d (dashboard)

Risk: 40 Concurrent SSH/inotifywait Processes

Each file-notify-add-watch via TRAMP spawns a separate inotifywait process on zzz. 40 watches = 40 persistent SSH channels + 40 inotifywait processes.

Mitigation: TRAMP reuses SSH connections (ControlMaster). So it's 1 SSH connection + 40 remote processes. The zzz server's inotify limit (fs.inotify.max_user_watches) is typically 65536 — 40 is nothing.

Fallback: If 40 watches proves unstable, consolidate to 2 watches on parent dirs (/home/ with recursive) + filter events by path. Or switch to the emacsclient-via-SSH approach (see FINDINGS.md).