# napi-data — Notas Personalizadas para Estudiantes > Sistema minimalista para servir retroalimentación personalizada a cada alumno, > directamente desde ficheros Markdown editados con Emacs. **Estado:** ✅ Producción — `https://notas.qu3v3d0.tech` (2026-02-22) --- ## ¿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. **Sin backend. Sin base de datos. Sin framework. Sin CDN.** --- ## 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) ──────────────────────────── /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 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 ALUMNO (cualquier dispositivo) ─────────────────────────────── https://notas.TU_DOMINIO → login con sus credenciales SFTP → ve sus notas en HTML → refresca → cambios inmediatos ``` --- ## Gestión de registros DNS - Se requiere un https://TU_DOMINIO ad-hoc - Por simplicidad, necesitas apuntar TODOS los subodminios a la dirección IP del servidor (usa 'wildcard' - '*') ## 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. - SSH accesible desde la máquina del profesor (para sshfs) --- ## Despliegue desde cero en un Debian nuevo ### 1. Instalar dependencias ```bash sudo apt update sudo apt install -y nginx libnginx-mod-http-auth-pam ``` ### 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`: ```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 ### 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 2. Editar feedback, notas, próximos pasos 3. C-x C-s ← guardar 4. Alumno refresca el navegador ← cambios visibles ``` **No hay paso 5.** --- ## Añadir un alumno nuevo ```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 ``` --- ## Formato del fichero notas.md ```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