diff --git a/CLAUDE.md b/CLAUDE.md index 8b906ae..04bb59f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 -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: - 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. +**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) -───────────────── ────────────────── -~/napi-data/ sshfs /var/www/napi/data/ - ├── anas/notas.md ←──────────→ ├── anas/notas.md - ├── pablo/notas.md ├── pablo/notas.md - ├── miguel/notas.md ├── miguel/notas.md - └── .../ (19 alumnos) └── .../ (19 alumnos) +PROFESOR (aldebaran / anka4) SERVIDOR (zzz / qu3v3d0.tech) +──────────────────────────── ───────────────────────────── +~/napi-data/ ←── sshfs ──→ /var/www/napi/data/ (DDAW2, 19 alumnos) +~/napi-data2/ ←── sshfs ──→ /var/www/napi2/data/ (ASIR1, 21 alumnos) - /var/www/napi/ - ├── viewer.html (app completa) - └── marked.min.js (renderer local) + /var/www/napi[2]/ + ├── viewer.html + ├── marked.min.js + └── twemoji.min.js -Nginx + libnginx-mod-http-auth-pam - → auth con credenciales SFTP del alumno - → $remote_user → sirve /var/www/napi/data/$remote_user/notas.md - → viewer.html renderiza el .md en el navegador (marked.js local) + Nginx + auth_pam → $remote_user + → data/$remote_user/notas.md + → viewer.html renderiza con marked.js ``` ### Stack | Componente | Tecnología | Dónde | |:-----------|:-----------|:------| -| **Datos** | Ficheros `notas.md` (Markdown) | `zzz:/var/www/napi/data/` | -| **Transporte** | sshfs mount persistente | aldebaran → zzz | -| **Servidor web** | Nginx | zzz | +| **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` (local, sin CDN) + `viewer.html` | zzz | -| **Notificaciones** (watcher nginx) | Python + slixmpp → XMPP | zzz | - -**Sin backend. Sin Python para servir. Sin JSON. Sin Syncthing.** +| **Renderer** | `marked.min.js` + `twemoji.min.js` + `viewer.html` | zzz | +| **Notificaciones DDAW2** | `nginx-user-config-watcher.sh` (v3) + XMPP | zzz | +| **Notificaciones ASIR1** | `python-upload-watcher.sh` (v7) + XMPP | zzz | +| **SSL** | Certificado wildcard `*.qu3v3d0.tech` | zzz | --- -## URL de Acceso +## Alumnos -``` -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 +### DDAW2 (19) — usernames = nombre de pila ``` anas, carlos, carlosv, daniel, danieln, erick, evelin, gianfranco, @@ -137,48 +71,53 @@ giorgio, joel, jorge, josue, juanan, juanjesus, kasandra, marius, 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 ```bash -ssh fenix@qu3v3d0.tech # clave sin -i -``` - -`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 +ssh fenix@qu3v3d0.tech # clave sin -i, fenix tiene sudo ``` --- -## Prácticas Documentadas +## Ficheros Clave -| Práctica | Directorio local | Descripción | -|:---------|:-----------------|:------------| -| P2.3 | `~/EducaMadrid/DDAW2/PRACTICA2.3-despliegue-nginx-via-sftp/` | Nginx config via SFTP + watcher inotify + XMPP | -| 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 | -| P2.6 | `~/EducaMadrid/DDAW2/PRACTICA2.6-Hextris-compresion-cache-y-mejora-de-rendimiento/` | Hextris + gzip + caché | -| P2.7 | `~/EducaMadrid/DDAW2/PRACTICA2.7-MULTI-SITIO-WEB-NGINX@zzz/` | Multi-sitio: Hextris + App propia + CSP + cabeceras | +### En zzz + +| Ruta | Descripción | +|:-----|:------------| +| `/var/www/napi/` | App DDAW2 (viewer + marked + twemoji + data/) | +| `/var/www/napi2/` | App ASIR1 (idem) | +| `/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 -- [ ] 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) diff --git a/README.md b/README.md index 73e2d0e..9167235 100644 --- a/README.md +++ b/README.md @@ -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. -**Estado:** ✅ Producción — `https://notas.qu3v3d0.tech` (2026-02-22) +**Estado:** ✅ Producción — 2 grupos activos (2026-02-25) ---- - -## ¿Qué hace esto? - -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. +| 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.** @@ -20,400 +17,334 @@ un fichero `.md` desde Emacs y el alumno lo ve al refrescar el navegador. ## Arquitectura ``` -PROFESOR (aldebaran / máquina local) -──────────────────────────────────── -~/napi-data/ ← sshfs mount - ├── _plantilla/notas.md - ├── alumno01/notas.md ──┐ - ├── alumno02/notas.md ──┤ escribe con Emacs - └── alumnoN/notas.md ──┘ C-x C-s → visible al instante - - │ sshfs (SSH port 22) - ▼ - -SERVIDOR (zzz / Debian VPS) +PROFESOR (aldebaran / anka4) ──────────────────────────── -/var/www/api/ - ├── viewer.html ← app completa (~40 líneas) - ├── marked.min.js ← renderer Markdown local (sin CDN) - └── data/ - ├── alumno01/notas.md - ├── alumno02/notas.md - └── alumnoN/notas.md +~/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 = "alumno01" - ↓ sirve data/alumno01/notas.md - ↓ viewer.html lo renderiza en el navegador + → 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.TU_DOMINIO - → login con sus credenciales SFTP +────────────────────────────── +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 | + --- -## Gestión de registros DNS +## Grupos y Alumnos -- Se requiere un https://TU_DOMINIO ad-hoc +### DDAW2 — Despliegue de Aplicaciones Web (19 alumnos) -- Por simplicidad, necesitas apuntar TODOS los subodminios a la dirección IP del servidor (usa 'wildcard' - '*') +- **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/` -## Requisitos del servidor (Debian) +``` +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` -- `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. +- 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 desde cero en un Debian nuevo +## Despliegue Rápido de un Nuevo Grupo -### 1. Instalar dependencias +### 1. En zzz: crear la app web ```bash -sudo apt update -sudo apt install -y nginx libnginx-mod-http-auth-pam +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. Crear estructura de directorios en el servidor - -```bash -sudo mkdir -p /var/www/api/data -sudo chown -R www-data:www-data /var/www/api -sudo chmod 755 /var/www/api -sudo chmod 755 /var/www/api/data -``` - -### 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 -``` - -> ⚠️ Una vez descargado `marked.min.js`, la app funciona **sin conexión a CDNs** -> — es el punto del diseño. No actualices el fichero sin probarlo primero. - -### 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`: +### 2. En zzz: crear server block Nginx ```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; + listen 80; + server_name SUBDOMINIO.qu3v3d0.tech; + return 301 https://$server_name$request_uri; } -# Redirección HTTP → HTTPS server { - listen 80; - server_name notas.TU_DOMINIO; - return 301 https://$server_name$request_uri; + 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/napi /etc/nginx/sites-enabled/ +sudo ln -s /etc/nginx/sites-available/napiN /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx ``` -### 6. Crear carpeta de datos para cada alumno +### 3. En zzz: crear usuarios SFTP ```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" +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 ``` -### 7. Verificar que funciona +### 4. Copiar datos de alumnos y montar sshfs ```bash -# Probar autenticación y respuesta -curl -u alumno01:SU_CONTRASEÑA -sk https://notas.TU_DOMINIO/notas.md | head -5 +# Copiar carpetas con notas.md al servidor +scp -r ~/napi-dataN/* fenix@qu3v3d0.tech:/var/www/napiN/data/ -# Debe devolver las primeras líneas del notas.md del alumno -``` - ---- - -## Configurar sshfs en la máquina 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 +# Crear unit systemd sshfs (~/.config/systemd/user/) systemctl --user daemon-reload -systemctl --user enable --now 'home-TUUSUARIO-napi\x2ddata.mount' - -# Verificar -systemctl --user status 'home-TUUSUARIO-napi\x2ddata.mount' +systemctl --user enable --now 'home-fenix-napi\x2ddataN.mount' ``` -> 💡 El nombre de la unit debe coincidir exactamente con la ruta del `Where` -> (reemplazando `/` por `-` y los guiones por `\x2d`). - --- -## Workflow del profesor +## Estructura del Repositorio ``` -1. C-x C-f ~/napi-data/pablo/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 +~/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 ``` -**No hay paso 5.** - --- -## Añadir un alumno nuevo +## Conexión a zzz ```bash -# En el servidor: -ALUMNO="nuevo_alumno" -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 - -# En la máquina del profesor (aparece automáticamente via sshfs): -ls ~/napi-data/$ALUMNO/ # → notas.md -# Editar con Emacs y personalizar +ssh fenix@qu3v3d0.tech # clave sin -i, fenix tiene sudo ``` --- -## Formato del fichero notas.md +## Gestión de Servicios -```markdown -# Notas — NombreAlumno - -> 🏫 **Módulo:** NOMBRE_MODULO -> 📅 **Última actualización:** YYYY-MM-DD - ---- - -## 📊 Resumen - -| Práctica | Título | Nota | Estado | -|:---------|:-------|:----:|:------:| -| P2.3 | Nginx via SFTP | 7/10 | ✅ | -| P2.4 | HTTP y HTTPS | 8/10 | ✅ | -| P2.7 | Multi-sitio | 9/10 | ✅ | - ---- - -## P2.7 — Multi-sitio Web con Nginx - -**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 - - - - - - Notas - - - -
Cargando...
- - - - -``` - -### `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 +### Watchers (en zzz) ```bash -# Estado -systemctl --user status 'home-TUUSUARIO-napi\x2ddata.mount' +sudo systemctl status python-upload-watcher.service # ASIR1 +sudo systemctl status nginx-user-config-watcher.service # DDAW2 +sudo systemctl restart python-upload-watcher.service +``` -# Montar -systemctl --user start 'home-TUUSUARIO-napi\x2ddata.mount' +### Mounts sshfs (en aldebaran/anka4) -# Desmontar -systemctl --user stop 'home-TUUSUARIO-napi\x2ddata.mount' +```bash +systemctl --user status home-fenix-napi\\x2ddata.mount # DDAW2 +systemctl --user status home-fenix-napi\\x2ddata2.mount # ASIR1 +``` -# Si se cuelga (mount zombie): -fusermount -uz ~/napi-data -systemctl --user start 'home-TUUSUARIO-napi\x2ddata.mount' +### 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 -| Síntoma | Causa probable | Solución | -|:--------|:---------------|:---------| -| `403 Forbidden` al acceder | www-data no está 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/` | -| `/notas.md` devuelve 404 | Carpeta del alumno no existe en `data/` | `sudo mkdir -p /var/www/api/data/$ALUMNO` | -| sshfs mount desaparece | Pérdida de conexión SSH | La opción `reconnect` lo recupera solo; si no, `systemctl --user restart` | -| 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 | -| `marked is not defined` | `marked.min.js` no accesible | Verificar que el fichero existe en `/var/www/api/` y tiene permisos de lectura | +| 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` +- 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` local — sin dependencia de CDNs externos -- Credenciales: las mismas que usa el alumno para subir archivos por SFTP +- `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 --- @@ -421,5 +352,7 @@ systemctl --user start 'home-TUUSUARIO-napi\x2ddata.mount' | Fecha | Hito | |:------|:-----| -| 2026-02-22 | MVP desplegado: sshfs + Nginx auth_pam + viewer.html + marked.js | -| 2026-02-22 | notas.md generadas para los 19 alumnos de DDAW2 | +| 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) | diff --git a/scripts/README.md b/scripts/README.md index d1a1602..9230423 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -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. @@ -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) - ♻️ **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`, `~$*` +- 🐍 **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` ### 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) 🐍 utils.py (1024 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) 📝 README.md (600 bytes) 📄 requisitos.txt (128 bytes) @@ -160,6 +163,6 @@ systemctl --user stop home-fenix-napi\\x2ddata2.mount | 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 | | `xmpp-notify.py` | **v1** | 2026-01-27 | Bot one-shot slixmpp |