- README.md: complete rewrite covering DDAW2 + ASIR1, deployment guide, troubleshooting, security, full student roster - CLAUDE.md: updated to reflect both groups, simplified structure - scripts/README.md: watcher bumped to v7 (fullnames, __pycache__ filter) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| Screenshots | ||
| nginx | ||
| scripts | ||
| .gitignore | ||
| CLAUDE.md | ||
| LICENSE | ||
| README.md | ||
| marked.min.js | ||
| viewer.html | ||
README.md
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-dataen gruposhadow(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
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
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; }
}
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
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
# 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
ssh fenix@qu3v3d0.tech # clave sin -i, fenix tiene sudo
Gestión de Servicios
Watchers (en zzz)
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)
systemctl --user status home-fenix-napi\\x2ddata.mount # DDAW2
systemctl --user status home-fenix-napi\\x2ddata2.mount # ASIR1
Logs
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 |
| 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.jsytwemoji.min.jslocales — 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 |
| 2027-02-04 | Renovar certificado SSL wildcard (caduca ~365 días desde 2026-02-04) |