# 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) |
