docs: rewrite all documentation for multi-group architecture

- 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>
This commit is contained in:
fenix 2026-02-25 20:36:38 +01:00
parent aa3126cdb6
commit 42c1a7df8f
3 changed files with 332 additions and 457 deletions

197
CLAUDE.md
View File

@ -1,12 +1,12 @@
# napi.qvd.tech — Retroalimentación a Estudiantes # napi — Retroalimentación Personalizada a Estudiantes
## ✅ Estado: MVP FUNCIONAL (2026-02-22) ## ✅ Estado: PRODUCCIÓN — 2 grupos activos (2026-02-25)
--- ---
## Contexto ## Contexto
Proyecto minimalista para proporcionar retroalimentación personalizada a los estudiantes de DDAW2, cerrando el bucle profesor → alumno. Proyecto minimalista para proporcionar retroalimentación personalizada a los estudiantes, cerrando el bucle profesor → alumno.
El profesor (Fénix) mantiene ficheros `notas.md` por alumno con: El profesor (Fénix) mantiene ficheros `notas.md` por alumno con:
- Tabla resumen de todas las prácticas (nota + estado) - Tabla resumen de todas las prácticas (nota + estado)
@ -15,121 +15,55 @@ El profesor (Fénix) mantiene ficheros `notas.md` por alumno con:
Los alumnos acceden desde cualquier dispositivo con su navegador, sin instalar nada. Los alumnos acceden desde cualquier dispositivo con su navegador, sin instalar nada.
**Sin backend. Sin base de datos. Sin framework. Sin CDN.**
--- ---
## Arquitectura Final ## Grupos Activos
| Grupo | URL | Alumnos | Datos local | Datos zzz |
|:------|:----|:-------:|:------------|:----------|
| **DDAW2** | `https://notas.qu3v3d0.tech` | 19 | `~/napi-data/` | `/var/www/napi/data/` |
| **ASIR1** | `https://asir1.qu3v3d0.tech` | 21 | `~/napi-data2/` | `/var/www/napi2/data/` |
---
## Arquitectura
``` ```
aldebaran (local) zzz (qu3v3d0.tech) PROFESOR (aldebaran / anka4) SERVIDOR (zzz / qu3v3d0.tech)
───────────────── ────────────────── ──────────────────────────── ─────────────────────────────
~/napi-data/ sshfs /var/www/napi/data/ ~/napi-data/ ←── sshfs ──→ /var/www/napi/data/ (DDAW2, 19 alumnos)
├── anas/notas.md ←──────────→ ├── anas/notas.md ~/napi-data2/ ←── sshfs ──→ /var/www/napi2/data/ (ASIR1, 21 alumnos)
├── pablo/notas.md ├── pablo/notas.md
├── miguel/notas.md ├── miguel/notas.md
└── .../ (19 alumnos) └── .../ (19 alumnos)
/var/www/napi/ /var/www/napi[2]/
├── viewer.html (app completa) ├── viewer.html
└── marked.min.js (renderer local) ├── marked.min.js
└── twemoji.min.js
Nginx + libnginx-mod-http-auth-pam Nginx + auth_pam → $remote_user
→ auth con credenciales SFTP del alumno → data/$remote_user/notas.md
→ $remote_user → sirve /var/www/napi/data/$remote_user/notas.md → viewer.html renderiza con marked.js
→ viewer.html renderiza el .md en el navegador (marked.js local)
``` ```
### Stack ### Stack
| Componente | Tecnología | Dónde | | Componente | Tecnología | Dónde |
|:-----------|:-----------|:------| |:-----------|:-----------|:------|
| **Datos** | Ficheros `notas.md` (Markdown) | `zzz:/var/www/napi/data/` | | **Datos** | Ficheros `notas.md` (Markdown) | zzz |
| **Transporte** | sshfs mount persistente | aldebaran → zzz | | **Transporte** | sshfs mounts persistentes (systemd) | aldebaran/anka4 → zzz |
| **Servidor web** | Nginx | zzz | | **Servidor web** | Nginx (1 server block por grupo) | zzz |
| **Autenticación** | `libnginx-mod-http-auth-pam` | zzz | | **Autenticación** | `libnginx-mod-http-auth-pam` | zzz |
| **Renderer** | `marked.min.js` (local, sin CDN) + `viewer.html` | zzz | | **Renderer** | `marked.min.js` + `twemoji.min.js` + `viewer.html` | zzz |
| **Notificaciones** (watcher nginx) | Python + slixmpp → XMPP | zzz | | **Notificaciones DDAW2** | `nginx-user-config-watcher.sh` (v3) + XMPP | zzz |
| **Notificaciones ASIR1** | `python-upload-watcher.sh` (v7) + XMPP | zzz |
**Sin backend. Sin Python para servir. Sin JSON. Sin Syncthing.** | **SSL** | Certificado wildcard `*.qu3v3d0.tech` | zzz |
--- ---
## URL de Acceso ## Alumnos
``` ### DDAW2 (19) — usernames = nombre de pila
https://notas.qu3v3d0.tech
```
Una sola URL para todos los alumnos. Nginx usa `$remote_user` tras auth_pam para servir el `.md` correcto.
- Alumno ingresa sus credenciales SFTP (mismas que FileZilla)
- Ve únicamente sus propias notas
- Refresco del navegador = actualización inmediata
---
## Ficheros Clave en zzz
| Ruta | Descripción |
|:-----|:------------|
| `/var/www/napi/data/$alumno/notas.md` | Fuente de datos por alumno |
| `/var/www/napi/viewer.html` | App completa (fetch + marked.js) |
| `/var/www/napi/marked.min.js` | Renderer Markdown local |
| `/etc/nginx/sites-enabled/napi` | Config Nginx del servicio |
`www-data` está en grupo `shadow` (necesario para auth_pam).
---
## Ficheros Clave en aldebaran
| Ruta | Descripción |
|:-----|:------------|
| `~/napi-data/` | Mount sshfs → `/var/www/napi/data/` en zzz |
| `~/.config/systemd/user/home-fenix-napi\x2ddata.mount` | Unit systemd persistente |
| `~/napi-data/_plantilla/notas.md` | Plantilla para alumnos nuevos |
| `~/napi-data/README.md` | Documentación del proyecto |
---
## Workflow Emacs (edición)
```
1. C-x C-f ~/napi-data/pablo/notas.md
2. Editar → C-x C-s
3. Alumno refresca navegador → cambios visibles al instante
```
El sshfs hace que guardar en aldebaran sea equivalente a escribir directamente en zzz.
---
## Formato notas.md
```markdown
# Notas — NombreAlumno
> 🏫 Módulo: DDAW2
> 📅 Última actualización: FECHA
## 📊 Resumen
| Práctica | Título | Nota | Estado |
| P2.3 | Nginx via SFTP | 7/10 | ✅ |
...
## P2.7 — Multi-sitio Web con Nginx
**Nota: 9/10**
[feedback personalizado]
### Criterios
| Criterio | Puntos | Estado |
...
```
Ver `_plantilla/notas.md` y cualquier alumno como referencia.
---
## Alumnos Activos (19) — DDAW2
``` ```
anas, carlos, carlosv, daniel, danieln, erick, evelin, gianfranco, anas, carlos, carlosv, daniel, danieln, erick, evelin, gianfranco,
@ -137,48 +71,53 @@ giorgio, joel, jorge, josue, juanan, juanjesus, kasandra, marius,
miguel, pablo, patrick miguel, pablo, patrick
``` ```
Credenciales SFTP en `~/EducaMadrid/DDAW2/PRACTICA2.4-despliegue-web-HTTP-y-HTTPS/README.md`. SFTP chroot: `/home/USER/html/`
### ASIR1 (21) — usernames = apellido en minúsculas
```
barja, barrios, cayo, contrera, duque, florea, gomes, izquierdo,
jara, lillo, linares, macedo, martinez, munoz, olcina, ponce,
posada, quiroz, reynoso, sierra, torrero
```
SFTP chroot: `/home/USER/python/` · Contraseñas: leet-speak (`a→4, e→3, i→1, o→0`)
--- ---
## SSH / Conexión a zzz ## SSH / Conexión a zzz
```bash ```bash
ssh fenix@qu3v3d0.tech # clave sin -i ssh fenix@qu3v3d0.tech # clave sin -i, fenix tiene sudo
```
`fenix` tiene sudo en zzz. Alumnos: grupo `sftpusers`, chroot `/home/$user`.
---
## Añadir Alumno Nuevo
```bash
# 1. En zzz: crear usuario SFTP (ver README de P2.4 para el script completo)
sudo useradd -m -d /home/$USER -s /usr/sbin/nologin -G sftpusers,www-data $USER
# 2. En aldebaran: crear carpeta de notas
mkdir ~/napi-data/$USER
cp ~/napi-data/_plantilla/notas.md ~/napi-data/$USER/notas.md
# Editar con Emacs y personalizar
``` ```
--- ---
## Prácticas Documentadas ## Ficheros Clave
| Práctica | Directorio local | Descripción | ### En zzz
|:---------|:-----------------|:------------|
| P2.3 | `~/EducaMadrid/DDAW2/PRACTICA2.3-despliegue-nginx-via-sftp/` | Nginx config via SFTP + watcher inotify + XMPP | | Ruta | Descripción |
| P2.4 | `~/EducaMadrid/DDAW2/PRACTICA2.4-despliegue-web-HTTP-y-HTTPS/` | HTTP + HTTPS con cert wildcard autofirmado | |:-----|:------------|
| P2.5 | `~/EducaMadrid/DDAW2/PRACTICA2.5-despliegue-web-bloquear-CDN-JS+servir-MD/` | CSP + bloqueo CDN + marked.js local | | `/var/www/napi/` | App DDAW2 (viewer + marked + twemoji + data/) |
| P2.6 | `~/EducaMadrid/DDAW2/PRACTICA2.6-Hextris-compresion-cache-y-mejora-de-rendimiento/` | Hextris + gzip + caché | | `/var/www/napi2/` | App ASIR1 (idem) |
| P2.7 | `~/EducaMadrid/DDAW2/PRACTICA2.7-MULTI-SITIO-WEB-NGINX@zzz/` | Multi-sitio: Hextris + App propia + CSP + cabeceras | | `/etc/nginx/sites-enabled/napi` | Server block DDAW2 |
| `/etc/nginx/sites-enabled/napi2` | Server block ASIR1 |
| `/usr/local/bin/python-upload-watcher.sh` | Watcher ASIR1 (v7) |
| `/usr/local/bin/nginx-user-config-watcher.sh` | Watcher DDAW2 (v3) |
| `/usr/local/bin/xmpp-notify.py` | Bot XMPP one-shot |
### En aldebaran/anka4
| Ruta | Descripción |
|:-----|:------------|
| `~/napi-data/` | sshfs → DDAW2 data |
| `~/napi-data2/` | sshfs → ASIR1 data |
| `~/.config/systemd/user/home-fenix-napi\x2ddata.mount` | Mount DDAW2 |
| `~/.config/systemd/user/home-fenix-napi\x2ddata2.mount` | Mount ASIR1 |
--- ---
## Pendiente ## Pendiente
- [ ] Rellenar notas de pablo (P2.3 → P2.6 pendientes de detallar)
- [ ] Considerar CSS más elaborado para viewer.html (opcional)
- [ ] Renovar certificado SSL wildcard cuando expire (generado 2026-02-04, válido 365 días) - [ ] Renovar certificado SSL wildcard cuando expire (generado 2026-02-04, válido 365 días)

605
README.md
View File

@ -1,17 +1,14 @@
# napi-data — Notas Personalizadas para Estudiantes # napi — Retroalimentación Personalizada a Estudiantes
> Sistema minimalista para servir retroalimentación personalizada a cada alumno, > Sistema minimalista para servir feedback personalizado a cada alumno,
> directamente desde ficheros Markdown editados con Emacs. > directamente desde ficheros Markdown editados con Emacs.
**Estado:** ✅ Producción — `https://notas.qu3v3d0.tech` (2026-02-22) **Estado:** ✅ Producción — 2 grupos activos (2026-02-25)
--- | Grupo | URL | Alumnos |
|:------|:----|:-------:|
## ¿Qué hace esto? | **DDAW2** — Despliegue Aplicaciones Web | `https://notas.qu3v3d0.tech` | 19 |
| **ASIR1** — Programación | `https://asir1.qu3v3d0.tech` | 21 |
Cada alumno entra en `https://notas.TU_DOMINIO` con sus credenciales SFTP y ve
**únicamente sus propias notas** en formato HTML renderizado. El profesor edita
un fichero `.md` desde Emacs y el alumno lo ve al refrescar el navegador.
**Sin backend. Sin base de datos. Sin framework. Sin CDN.** **Sin backend. Sin base de datos. Sin framework. Sin CDN.**
@ -20,400 +17,334 @@ un fichero `.md` desde Emacs y el alumno lo ve al refrescar el navegador.
## Arquitectura ## Arquitectura
``` ```
PROFESOR (aldebaran / máquina local) PROFESOR (aldebaran / anka4)
──────────────────────────────────── ────────────────────────────
~/napi-data/ ← sshfs mount ~/napi-data/ ← sshfs mount (DDAW2)
├── _plantilla/notas.md ├── _plantilla/notas.md
├── alumno01/notas.md ──┐ ├── anas/notas.md
├── alumno02/notas.md ──┤ escribe con Emacs └── ... (19 alumnos)
└── alumnoN/notas.md ──┘ C-x C-s → visible al instante
~/napi-data2/ ← sshfs mount (ASIR1)
├── _plantilla/notas.md
├── barja/notas.md
└── ... (21 alumnos)
│ sshfs (SSH port 22) │ sshfs (SSH port 22)
SERVIDOR (zzz / Debian VPS) SERVIDOR (zzz / qu3v3d0.tech)
──────────────────────────── ─────────────────────────────
/var/www/api/ /var/www/napi/ ← DDAW2
├── viewer.html ← app completa (~40 líneas) ├── viewer.html
├── marked.min.js ← renderer Markdown local (sin CDN) ├── marked.min.js
└── data/ ├── twemoji.min.js
├── alumno01/notas.md └── data/$alumno/notas.md
├── alumno02/notas.md
└── alumnoN/notas.md /var/www/napi2/ ← ASIR1
├── viewer.html
├── marked.min.js
├── twemoji.min.js
└── data/$alumno/notas.md
Nginx + libnginx-mod-http-auth-pam Nginx + libnginx-mod-http-auth-pam
↓ auth con credenciales SFTP del alumno → auth con credenciales SFTP del alumno
↓ $remote_user = "alumno01" → $remote_user → sirve data/$remote_user/notas.md
↓ sirve data/alumno01/notas.md → viewer.html renderiza el .md con marked.js + twemoji
↓ viewer.html lo renderiza en el navegador
ALUMNO (cualquier dispositivo) ALUMNO (cualquier dispositivo)
─────────────────────────────── ──────────────────────────────
https://notas.TU_DOMINIO https://notas.qu3v3d0.tech → DDAW2
→ login con sus credenciales SFTP https://asir1.qu3v3d0.tech → ASIR1
→ login con credenciales SFTP
→ ve sus notas en HTML → ve sus notas en HTML
→ refresca → cambios inmediatos → refresca → cambios inmediatos
``` ```
--- ### Stack
## Gestión de registros DNS | Componente | Tecnología | Dónde |
|:-----------|:-----------|:------|
- Se requiere un https://TU_DOMINIO ad-hoc | **Datos** | Ficheros `notas.md` (Markdown) | zzz |
| **Transporte** | sshfs mounts persistentes (systemd) | aldebaran/anka4 → zzz |
- Por simplicidad, necesitas apuntar TODOS los subodminios a la dirección IP del servidor (usa 'wildcard' - '*') | **Servidor web** | Nginx (1 server block por grupo) | zzz |
| **Autenticación** | `libnginx-mod-http-auth-pam` | zzz |
## Requisitos del servidor (Debian) | **Renderer** | `marked.min.js` + `twemoji.min.js` + `viewer.html` | zzz |
| **Notificaciones DDAW2** | `nginx-user-config-watcher.sh` + XMPP | zzz |
- Debian 11/12/... | **Notificaciones ASIR1** | `python-upload-watcher.sh` (v7) + XMPP | zzz |
- Nginx | **SSL** | Certificado wildcard `*.qu3v3d0.tech` | zzz |
- `libnginx-mod-http-auth-pam` | **DNS** | Wildcard `*.qu3v3d0.tech``161.22.44.104` | DNS |
- `libpam-runtime` (incluido por defecto)
- Certificado SSL (Let's Encrypt o autofirmado)
- Usuarios SFTP ya configurados en el sistema (grupo `sftpusers`) usando 'chroot' , partiendo de que cada estudiante tiene su 'usuario' del sistema.
- SSH accesible desde la máquina del profesor (para sshfs)
--- ---
## Despliegue desde cero en un Debian nuevo ## Grupos y Alumnos
### 1. Instalar dependencias ### DDAW2 — Despliegue de Aplicaciones Web (19 alumnos)
```bash - **URL:** `https://notas.qu3v3d0.tech`
sudo apt update - **Datos:** `~/napi-data/``zzz:/var/www/napi/data/`
sudo apt install -y nginx libnginx-mod-http-auth-pam - **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
``` ```
### 2. Crear estructura de directorios en el servidor ### ASIR1 — Programación (21 alumnos)
```bash - **URL:** `https://asir1.qu3v3d0.tech`
sudo mkdir -p /var/www/api/data - **Datos:** `~/napi-data2/``zzz:/var/www/napi2/data/`
sudo chown -R www-data:www-data /var/www/api - **Usernames:** apellido en minúsculas (sin tildes)
sudo chmod 755 /var/www/api - **Contraseñas:** leet-speak del apellido (`a→4, e→3, i→1, o→0`)
sudo chmod 755 /var/www/api/data - **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)
``` ```
### 3. Desplegar viewer.html y marked.min.js
```bash
# Copiar viewer.html al servidor (ver sección "Ficheros de la app" más abajo)
sudo cp viewer.html /var/www/api/viewer.html
sudo chown www-data:www-data /var/www/api/viewer.html
# Descargar marked.min.js (o copiar desde backup)
# https://cdn.jsdelivr.net/npm/marked/marked.min.js
sudo wget -O /var/www/api/marked.min.js \
https://cdn.jsdelivr.net/npm/marked/marked.min.js
sudo chown www-data:www-data /var/www/api/marked.min.js
``` ```
📁 [Andrés Barrios] ♻️ Re-entrega ASIR1: PRACTICA3.1/ (3 ficheros)
> ⚠️ Una vez descargado `marked.min.js`, la app funciona **sin conexión a CDNs** 🐍 main.py (2100 bytes)
> — es el punto del diseño. No actualices el fichero sin probarlo primero. 📝 README.md (600 bytes)
📄 requisitos.txt (128 bytes)
### 4. Añadir www-data al grupo shadow (para auth_pam)
```bash
sudo usermod -aG shadow www-data
# Reiniciar nginx para que tome el cambio de grupo
sudo systemctl restart nginx
```
### 5. Configurar Nginx
Crear `/etc/nginx/sites-available/napi`:
```nginx
server {
listen 443 ssl;
server_name notas.TU_DOMINIO;
# --- SSL ---
ssl_certificate /etc/ssl/certs/TU_DOMINIO.crt;
ssl_certificate_key /etc/ssl/private/TU_DOMINIO.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /var/www/api;
index viewer.html;
# --- Autenticación PAM ---
auth_pam "Notas DDAW2";
auth_pam_service_name "common-auth";
# --- Servir notas del alumno autenticado ---
location = /notas.md {
alias /var/www/api/data/$remote_user/notas.md;
default_type text/plain;
charset utf-8;
}
# --- Assets estáticos (viewer + marked) ---
location ~* \.(html|js)$ {
auth_pam off; # viewer.html y marked.min.js son públicos
expires 1h;
}
access_log /var/log/nginx/napi-access.log;
error_log /var/log/nginx/napi-error.log;
}
# Redirección HTTP → HTTPS
server {
listen 80;
server_name notas.TU_DOMINIO;
return 301 https://$server_name$request_uri;
}
```
```bash
sudo ln -s /etc/nginx/sites-available/napi /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
### 6. Crear carpeta de datos para cada alumno
```bash
# Para un alumno:
ALUMNO="alumno01"
sudo mkdir -p /var/www/api/data/$ALUMNO
sudo cp /var/www/api/data/_plantilla/notas.md /var/www/api/data/$ALUMNO/notas.md
sudo chown -R www-data:www-data /var/www/api/data/$ALUMNO
# Para todos los alumnos de una vez (si ya existen como usuarios del sistema):
for user in $(getent group sftpusers | cut -d: -f4 | tr ',' ' '); do
sudo mkdir -p /var/www/api/data/$user
sudo cp /var/www/api/data/_plantilla/notas.md /var/www/api/data/$user/notas.md 2>/dev/null || true
sudo chown -R www-data:www-data /var/www/api/data/$user
echo "✅ $user"
done
```
### 7. Verificar que funciona
```bash
# Probar autenticación y respuesta
curl -u alumno01:SU_CONTRASEÑA -sk https://notas.TU_DOMINIO/notas.md | head -5
# Debe devolver las primeras líneas del notas.md del alumno
``` ```
--- ---
## Configurar sshfs en la máquina del profesor ## Workflow del Profesor
### Instalar sshfs
```bash
# Debian/Ubuntu
sudo apt install sshfs
# Fedora/RHEL
sudo dnf install fuse-sshfs
```
### Crear punto de montaje
```bash
mkdir -p ~/napi-data
```
### Montar manualmente (prueba)
```bash
sshfs USUARIO@TU_SERVIDOR:/var/www/api/data ~/napi-data \
-o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3
```
### Montaje persistente con systemd (recomendado)
Crear `~/.config/systemd/user/home-TUUSUARIO-napi\x2ddata.mount`
(sustituye `TUUSUARIO` por tu usuario real, ej: `fenix`):
```ini
[Unit]
Description=napi-data → servidor:/var/www/api/data (sshfs)
After=network-online.target
[Mount]
What=USUARIO@TU_SERVIDOR:/var/www/api/data
Where=/home/TUUSUARIO/napi-data
Type=fuse.sshfs
Options=reconnect,ServerAliveInterval=15,ServerAliveCountMax=3
[Install]
WantedBy=default.target
```
```bash
systemctl --user daemon-reload
systemctl --user enable --now 'home-TUUSUARIO-napi\x2ddata.mount'
# Verificar
systemctl --user status 'home-TUUSUARIO-napi\x2ddata.mount'
```
> 💡 El nombre de la unit debe coincidir exactamente con la ruta del `Where`
> (reemplazando `/` por `-` y los guiones por `\x2d`).
---
## Workflow del profesor
``` ```
1. C-x C-f ~/napi-data/pablo/notas.md ← abrir en Emacs 1. C-x C-f ~/napi-data2/barrios/notas.md ← abrir en Emacs
2. Editar feedback, notas, próximos pasos 2. Editar feedback, notas, próximos pasos
3. C-x C-s ← guardar 3. C-x C-s ← guardar
4. Alumno refresca el navegador ← cambios visibles 4. Alumno refresca el navegador ← cambios visibles
``` ```
**No hay paso 5.** **No hay paso 5.** El sshfs hace que guardar localmente sea equivalente a escribir en zzz.
--- ---
## Añadir un alumno nuevo ## 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 ```bash
# En el servidor: sudo mkdir -p /var/www/napiN/data
ALUMNO="nuevo_alumno" sudo cp /var/www/napi/viewer.html /var/www/napiN/
sudo mkdir -p /var/www/api/data/$ALUMNO sudo cp /var/www/napi/marked.min.js /var/www/napiN/
sudo cp /var/www/api/data/_plantilla/notas.md /var/www/api/data/$ALUMNO/notas.md sudo cp /var/www/napi/twemoji.min.js /var/www/napiN/
sudo chown -R www-data:www-data /var/www/api/data/$ALUMNO sudo chown fenix:www-data /var/www/napiN/data
sudo chmod 775 /var/www/napiN/data
# En la máquina del profesor (aparece automáticamente via sshfs):
ls ~/napi-data/$ALUMNO/ # → notas.md
# Editar con Emacs y personalizar
``` ```
--- ### 2. En zzz: crear server block Nginx
## Formato del fichero notas.md ```nginx
server {
listen 80;
server_name SUBDOMINIO.qu3v3d0.tech;
return 301 https://$server_name$request_uri;
}
```markdown server {
# Notas — NombreAlumno listen 443 ssl;
server_name SUBDOMINIO.qu3v3d0.tech;
> 🏫 **Módulo:** NOMBRE_MODULO ssl_certificate /etc/ssl/certs/qu3v3d0.tech.crt;
> 📅 **Última actualización:** YYYY-MM-DD ssl_certificate_key /etc/ssl/private/qu3v3d0.tech.key;
--- root /var/www/napiN;
## 📊 Resumen auth_pam "Notas GRUPO";
auth_pam_service_name "common-auth";
| Práctica | Título | Nota | Estado | location = / { try_files /viewer.html =404; }
|:---------|:-------|:----:|:------:|
| P2.3 | Nginx via SFTP | 7/10 | ✅ |
| P2.4 | HTTP y HTTPS | 8/10 | ✅ |
| P2.7 | Multi-sitio | 9/10 | ✅ |
--- 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";
}
## P2.7 — Multi-sitio Web con Nginx location ~* \.(js|css)$ { expires 7d; }
location / { return 404; }
**Nota: 9/10** · _Escaneado: 2026-02-19_ }
NombreAlumno, [feedback personalizado].
### Criterios
| Criterio | Puntos | Estado |
|:---------|:------:|:------:|
| Hextris desplegado + HTTPS | 1,5/1,5 | ✅ |
| Gzip | 1/1 | ✅ |
| CSP correcta | 1/1,5 | ⚠️ unsafe-inline |
### Próximos pasos
1. Corregir CSP: eliminar `unsafe-inline`
2. Subir capturas de verificación
``` ```
Ver `_plantilla/notas.md` como punto de partida para nuevos alumnos.
---
## Ficheros de la app (en el servidor)
### `viewer.html`
App completa en ~40 líneas. Fetcha `/notas.md` (que Nginx resuelve al fichero
del alumno autenticado) y lo renderiza con `marked.min.js`:
```html
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notas</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ccc; padding: 0.5rem 1rem; text-align: left; }
th { background: #f4f4f4; }
code { background: #f0f0f0; padding: 0.1em 0.4em; border-radius: 3px; }
pre code { display: block; padding: 1rem; overflow-x: auto; }
blockquote { border-left: 4px solid #ccc; margin: 0; padding-left: 1rem; color: #555; }
</style>
</head>
<body>
<div id="content">Cargando...</div>
<script src="marked.min.js"></script>
<script>
fetch('/notas.md')
.then(r => r.ok ? r.text() : Promise.reject(r.status))
.then(md => { document.getElementById('content').innerHTML = marked.parse(md); })
.catch(e => { document.getElementById('content').textContent = 'Error: ' + e; });
</script>
</body>
</html>
```
### `marked.min.js`
Descargar de https://cdn.jsdelivr.net/npm/marked/marked.min.js y guardar como
fichero local en `/var/www/api/marked.min.js`. No referenciar CDN externo
(rompe la CSP y crea dependencia de terceros).
---
## Gestión del mount sshfs
```bash ```bash
# Estado sudo ln -s /etc/nginx/sites-available/napiN /etc/nginx/sites-enabled/
systemctl --user status 'home-TUUSUARIO-napi\x2ddata.mount' sudo nginx -t && sudo systemctl reload nginx
```
# Montar ### 3. En zzz: crear usuarios SFTP
systemctl --user start 'home-TUUSUARIO-napi\x2ddata.mount'
# Desmontar ```bash
systemctl --user stop 'home-TUUSUARIO-napi\x2ddata.mount' 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
```
# Si se cuelga (mount zombie): ### 4. Copiar datos de alumnos y montar sshfs
fusermount -uz ~/napi-data
systemctl --user start 'home-TUUSUARIO-napi\x2ddata.mount' ```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 ## Troubleshooting
| Síntoma | Causa probable | Solución | | Problema | Causa | Solución |
|:--------|:---------------|:---------| |:---------|:------|:---------|
| `403 Forbidden` al acceder | www-data no está en grupo shadow | `sudo usermod -aG shadow www-data && sudo systemctl restart nginx` | | `403 Forbidden` | www-data no en grupo shadow | `sudo usermod -aG shadow www-data && sudo systemctl restart nginx` |
| `401 Unauthorized` con credenciales correctas | PAM no configurado | Verificar que `auth_pam_service_name "common-auth"` existe en `/etc/pam.d/` | | `401 Unauthorized` | Credenciales incorrectas o PAM mal configurado | Verificar `/etc/pam.d/common-auth` |
| `/notas.md` devuelve 404 | Carpeta del alumno no existe en `data/` | `sudo mkdir -p /var/www/api/data/$ALUMNO` | | `twemoji is not defined` | Falta `twemoji.min.js` en el root del site | `sudo cp /var/www/napi/twemoji.min.js /var/www/napiN/` |
| sshfs mount desaparece | Pérdida de conexión SSH | La opción `reconnect` lo recupera solo; si no, `systemctl --user restart` | | `404` en `/notas.md` | Carpeta del alumno no existe en `data/` | Crear carpeta + copiar plantilla |
| El alumno ve las notas de otro | `$remote_user` vacío o PAM no activo | Verificar que `auth_pam` y `auth_pam_service_name` están en el bloque correcto | | sshfs zombie | Conexión SSH caída | `fusermount -uz ~/napi-dataN && systemctl --user restart ...mount` |
| `marked is not defined` | `marked.min.js` no accesible | Verificar que el fichero existe en `/var/www/api/` y tiene permisos de lectura | | 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 ## Seguridad
- Cada alumno solo ve sus propias notas — Nginx resuelve el path con `$remote_user` - 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 - Sin ejecución de código en el servidor — todo es estático
- `marked.min.js` local — sin dependencia de CDNs externos - `marked.min.js` y `twemoji.min.js` locales — sin dependencia de CDNs externos
- Credenciales: las mismas que usa el alumno para subir archivos por SFTP - Credenciales: las mismas que usa el alumno para SFTP (FileZilla)
- SSL/TLS obligatorio (redirección 301 desde HTTP) - SSL/TLS obligatorio (redirección 301 desde HTTP)
- SFTP con chroot — alumnos no pueden ver otros directorios
--- ---
@ -421,5 +352,7 @@ systemctl --user start 'home-TUUSUARIO-napi\x2ddata.mount'
| Fecha | Hito | | Fecha | Hito |
|:------|:-----| |:------|:-----|
| 2026-02-22 | MVP desplegado: sshfs + Nginx auth_pam + viewer.html + marked.js | | 2026-02-22 | MVP desplegado: notas.qu3v3d0.tech para DDAW2 (19 alumnos) |
| 2026-02-22 | notas.md generadas para los 19 alumnos de DDAW2 | | 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) |

View File

@ -18,7 +18,7 @@ Copia de referencia de todos los scripts y unidades systemd desplegados en **zzz
--- ---
## 🐍 python-upload-watcher.sh (v5) ## 🐍 python-upload-watcher.sh (v7)
**Propósito:** Monitorizar las entregas de prácticas de Programación (ASIR1) subidas por SFTP. **Propósito:** Monitorizar las entregas de prácticas de Programación (ASIR1) subidas por SFTP.
@ -26,13 +26,16 @@ Copia de referencia de todos los scripts y unidades systemd desplegados en **zzz
- ✅ **Batching por carpeta** — Cuando un alumno sube `PRACTICA3.1/` con N ficheros, envía **un solo mensaje XMPP** con el listado completo (espera 10s de silencio) - ✅ **Batching por carpeta** — Cuando un alumno sube `PRACTICA3.1/` con N ficheros, envía **un solo mensaje XMPP** con el listado completo (espera 10s de silencio)
- ♻️ **Re-entregas** — Si el alumno borra y re-sube la misma carpeta (<120s), etiqueta como "Re-entrega" - ♻️ **Re-entregas** — Si el alumno borra y re-sube la misma carpeta (<120s), etiqueta como "Re-entrega"
- 👤 **Nombre completo** — Notifica `[María Jara]` en vez de `[jara]` (mapa usuario → nombre en el script)
- 🪟 **Windows-safe** — Ignora `Thumbs.db`, `desktop.ini`, `.DS_Store`, `*.tmp`, `~$*` - 🪟 **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/`
- 📁 **Iconos por tipo** — 🐍 `.py` · 📦 `.zip/.rar` · 📝 `.md` · 📄 `.txt` · 📕 `.pdf` · 📘 `.docx` - 📁 **Iconos por tipo** — 🐍 `.py` · 📦 `.zip/.rar` · 📝 `.md` · 📄 `.txt` · 📕 `.pdf` · 📘 `.docx`
### Ejemplo de notificación XMPP ### Ejemplo de notificación XMPP
``` ```
📁 [barrios] Entrega ASIR1: PRACTICA3.1/ (4 ficheros) 📁 [Andrés Barrios] Entrega ASIR1: PRACTICA3.1/ (4 ficheros)
🐍 main.py (2048 bytes) 🐍 main.py (2048 bytes)
🐍 utils.py (1024 bytes) 🐍 utils.py (1024 bytes)
📝 README.md (512 bytes) 📝 README.md (512 bytes)
@ -40,7 +43,7 @@ Copia de referencia de todos los scripts y unidades systemd desplegados en **zzz
``` ```
``` ```
📁 [barrios] ♻️ Re-entrega ASIR1: PRACTICA3.1/ (3 ficheros) 📁 [Andrés Barrios] ♻️ Re-entrega ASIR1: PRACTICA3.1/ (3 ficheros)
🐍 main.py (2100 bytes) 🐍 main.py (2100 bytes)
📝 README.md (600 bytes) 📝 README.md (600 bytes)
📄 requisitos.txt (128 bytes) 📄 requisitos.txt (128 bytes)
@ -160,6 +163,6 @@ systemctl --user stop home-fenix-napi\\x2ddata2.mount
| Script | Versión | Fecha | Cambios | | Script | Versión | Fecha | Cambios |
|:-------|:--------|:------|:--------| |:-------|:--------|:------|:--------|
| `python-upload-watcher.sh` | **v5** | 2026-02-25 | Batching + re-entregas + Windows-safe + fix `local` en subshell | | `python-upload-watcher.sh` | **v7** | 2026-02-25 | Batching + re-entregas + Windows-safe + `__pycache__` filter + nombres completos |
| `nginx-user-config-watcher.sh` | **v3** | 2026-02-19 | Multi-sitio + undeploy + CSP analysis | | `nginx-user-config-watcher.sh` | **v3** | 2026-02-19 | Multi-sitio + undeploy + CSP analysis |
| `xmpp-notify.py` | **v1** | 2026-01-27 | Bot one-shot slixmpp | | `xmpp-notify.py` | **v1** | 2026-01-27 | Bot one-shot slixmpp |