docs: add multi-file notes and mount watchdog to README

- Document ?f= param for internal .md links in viewer.html
- Add nginx location block for per-user .md files to deploy template
- Document mount watchdog timer (auto-recovery every 12min)
- Update nginx reference copies with new .md location block
- Update repo structure and history

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
fenix 2026-03-10 15:30:21 +01:00
parent ee0ab52218
commit 3fa2b7a5d7
4 changed files with 103 additions and 7 deletions

View File

@ -169,6 +169,59 @@ Monitoriza `/home/USER/python/` — detecta entregas de prácticas de Programaci
--- ---
## Notas Multi-fichero (enlaces internos)
El viewer soporta distribuir el feedback en múltiples ficheros `.md` por alumno. Útil cuando `notas.md` crece demasiado con el feedback acumulado de muchas prácticas.
### Estructura
```
data/barrios/
├── notas.md ← TOC con tabla resumen y enlaces
├── nota-p3.1-sqlite.md ← feedback detallado P3.1
├── nota-p3.2-funciones.md ← feedback detallado P3.2
└── nota-p3.3-ficheros.md ← feedback detallado P3.3
```
### Ejemplo de `notas.md` con enlaces
```markdown
# Notas — Andrés Barrios
| **Práctica** | **Nota** | **Estado** |
|:-------------|:--------:|:----------:|
| P3.1 — [SQLite](nota-p3.1-sqlite.md) | 8 | Entregada |
| P3.2 — [Funciones](nota-p3.2-funciones.md) | 7 | Entregada |
| P3.3 — [Ficheros](nota-p3.3-ficheros.md) | — | Pendiente |
| **TOTAL** | **7.5** | |
```
### Ejemplo de sub-nota (`nota-p3.1-sqlite.md`)
```markdown
# Práctica 3.1 — SQLite
## Feedback
Buen uso de SELECT con WHERE compuestos. Normalización correcta hasta 3FN.
### A mejorar
- Revisar los JOINs entre tablas con FK compuestas
**Nota: 8/10**
[← Volver a notas](notas.md)
```
### Cómo funciona
1. `viewer.html` lee el parámetro `?f=` de la URL (por defecto `notas.md`)
2. Los enlaces internos a `.md` se reescriben automáticamente a `/?f=nombre.md`
3. Nginx sirve cualquier `.md` del directorio del alumno autenticado
4. El alumno navega entre ficheros sin salir del viewer — el botón atrás del navegador funciona
5. **Compatible hacia atrás** — si solo existe `notas.md`, todo funciona igual que antes
---
## Requisitos del Servidor (Debian) ## Requisitos del Servidor (Debian)
- Debian 11/12/... - Debian 11/12/...
@ -225,6 +278,14 @@ server {
add_header Cache-Control "no-cache"; add_header Cache-Control "no-cache";
} }
# Notas multi-fichero (enlaces internos desde notas.md)
location ~ ^/(.+\.md)$ {
alias /var/www/napiN/data/$remote_user/$1;
default_type text/plain;
charset utf-8;
add_header Cache-Control "no-cache";
}
location ~* \.(js|css)$ { expires 7d; } location ~* \.(js|css)$ { expires 7d; }
location / { return 404; } location / { return 404; }
} }
@ -282,7 +343,9 @@ systemctl --user enable --now 'home-fenix-napi\x2ddataN.mount'
│ ├── python-upload-watcher.service ← unit systemd │ ├── python-upload-watcher.service ← unit systemd
│ ├── nginx-user-config-watcher.service │ ├── nginx-user-config-watcher.service
│ ├── home-fenix-napi-data.mount ← sshfs DDAW2 │ ├── home-fenix-napi-data.mount ← sshfs DDAW2
│ └── home-fenix-napi-data2.mount ← sshfs ASIR1 │ ├── home-fenix-napi-data2.mount ← sshfs ASIR1
│ ├── napi-mount-watchdog.service ← watchdog auto-recovery
│ └── napi-mount-watchdog.timer ← timer cada 12 min
└── Screenshots/ └── Screenshots/
└── zzz@librebits.info.png ← ejemplo notificaciones XMPP └── zzz@librebits.info.png ← ejemplo notificaciones XMPP
``` ```
@ -314,6 +377,15 @@ systemctl --user status home-fenix-napi\\x2ddata.mount # DDAW2
systemctl --user status home-fenix-napi\\x2ddata2.mount # ASIR1 systemctl --user status home-fenix-napi\\x2ddata2.mount # ASIR1
``` ```
### Watchdog de mounts (en aldebaran/anka4)
Si zzz se reinicia o la conexión SSH se corta, los mounts quedan en estado `failed` permanentemente. El watchdog los detecta y reinicia automáticamente cada 12 minutos.
```bash
systemctl --user status napi-mount-watchdog.timer
systemctl --user list-timers napi-mount-watchdog.timer
```
### Logs ### Logs
```bash ```bash
@ -331,7 +403,7 @@ sudo tail -f /var/log/nginx/napi-error.log # Nginx errors
| `401 Unauthorized` | Credenciales incorrectas o PAM mal configurado | Verificar `/etc/pam.d/common-auth` | | `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/` | | `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 | | `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` | | sshfs zombie | Conexión SSH caída | `fusermount -uz ~/napi-dataN && systemctl --user restart ...mount` (el watchdog lo hace automáticamente cada 12 min) |
| Emacs muestra datos stale tras editar | sshfs cachea lecturas del kernel | Añadir `auto_cache` a las Options del mount (ver abajo) | | Emacs muestra datos stale tras editar | sshfs cachea lecturas del kernel | Añadir `auto_cache` a las Options del mount (ver abajo) |
| Watcher no detecta re-entregas | Versión antigua del watcher | Actualizar a v7+ y reiniciar servicio | | Watcher no detecta re-entregas | Versión antigua del watcher | Actualizar a v7+ y reiniciar servicio |
| `__pycache__` en notificaciones | Watcher < v6 | Actualizar a v7+ | | `__pycache__` en notificaciones | Watcher < v6 | Actualizar a v7+ |
@ -357,4 +429,6 @@ sudo tail -f /var/log/nginx/napi-error.log # Nginx errors
| 2026-02-25 | ASIR1 desplegado: asir1.qu3v3d0.tech para Programación (21 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 | | 2026-02-25 | Watcher ASIR1 v7: batching, re-entregas, Windows-safe, __pycache__ filter, nombres completos |
| 2026-03-02 | sshfs mounts: añadido `auto_cache` para evitar datos stale en Emacs | | 2026-03-02 | sshfs mounts: añadido `auto_cache` para evitar datos stale en Emacs |
| 2026-03-10 | Watchdog timer para auto-recovery de mounts sshfs caídos |
| 2026-03-10 | Soporte notas multi-fichero: viewer.html `?f=` + nginx `location ~ .md` |
| 2027-02-04 | Renovar certificado SSL wildcard (caduca ~365 días desde 2026-02-04) | | 2027-02-04 | Renovar certificado SSL wildcard (caduca ~365 días desde 2026-02-04) |

View File

@ -1,6 +1,6 @@
# napi — Notas API (notas.qu3v3d0.tech) # napi — Notas API (notas.qu3v3d0.tech)
# Auth: PAM (mismas credenciales que SFTP) # Auth: PAM (mismas credenciales que SFTP)
# Datos: /var/www/api/data/$remote_user/notas.md # Datos: /var/www/napi/data/$remote_user/notas.md
server { server {
listen 80; listen 80;
@ -15,7 +15,7 @@ server {
ssl_certificate /etc/ssl/certs/qu3v3d0.tech.crt; ssl_certificate /etc/ssl/certs/qu3v3d0.tech.crt;
ssl_certificate_key /etc/ssl/private/qu3v3d0.tech.key; ssl_certificate_key /etc/ssl/private/qu3v3d0.tech.key;
root /var/www/api; root /var/www/napi;
auth_pam "Notas DDAW2"; auth_pam "Notas DDAW2";
auth_pam_service_name "common-auth"; auth_pam_service_name "common-auth";
@ -25,7 +25,7 @@ server {
} }
location = /notas.md { location = /notas.md {
alias /var/www/api/data/$remote_user/notas.md; alias /var/www/napi/data/$remote_user/notas.md;
default_type text/plain; default_type text/plain;
charset utf-8; charset utf-8;
add_header Cache-Control "no-cache"; add_header Cache-Control "no-cache";
@ -35,6 +35,13 @@ server {
expires 7d; expires 7d;
} }
location ~ ^/(.+\.md)$ {
alias /var/www/napi/data/$remote_user/$1;
default_type text/plain;
charset utf-8;
add_header Cache-Control "no-cache";
}
location / { location / {
return 404; return 404;
} }

View File

@ -35,6 +35,13 @@ server {
expires 7d; expires 7d;
} }
location ~ ^/(.+\.md)$ {
alias /var/www/napi2/data/$remote_user/$1;
default_type text/plain;
charset utf-8;
add_header Cache-Control "no-cache";
}
location / { location / {
return 404; return 404;
} }

View File

@ -20,9 +20,17 @@
<body> <body>
<div id="out">Cargando...</div> <div id="out">Cargando...</div>
<script> <script>
fetch('/notas.md') const file = new URLSearchParams(location.search).get('f') || 'notas.md';
fetch('/' + file)
.then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(md => document.getElementById('out').innerHTML = marked.parse(md)) .then(md => {
document.getElementById('out').innerHTML = marked.parse(md);
// Rewrite internal .md links to render through viewer
document.querySelectorAll('#out a[href$=".md"]').forEach(a => {
const href = a.getAttribute('href');
if (!href.startsWith('http')) a.href = '/?f=' + href;
});
})
.catch(e => document.getElementById('out').textContent = 'Error cargando notas: ' + e); .catch(e => document.getElementById('out').textContent = 'Error cargando notas: ' + e);
</script> </script>
</body> </body>