# napi — Retroalimentación Personalizada a Estudiantes > Sistema minimalista para servir feedback personalizado a cada alumno, > directamente desde ficheros Markdown editados con Emacs. **Estado:** ✅ Producción — 2 grupos activos (2026-02-25) | Grupo | URL | Alumnos | |:------|:----|:-------:| | **DDAW2** — Despliegue Aplicaciones Web | `https://notas.qu3v3d0.tech` | 19 | | **ASIR1** — Programación | `https://asir1.qu3v3d0.tech` | 21 | **Sin backend. Sin base de datos. Sin framework. Sin CDN.** --- ## Arquitectura ``` PROFESOR (aldebaran / anka4) ──────────────────────────── ~/napi-data/ ← sshfs mount (DDAW2) ├── _plantilla/notas.md ├── anas/notas.md └── ... (19 alumnos) ~/napi-data2/ ← sshfs mount (ASIR1) ├── _plantilla/notas.md ├── barja/notas.md └── ... (21 alumnos) │ sshfs (SSH port 22) ▼ SERVIDOR (zzz / qu3v3d0.tech) ───────────────────────────── /var/www/napi/ ← DDAW2 ├── viewer.html ├── marked.min.js ├── twemoji.min.js └── data/$alumno/notas.md /var/www/napi2/ ← ASIR1 ├── viewer.html ├── marked.min.js ├── twemoji.min.js └── data/$alumno/notas.md Nginx + libnginx-mod-http-auth-pam → auth con credenciales SFTP del alumno → $remote_user → sirve data/$remote_user/notas.md → viewer.html renderiza el .md con marked.js + twemoji ALUMNO (cualquier dispositivo) ────────────────────────────── https://notas.qu3v3d0.tech → DDAW2 https://asir1.qu3v3d0.tech → ASIR1 → login con credenciales SFTP → ve sus notas en HTML → refresca → cambios inmediatos ``` ### Stack | Componente | Tecnología | Dónde | |:-----------|:-----------|:------| | **Datos** | Ficheros `notas.md` (Markdown) | zzz | | **Transporte** | sshfs mounts persistentes (systemd) | aldebaran/anka4 → zzz | | **Servidor web** | Nginx (1 server block por grupo) | zzz | | **Autenticación** | `libnginx-mod-http-auth-pam` | zzz | | **Renderer** | `marked.min.js` + `twemoji.min.js` + `viewer.html` | zzz | | **Notificaciones DDAW2** | `nginx-user-config-watcher.sh` + XMPP | zzz | | **Notificaciones ASIR1** | `python-upload-watcher.sh` (v7) + XMPP | zzz | | **SSL** | Certificado wildcard `*.qu3v3d0.tech` | zzz | | **DNS** | Wildcard `*.qu3v3d0.tech` → `161.22.44.104` | DNS | --- ## Grupos y Alumnos ### DDAW2 — Despliegue de Aplicaciones Web (19 alumnos) - **URL:** `https://notas.qu3v3d0.tech` - **Datos:** `~/napi-data/` → `zzz:/var/www/napi/data/` - **Usernames:** nombre de pila en minúsculas - **SFTP chroot:** `/home/USER/html/` ``` anas, carlos, carlosv, daniel, danieln, erick, evelin, gianfranco, giorgio, joel, jorge, josue, juanan, juanjesus, kasandra, marius, miguel, pablo, patrick ``` ### ASIR1 — Programación (21 alumnos) - **URL:** `https://asir1.qu3v3d0.tech` - **Datos:** `~/napi-data2/` → `zzz:/var/www/napi2/data/` - **Usernames:** apellido en minúsculas (sin tildes) - **Contraseñas:** leet-speak del apellido (`a→4, e→3, i→1, o→0`) - **SFTP chroot:** `/home/USER/python/` | Username | Alumno | Username | Alumno | |:---------|:-------|:---------|:-------| | **barja** | Alex Barja | **martinez** | Daniel Martínez | | **barrios** | Andrés Barrios | **munoz** | Jordy Muñoz | | **cayo** | Jared Cayo | **olcina** | Jorge Olcina | | **contrera** | Luciano Contrera | **ponce** | Francisco Ponce | | **duque** | Jorge Duque | **posada** | Santiago Posada | | **florea** | Alejandro Florea | **quiroz** | Alexander Quiroz | | **gomes** | Gabriel Gomes | **reynoso** | Jorge Reynoso | | **izquierdo** | Daniel Izquierdo | **sierra** | Leonel Sierra | | **jara** | María Jara | **torrero** | Mario Torrero | | **lillo** | Fernando Lillo | | | | **linares** | Christopher Linares | | | | **macedo** | Eduardo Macedo | | | --- ## Notificaciones XMPP Ambos grupos tienen watchers en zzz que notifican al profesor via XMPP (`jla@librebits.info`) cuando un alumno sube ficheros por SFTP. ### DDAW2: `nginx-user-config-watcher.sh` (v3) Monitoriza `/home/USER/html/*.conf` — valida, despliega y analiza CSP de configuraciones Nginx. ### ASIR1: `python-upload-watcher.sh` (v7) Monitoriza `/home/USER/python/` — detecta entregas de prácticas de Programación. **Características:** - **Batching por carpeta** — Si un alumno sube `PRACTICA3.1/` con N ficheros, envía **un solo mensaje** con el listado completo (espera 10s de silencio) - **Re-entregas** — Detecta delete+recreate de una carpeta como "re-entrega" - **Nombre completo** — Notifica `[María Jara]` en vez de `[jara]` - **Windows-safe** — Ignora `Thumbs.db`, `desktop.ini`, `.DS_Store`, `*.tmp` - **Python-safe** — Ignora `__pycache__/`, `*.pyc`, `*.pyo` - **Dev-safe** — Ignora `.git/`, `.venv/`, `node_modules/`, `.idea/`, `.vscode/` **Ejemplo de notificación:** ``` 📁 [Andrés Barrios] Entrega ASIR1: PRACTICA3.1/ (4 ficheros) 🐍 main.py (2048 bytes) 🐍 utils.py (1024 bytes) 📝 README.md (512 bytes) 📄 requisitos.txt (128 bytes) ``` ``` 📁 [Andrés Barrios] ♻️ Re-entrega ASIR1: PRACTICA3.1/ (3 ficheros) 🐍 main.py (2100 bytes) 📝 README.md (600 bytes) 📄 requisitos.txt (128 bytes) ``` --- ## Workflow del Profesor ``` 1. C-x C-f ~/napi-data2/barrios/notas.md ← abrir en Emacs 2. Editar feedback, notas, próximos pasos 3. C-x C-s ← guardar 4. Alumno refresca el navegador ← cambios visibles ``` **No hay paso 5.** El sshfs hace que guardar localmente sea equivalente a escribir en zzz. --- ## Requisitos del Servidor (Debian) - Debian 11/12/... - Nginx + `libnginx-mod-http-auth-pam` - Certificado SSL (wildcard recomendado para múltiples subdominios) - Usuarios SFTP con chroot (`grupo sftpusers`) - `www-data` en grupo `shadow` (necesario para auth_pam) - Python 3 + `slixmpp` (para notificaciones XMPP) - `inotify-tools` (para los watchers) - SSH accesible desde la máquina del profesor (para sshfs) --- ## Despliegue Rápido de un Nuevo Grupo ### 1. En zzz: crear la app web ```bash sudo mkdir -p /var/www/napiN/data sudo cp /var/www/napi/viewer.html /var/www/napiN/ sudo cp /var/www/napi/marked.min.js /var/www/napiN/ sudo cp /var/www/napi/twemoji.min.js /var/www/napiN/ sudo chown fenix:www-data /var/www/napiN/data sudo chmod 775 /var/www/napiN/data ``` ### 2. En zzz: crear server block Nginx ```nginx server { listen 80; server_name SUBDOMINIO.qu3v3d0.tech; return 301 https://$server_name$request_uri; } server { listen 443 ssl; server_name SUBDOMINIO.qu3v3d0.tech; ssl_certificate /etc/ssl/certs/qu3v3d0.tech.crt; ssl_certificate_key /etc/ssl/private/qu3v3d0.tech.key; root /var/www/napiN; auth_pam "Notas GRUPO"; auth_pam_service_name "common-auth"; location = / { try_files /viewer.html =404; } location = /notas.md { alias /var/www/napiN/data/$remote_user/notas.md; default_type text/plain; charset utf-8; add_header Cache-Control "no-cache"; } location ~* \.(js|css)$ { expires 7d; } location / { return 404; } } ``` ```bash sudo ln -s /etc/nginx/sites-available/napiN /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx ``` ### 3. En zzz: crear usuarios SFTP ```bash for user in alumno1 alumno2 alumnoN; do sudo useradd -m -d /home/$user -s /usr/sbin/nologin -G sftpusers,www-data $user echo "$user:CONTRASEÑA" | sudo chpasswd sudo mkdir -p /home/$user/CARPETA # html/ o python/ según grupo sudo chown root:root /home/$user sudo chmod 755 /home/$user sudo chown $user:www-data /home/$user/CARPETA sudo chmod 775 /home/$user/CARPETA done ``` ### 4. Copiar datos de alumnos y montar sshfs ```bash # Copiar carpetas con notas.md al servidor scp -r ~/napi-dataN/* fenix@qu3v3d0.tech:/var/www/napiN/data/ # Crear unit systemd sshfs (~/.config/systemd/user/) systemctl --user daemon-reload systemctl --user enable --now 'home-fenix-napi\x2ddataN.mount' ``` --- ## Estructura del Repositorio ``` ~/napi/ ├── README.md ← este fichero ├── CLAUDE.md ← instrucciones para Claude Code ├── viewer.html ← app web (fetch + marked.js + twemoji) ├── marked.min.js ← renderer Markdown local ├── nginx/ │ ├── README.md ← documentación configs Nginx │ ├── napi-ddaw2.conf ← server block notas.qu3v3d0.tech │ └── napi2-asir1.conf ← server block asir1.qu3v3d0.tech ├── scripts/ │ ├── README.md ← documentación watchers y servicios │ ├── python-upload-watcher.sh ← watcher ASIR1 (v7) │ ├── nginx-user-config-watcher.sh ← watcher DDAW2 (v3) │ ├── xmpp-notify.py ← bot XMPP one-shot │ ├── python-upload-watcher.service ← unit systemd │ ├── nginx-user-config-watcher.service │ ├── home-fenix-napi-data.mount ← sshfs DDAW2 │ └── home-fenix-napi-data2.mount ← sshfs ASIR1 └── Screenshots/ └── zzz@librebits.info.png ← ejemplo notificaciones XMPP ``` --- ## Conexión a zzz ```bash ssh fenix@qu3v3d0.tech # clave sin -i, fenix tiene sudo ``` --- ## Gestión de Servicios ### Watchers (en zzz) ```bash sudo systemctl status python-upload-watcher.service # ASIR1 sudo systemctl status nginx-user-config-watcher.service # DDAW2 sudo systemctl restart python-upload-watcher.service ``` ### Mounts sshfs (en aldebaran/anka4) ```bash systemctl --user status home-fenix-napi\\x2ddata.mount # DDAW2 systemctl --user status home-fenix-napi\\x2ddata2.mount # ASIR1 ``` ### Logs ```bash sudo tail -f /var/log/python-upload-watcher.log # ASIR1 watcher sudo tail -f /var/log/nginx/napi-error.log # Nginx errors ``` --- ## Troubleshooting | Problema | Causa | Solución | |:---------|:------|:---------| | `403 Forbidden` | www-data no en grupo shadow | `sudo usermod -aG shadow www-data && sudo systemctl restart nginx` | | `401 Unauthorized` | Credenciales incorrectas o PAM mal configurado | Verificar `/etc/pam.d/common-auth` | | `twemoji is not defined` | Falta `twemoji.min.js` en el root del site | `sudo cp /var/www/napi/twemoji.min.js /var/www/napiN/` | | `404` en `/notas.md` | Carpeta del alumno no existe en `data/` | Crear carpeta + copiar plantilla | | sshfs zombie | Conexión SSH caída | `fusermount -uz ~/napi-dataN && systemctl --user restart ...mount` | | Emacs muestra datos stale tras editar | sshfs cachea lecturas del kernel | Añadir `auto_cache` a las Options del mount (ver abajo) | | Watcher no detecta re-entregas | Versión antigua del watcher | Actualizar a v7+ y reiniciar servicio | | `__pycache__` en notificaciones | Watcher < v6 | Actualizar a v7+ | --- ## Seguridad - Cada alumno solo ve **sus propias notas** — Nginx resuelve el path con `$remote_user` - Sin ejecución de código en el servidor — todo es estático - `marked.min.js` y `twemoji.min.js` locales — sin dependencia de CDNs externos - Credenciales: las mismas que usa el alumno para SFTP (FileZilla) - SSL/TLS obligatorio (redirección 301 desde HTTP) - SFTP con chroot — alumnos no pueden ver otros directorios --- ## Historial | Fecha | Hito | |:------|:-----| | 2026-02-22 | MVP desplegado: notas.qu3v3d0.tech para DDAW2 (19 alumnos) | | 2026-02-25 | ASIR1 desplegado: asir1.qu3v3d0.tech para Programación (21 alumnos) | | 2026-02-25 | Watcher ASIR1 v7: batching, re-entregas, Windows-safe, __pycache__ filter, nombres completos | | 2026-03-02 | sshfs mounts: añadido `auto_cache` para evitar datos stale en Emacs | | 2027-02-04 | Renovar certificado SSL wildcard (caduca ~365 días desde 2026-02-04) |