359 lines
12 KiB
Markdown
359 lines
12 KiB
Markdown
# 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` |
|
|
| 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 |
|
|
| 2027-02-04 | Renovar certificado SSL wildcard (caduca ~365 días desde 2026-02-04) |
|