Compare commits
1 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
cf31824f03 |
148
PLAN.md
148
PLAN.md
|
|
@ -1,148 +0,0 @@
|
||||||
# Plan: Emacs Integration for napi — Remote File-Notify via TRAMP
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
Fénix wants **real-time awareness in Emacs** of student deliveries on zzz (qu3v3d0.tech). The GNU Emacs manual confirms that `file-notify-add-watch` works on **remote machines** via TRAMP — it runs `inotifywait` on the remote host through SSH and streams events back in real-time. No polling.
|
|
||||||
|
|
||||||
This means: **no modifications to watcher scripts on zzz**, no reverse SSH tunnels, no Porthole/JSON-RPC. Pure Emacs, using built-in capabilities + a dedicated daemon.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
emacs --daemon=napi (local)
|
|
||||||
│
|
|
||||||
├── TRAMP SSH → zzz:/home/*/python/ (ASIR1 — 21 dirs)
|
|
||||||
│ └── inotifywait running on zzz (auto-started by TRAMP)
|
|
||||||
│
|
|
||||||
├── TRAMP SSH → zzz:/home/*/html/ (DDAW2 — 19 dirs)
|
|
||||||
│ └── inotifywait running on zzz (auto-started by TRAMP)
|
|
||||||
│
|
|
||||||
└── On file event → forward notification to `water` daemon
|
|
||||||
├── Desktop notification (D-Bus)
|
|
||||||
├── Sound alert (paplay)
|
|
||||||
├── *napi-log* buffer (persistent log)
|
|
||||||
└── Minibuffer message
|
|
||||||
|
|
||||||
emacsclient --socket-name=napi --eval '(napi-dashboard)' ← interact
|
|
||||||
emacsclient --socket-name=water --eval '(napi-log)' ← view in main UI
|
|
||||||
```
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- `inotify-tools` installed on zzz (provides `inotifywait`) — **already present** (watchers use it)
|
|
||||||
- SSH key access from local → zzz — **already working** (`ssh fenix@qu3v3d0.tech`)
|
|
||||||
- Emacs 30.1 on local — **confirmed**
|
|
||||||
|
|
||||||
## Implementation Steps
|
|
||||||
|
|
||||||
### Step 1: Create `~/napi/emacs/napi.el`
|
|
||||||
|
|
||||||
The core elisp package. ~200 lines. Key components:
|
|
||||||
|
|
||||||
| Component | Purpose |
|
|
||||||
|:----------|:--------|
|
|
||||||
| `napi-watch-start` | Sets up TRAMP file-notify watches on all student dirs |
|
|
||||||
| `napi-watch-stop` | Removes all watches cleanly |
|
|
||||||
| `napi-notify` | Handles a file event: log + desktop notification + sound |
|
|
||||||
| `napi-log` | Interactive: open `*napi-log*` buffer |
|
|
||||||
| `napi-open-student` | Interactive: completing-read → open student dir/notas.md |
|
|
||||||
| `napi-dashboard` | Interactive: overview of all students + last delivery |
|
|
||||||
| `C-c n` prefix | Keybindings for all interactive commands |
|
|
||||||
|
|
||||||
**Student name maps** (username → full name) embedded in elisp, mirroring watcher scripts.
|
|
||||||
|
|
||||||
**TRAMP watch setup** — the core innovation:
|
|
||||||
```elisp
|
|
||||||
;; For each ASIR1 student:
|
|
||||||
(file-notify-add-watch
|
|
||||||
"/ssh:fenix@qu3v3d0.tech:/home/jara/python/"
|
|
||||||
'(change)
|
|
||||||
#'napi--handle-event)
|
|
||||||
|
|
||||||
;; 40 watches total (21 ASIR1 + 19 DDAW2)
|
|
||||||
;; Each spawns a persistent inotifywait on zzz via SSH
|
|
||||||
```
|
|
||||||
|
|
||||||
**Event handler** filters noise (Thumbs.db, __pycache__, .tmp, etc.) and forwards clean notifications to `water` daemon via:
|
|
||||||
```elisp
|
|
||||||
(shell-command
|
|
||||||
"emacsclient --socket-name=water --eval '(napi--show-notification ...)' &")
|
|
||||||
```
|
|
||||||
|
|
||||||
**Reconnection**: TRAMP sends a `stopped` event when SSH drops. The handler auto-re-establishes the watch after a delay.
|
|
||||||
|
|
||||||
### Step 2: Create systemd user unit for `napi` daemon
|
|
||||||
|
|
||||||
File: `~/.config/systemd/user/emacs-napi.service`
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=Emacs daemon for napi student monitoring
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=forking
|
|
||||||
ExecStart=/usr/bin/emacs --daemon=napi -l ~/napi/emacs/napi-init.el
|
|
||||||
ExecStop=/usr/bin/emacsclient --socket-name=napi --eval "(kill-emacs)"
|
|
||||||
Restart=on-failure
|
|
||||||
RestartSec=30
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Create `~/napi/emacs/napi-init.el`
|
|
||||||
|
|
||||||
Minimal init file for the `napi` daemon (not the full `~/.emacs.d/init.el`):
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
;; Loaded ONLY by emacs --daemon=napi
|
|
||||||
(load "~/napi/emacs/napi.el")
|
|
||||||
(napi-watch-start)
|
|
||||||
(message "napi: watching %d student directories on zzz" napi--watch-count)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Also load `napi.el` in `water` daemon
|
|
||||||
|
|
||||||
Add to `~/.emacs.d/init.el` (near line 1478, following `funciones-art.el` pattern):
|
|
||||||
|
|
||||||
```elisp
|
|
||||||
(load "~/napi/emacs/napi.el")
|
|
||||||
```
|
|
||||||
|
|
||||||
This gives the `water` daemon the `napi-log`, `napi-dashboard`, `napi-open-student` commands and the `C-c n` keybindings — but **no watches** (those run in the `napi` daemon only).
|
|
||||||
|
|
||||||
### Step 5: Verify `inotifywait` on zzz
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ssh fenix@qu3v3d0.tech "which inotifywait && inotifywait --help | head -1"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files to Create/Modify
|
|
||||||
|
|
||||||
| File | Action | Location |
|
|
||||||
|:-----|:-------|:---------|
|
|
||||||
| `emacs/napi.el` | **CREATE** | `~/napi/emacs/napi.el` |
|
|
||||||
| `emacs/napi-init.el` | **CREATE** | `~/napi/emacs/napi-init.el` |
|
|
||||||
| `emacs-napi.service` | **CREATE** | `~/.config/systemd/user/emacs-napi.service` |
|
|
||||||
| `init.el` | **MODIFY** (1 line) | `~/.emacs.d/init.el` |
|
|
||||||
|
|
||||||
**No modifications to zzz** — no watcher changes, no new SSH keys, no reverse tunnels.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
1. **Unit test** — load `napi.el` in `water`, call `(napi-notify ...)` manually
|
|
||||||
2. **TRAMP watch test** — single watch on one student dir, touch a file on zzz
|
|
||||||
3. **Full integration** — start `napi` daemon, simulate SFTP upload as student
|
|
||||||
4. **Stress test** — confirm 40 concurrent TRAMP watches are stable
|
|
||||||
5. **Reconnection** — kill SSH, verify watch auto-restores
|
|
||||||
6. **UI test** — `C-c n l` (log), `C-c n s` (student), `C-c n d` (dashboard)
|
|
||||||
|
|
||||||
## Risk: 40 Concurrent SSH/inotifywait Processes
|
|
||||||
|
|
||||||
Each `file-notify-add-watch` via TRAMP spawns a separate `inotifywait` process on zzz. 40 watches = 40 persistent SSH channels + 40 inotifywait processes.
|
|
||||||
|
|
||||||
**Mitigation**: TRAMP reuses SSH connections (ControlMaster). So it's 1 SSH connection + 40 remote processes. The zzz server's `inotify` limit (`fs.inotify.max_user_watches`) is typically 65536 — 40 is nothing.
|
|
||||||
|
|
||||||
**Fallback**: If 40 watches proves unstable, consolidate to 2 watches on parent dirs (`/home/` with recursive) + filter events by path. Or switch to the emacsclient-via-SSH approach (see FINDINGS.md).
|
|
||||||
|
|
@ -332,6 +332,7 @@ sudo tail -f /var/log/nginx/napi-error.log # Nginx errors
|
||||||
| `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` |
|
||||||
|
| 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+ |
|
||||||
|
|
||||||
|
|
@ -355,4 +356,5 @@ sudo tail -f /var/log/nginx/napi-error.log # Nginx errors
|
||||||
| 2026-02-22 | MVP desplegado: notas.qu3v3d0.tech para DDAW2 (19 alumnos) |
|
| 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 | 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 |
|
||||||
| 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) |
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 948 KiB |
35
TASKS.org
35
TASKS.org
|
|
@ -18,37 +18,4 @@ es decir, mencionar el/la autor/a de la entrega por ambos, nombre y apellido
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
* TODO Emacs packages using JSON-RPC o other kinda-API thing i could be using in a complementary way to streamline my interaction with the students via 'zzz' server where 'napi' is hosted / serving
|
* esto
|
||||||
|
|
||||||
|
|
||||||
---
|
|
||||||
Emacs Packages Using JSON-RPC / Structured Protocols
|
|
||||||
|
|
||||||
For napi/ Specifically (file-sync + notification)
|
|
||||||
|
|
||||||
┌──────────────────────────────────────────────────────────────────────────────────────────────┬──────────────────────────────────────────┬────────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Package │ What It Does │ napi Complement │
|
|
||||||
├──────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
|
|
||||||
│ https://github.com/legoscia/emacs-jabber │ XMPP client for Emacs │ Direct integration with your slixmpp notification system │
|
|
||||||
├──────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
|
|
||||||
│ https://www.gnu.org/software/emacs/manual/html_node/elisp/File-Notifications.html (built-in) │ inotify/kqueue file watching │ Watch ~/napi-data/ for student uploads natively in Emacs │
|
|
||||||
├──────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
|
|
||||||
│ https://github.com/jcaw/json-rpc-server.el + https://github.com/jcaw/porthole │ Expose Emacs functions via HTTP JSON-RPC │ Let external scripts (your inotify watchers) trigger Emacs actions │
|
|
||||||
└──────────────────────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────┴────────────────────────────────────────────────────────────────────┘
|
|
||||||
|
|
||||||
** Explore the idea (as referred on above's gnu.org manual - link) "Notifications on File Changes", which states :
|
|
||||||
|
|
||||||
"Several operating systems support watching of filesystems for changes to files or their attributes. If configured
|
|
||||||
properly, Emacs links a respective library like inotify and others. These libraries enable watching of filesystems on the local machine.
|
|
||||||
|
|
||||||
**It is also possible to watch filesystems on remote machines**, see Remote Files in The GNU Emacs Manual. This does not
|
|
||||||
depend on one of the libraries linked to Emacs.
|
|
||||||
|
|
||||||
Since all these libraries emit different events upon notified file changes, Emacs provides a special library filenotify
|
|
||||||
which presents a unified interface to applications. Lisp programs that want to receive file notifications should always
|
|
||||||
use this library in preference to the native ones. [..]
|
|
||||||
|
|
||||||
|
|
||||||
the **bold** is mine ! ...
|
|
||||||
|
|
||||||
*** Am i crazy saying that ~/napi could be running remotely, as long as a 'emacs -daemon=napi' which i could hook into an 'emacsclient' ?
|
|
||||||
|
|
|
||||||
56
api.php
56
api.php
|
|
@ -1,56 +0,0 @@
|
||||||
<?php
|
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
|
||||||
|
|
||||||
// Obtener usuario autenticado
|
|
||||||
$user = $_SERVER['REMOTE_USER'] ?? '';
|
|
||||||
if (empty($user)) {
|
|
||||||
http_response_code(401);
|
|
||||||
exit("# Error\n\nNo autenticado.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Leer markdown
|
|
||||||
$file = __DIR__ . '/data/tabla-de-seguimiento.md';
|
|
||||||
if (!file_exists($file)) {
|
|
||||||
http_response_code(404);
|
|
||||||
exit("# Dashboard sin Datos\n\nEl profesor aún no ha subido la tabla.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$markdown = file_get_contents($file);
|
|
||||||
$lines = explode("\n", $markdown);
|
|
||||||
|
|
||||||
// Filtrar solo sección del usuario
|
|
||||||
$output = [];
|
|
||||||
$inUserSection = false;
|
|
||||||
$headerShown = false;
|
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
// Título principal
|
|
||||||
if (!$headerShown && preg_match('/^# /', $line)) {
|
|
||||||
$output[] = $line;
|
|
||||||
$headerShown = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detectar sección ## username
|
|
||||||
if (preg_match('/^## (.+)$/', $line, $matches)) {
|
|
||||||
$sectionUser = trim($matches[1]);
|
|
||||||
$inUserSection = ($sectionUser === $user);
|
|
||||||
if ($inUserSection) {
|
|
||||||
$output[] = $line;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si estamos en sección del usuario, capturar
|
|
||||||
if ($inUserSection) {
|
|
||||||
$output[] = $line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si no hay datos para el usuario
|
|
||||||
if (count($output) <= 1) {
|
|
||||||
$output[] = "\n## $user\n";
|
|
||||||
$output[] = "**No hay datos registrados.**\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo implode("\n", $output);
|
|
||||||
|
|
@ -1,622 +0,0 @@
|
||||||
# Dashboard de Seguimiento - Guía de Implementación
|
|
||||||
|
|
||||||
> Sistema KISS de seguimiento académico: Markdown + Basic Auth + PHP + Vanilla JS
|
|
||||||
|
|
||||||
**Servidor:** qu3v3d0.tech | **Filosofía:** Unix + KISS
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Qué hace este sistema
|
|
||||||
|
|
||||||
```
|
|
||||||
Profesor sube tabla-de-seguimiento.md
|
|
||||||
↓
|
|
||||||
Alumnos acceden con user/pass SFTP
|
|
||||||
↓
|
|
||||||
Cada alumno ve SOLO su progreso
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Paso 1: Crear htpasswd para Basic Auth
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Generar archivo de passwords (password = username por defecto)
|
|
||||||
sudo touch /etc/nginx/.htpasswd_sftp
|
|
||||||
|
|
||||||
for user in alumno{01..20}; do
|
|
||||||
echo "$user:$(openssl passwd -apr1 $user)" | \
|
|
||||||
sudo tee -a /etc/nginx/.htpasswd_sftp > /dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
# Permisos restrictivos
|
|
||||||
sudo chmod 640 /etc/nginx/.htpasswd_sftp
|
|
||||||
sudo chown root:www-data /etc/nginx/.htpasswd_sftp
|
|
||||||
|
|
||||||
# Verificar
|
|
||||||
sudo cat /etc/nginx/.htpasswd_sftp
|
|
||||||
```
|
|
||||||
|
|
||||||
**✅ Verificación:**
|
|
||||||
```
|
|
||||||
alumno01:$apr1$xyz$...
|
|
||||||
alumno02:$apr1$abc$...
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Paso 2: Crear estructura de directorios
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Crear directorios
|
|
||||||
sudo mkdir -p /home/admin/html/dashboard/data
|
|
||||||
|
|
||||||
# Permisos
|
|
||||||
sudo chown -R admin:www-data /home/admin/html/dashboard
|
|
||||||
sudo chmod 755 /home/admin/html/dashboard
|
|
||||||
sudo chmod 775 /home/admin/html/dashboard/data
|
|
||||||
```
|
|
||||||
|
|
||||||
**Estructura final:**
|
|
||||||
```
|
|
||||||
/home/admin/html/dashboard/
|
|
||||||
├── index.html # Cliente (vanilla JS + marked.js)
|
|
||||||
├── api.php # filtro por usuario
|
|
||||||
├── style.css # estilo minimalista
|
|
||||||
└── data/
|
|
||||||
└── tabla-de-seguimiento.md
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Paso 3: Crear api.php
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tee /home/admin/html/dashboard/api.php > /dev/null << 'EOF'
|
|
||||||
<?php
|
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
|
||||||
|
|
||||||
// Obtener usuario autenticado
|
|
||||||
$user = $_SERVER['REMOTE_USER'] ?? '';
|
|
||||||
if (empty($user)) {
|
|
||||||
http_response_code(401);
|
|
||||||
exit("# Error\n\nNo autenticado.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Leer markdown
|
|
||||||
$file = __DIR__ . '/data/tabla-de-seguimiento.md';
|
|
||||||
if (!file_exists($file)) {
|
|
||||||
http_response_code(404);
|
|
||||||
exit("# Dashboard sin Datos\n\nEl profesor aún no ha subido la tabla.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$markdown = file_get_contents($file);
|
|
||||||
$lines = explode("\n", $markdown);
|
|
||||||
|
|
||||||
// Filtrar solo sección del usuario
|
|
||||||
$output = [];
|
|
||||||
$inUserSection = false;
|
|
||||||
$headerShown = false;
|
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
// Título principal
|
|
||||||
if (!$headerShown && preg_match('/^# /', $line)) {
|
|
||||||
$output[] = $line;
|
|
||||||
$headerShown = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detectar sección ## username
|
|
||||||
if (preg_match('/^## (.+)$/', $line, $matches)) {
|
|
||||||
$sectionUser = trim($matches[1]);
|
|
||||||
$inUserSection = ($sectionUser === $user);
|
|
||||||
if ($inUserSection) {
|
|
||||||
$output[] = $line;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si estamos en sección del usuario, capturar
|
|
||||||
if ($inUserSection) {
|
|
||||||
$output[] = $line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si no hay datos para el usuario
|
|
||||||
if (count($output) <= 1) {
|
|
||||||
$output[] = "\n## $user\n";
|
|
||||||
$output[] = "**No hay datos registrados.**\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo implode("\n", $output);
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo chown admin:www-data /home/admin/html/dashboard/api.php
|
|
||||||
sudo chmod 644 /home/admin/html/dashboard/api.php
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Paso 4: Crear index.html
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tee /home/admin/html/dashboard/index.html > /dev/null << 'EOF'
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="es">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Dashboard de Seguimiento</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1>🎯 Dashboard de Seguimiento</h1>
|
|
||||||
<p class="subtitle">qu3v3d0.tech</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main id="dashboard">
|
|
||||||
<div class="loader">
|
|
||||||
<p>Cargando tu progreso...</p>
|
|
||||||
<div class="spinner"></div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<p>Filosofía KISS + Unix</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const parseMarkdown = (md) => marked.parse(md);
|
|
||||||
const renderHTML = (html) => {
|
|
||||||
document.getElementById('dashboard').innerHTML = html;
|
|
||||||
};
|
|
||||||
const showError = (msg) => {
|
|
||||||
const html = `<div class="error"><h2>⚠️ Error</h2><p>${msg}</p></div>`;
|
|
||||||
renderHTML(html);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch('api.php')
|
|
||||||
.then(r => r.ok ? r.text() : Promise.reject(r))
|
|
||||||
.then(parseMarkdown)
|
|
||||||
.then(renderHTML)
|
|
||||||
.catch(e => showError(e.message));
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo chown admin:www-data /home/admin/html/dashboard/index.html
|
|
||||||
sudo chmod 644 /home/admin/html/dashboard/index.html
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Paso 5: Crear style.css
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tee /home/admin/html/dashboard/style.css > /dev/null << 'EOF'
|
|
||||||
:root {
|
|
||||||
--bg: #f5f5f5;
|
|
||||||
--fg: #333;
|
|
||||||
--accent: #0066cc;
|
|
||||||
--success: #28a745;
|
|
||||||
--error: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: system-ui, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--fg);
|
|
||||||
background: var(--bg);
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
background: var(--accent);
|
|
||||||
color: white;
|
|
||||||
padding: 1.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
|
|
||||||
.subtitle { font-size: 0.9rem; opacity: 0.9; }
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex: 1;
|
|
||||||
max-width: 900px;
|
|
||||||
width: 100%;
|
|
||||||
margin: 2rem auto;
|
|
||||||
background: white;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader { text-align: center; padding: 2rem; }
|
|
||||||
.spinner {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
margin: 1rem auto;
|
|
||||||
border: 4px solid #ddd;
|
|
||||||
border-top-color: var(--accent);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: var(--accent);
|
|
||||||
border-bottom: 2px solid var(--accent);
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 { margin-top: 1.5rem; margin-bottom: 1rem; color: #555; }
|
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
th, td {
|
|
||||||
padding: 0.75rem;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
background: var(--bg);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:hover { background: #f9f9f9; }
|
|
||||||
|
|
||||||
ul { margin-left: 1.5rem; margin-bottom: 1rem; }
|
|
||||||
li { margin-bottom: 0.5rem; }
|
|
||||||
|
|
||||||
input[type="checkbox"] { margin-right: 0.5rem; }
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background: #fff3cd;
|
|
||||||
border: 2px solid #ffc107;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error h2 { color: var(--error); margin-bottom: 1rem; }
|
|
||||||
|
|
||||||
footer {
|
|
||||||
background: #2c3e50;
|
|
||||||
color: white;
|
|
||||||
text-align: center;
|
|
||||||
padding: 1rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
main { margin: 1rem; padding: 1rem; }
|
|
||||||
header h1 { font-size: 1.4rem; }
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo chown admin:www-data /home/admin/html/dashboard/style.css
|
|
||||||
sudo chmod 644 /home/admin/html/dashboard/style.css
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Paso 6: Configurar nginx
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tee /etc/nginx/sites-available/dashboard > /dev/null << 'EOF'
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name dashboard.qu3v3d0.tech;
|
|
||||||
|
|
||||||
root /home/admin/html/dashboard;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Basic Auth
|
|
||||||
auth_basic "Dashboard Alumnos";
|
|
||||||
auth_basic_user_file /etc/nginx/.htpasswd_sftp;
|
|
||||||
|
|
||||||
# PHP-FPM
|
|
||||||
location ~ \.php$ {
|
|
||||||
include snippets/fastcgi-php.conf;
|
|
||||||
fastcgi_pass unix:/var/run/php/php-fpm.sock;
|
|
||||||
fastcgi_param REMOTE_USER $remote_user;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Caché de estáticos
|
|
||||||
location ~* \.(css|js)$ {
|
|
||||||
expires 1h;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Proteger data/
|
|
||||||
location /data/ { deny all; }
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
access_log /var/log/nginx/dashboard-access.log;
|
|
||||||
error_log /var/log/nginx/dashboard-error.log;
|
|
||||||
|
|
||||||
autoindex off;
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Activar
|
|
||||||
sudo ln -s /etc/nginx/sites-available/dashboard /etc/nginx/sites-enabled/
|
|
||||||
|
|
||||||
# Validar y recargar
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Paso 7: Crear tabla-de-seguimiento.md
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tee /home/admin/html/dashboard/data/tabla-de-seguimiento.md > /dev/null << 'EOF'
|
|
||||||
# Seguimiento - Grupo 2026
|
|
||||||
|
|
||||||
## alumno01
|
|
||||||
| Tarea | Estado | Fecha |
|
|
||||||
|:------|:------:|------:|
|
|
||||||
| Config nginx básica | ❌ | - |
|
|
||||||
| JSON con CORS | ❌ | - |
|
|
||||||
| Error pages | ❌ | - |
|
|
||||||
| Caché estático | ❌ | - |
|
|
||||||
|
|
||||||
## alumno02
|
|
||||||
| Tarea | Estado | Fecha |
|
|
||||||
|:------|:------:|------:|
|
|
||||||
| Config nginx básica | ❌ | - |
|
|
||||||
| JSON con CORS | ❌ | - |
|
|
||||||
| Error pages | ❌ | - |
|
|
||||||
| Caché estático | ❌ | - |
|
|
||||||
|
|
||||||
## alumno03
|
|
||||||
| Tarea | Estado | Fecha |
|
|
||||||
|:------|:------:|------:|
|
|
||||||
| Config nginx básica | ❌ | - |
|
|
||||||
| JSON con CORS | ❌ | - |
|
|
||||||
| Error pages | ❌ | - |
|
|
||||||
| Caché estático | ❌ | - |
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo chown admin:www-data /home/admin/html/dashboard/data/tabla-de-seguimiento.md
|
|
||||||
sudo chmod 664 /home/admin/html/dashboard/data/tabla-de-seguimiento.md
|
|
||||||
```
|
|
||||||
|
|
||||||
**Formato alternativo con checkboxes:**
|
|
||||||
```markdown
|
|
||||||
## alumno01
|
|
||||||
- [ ] Config nginx básica
|
|
||||||
- [ ] JSON con CORS
|
|
||||||
- [ ] Error pages
|
|
||||||
- [ ] Caché estático
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Paso 8: Verificación
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Permisos
|
|
||||||
ls -la /home/admin/html/dashboard/
|
|
||||||
ls -la /home/admin/html/dashboard/data/
|
|
||||||
|
|
||||||
# 2. Nginx
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl status nginx
|
|
||||||
|
|
||||||
# 3. PHP-FPM
|
|
||||||
sudo systemctl status php*-fpm
|
|
||||||
|
|
||||||
# 4. Test htpasswd
|
|
||||||
htpasswd -v /etc/nginx/.htpasswd_sftp alumno01
|
|
||||||
|
|
||||||
# 5. Test curl
|
|
||||||
curl -u alumno01:alumno01 http://localhost/dashboard/api.php
|
|
||||||
```
|
|
||||||
|
|
||||||
**✅ Debe retornar:**
|
|
||||||
```markdown
|
|
||||||
# Seguimiento - Grupo 2026
|
|
||||||
|
|
||||||
## alumno01
|
|
||||||
| Tarea | Estado | Fecha |
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Paso 9: Test en navegador
|
|
||||||
|
|
||||||
1. Abrir: `http://dashboard.qu3v3d0.tech`
|
|
||||||
2. Login: `alumno01` / `alumno01`
|
|
||||||
3. Debe mostrar solo la sección de alumno01
|
|
||||||
4. Probar con alumno02, alumno03...
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Uso para el Profesor
|
|
||||||
|
|
||||||
### Actualizar tabla via SFTP
|
|
||||||
|
|
||||||
```
|
|
||||||
Host: qu3v3d0.tech
|
|
||||||
User: admin
|
|
||||||
Password: ****
|
|
||||||
Path: /html/dashboard/data/
|
|
||||||
File: tabla-de-seguimiento.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### Actualizar desde servidor
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ssh admin@qu3v3d0.tech
|
|
||||||
nano ~/html/dashboard/data/tabla-de-seguimiento.md
|
|
||||||
# Editar y guardar
|
|
||||||
```
|
|
||||||
|
|
||||||
### Formato de la tabla
|
|
||||||
|
|
||||||
**Emojis de estado:**
|
|
||||||
- ✅ Completado
|
|
||||||
- ❌ Pendiente
|
|
||||||
- ⏳ En progreso
|
|
||||||
|
|
||||||
**Ejemplo:**
|
|
||||||
```markdown
|
|
||||||
## alumno01
|
|
||||||
| Tarea | Estado | Fecha |
|
|
||||||
|:------|:------:|------:|
|
|
||||||
| Config nginx básica | ✅ | 2026-01-20 |
|
|
||||||
| JSON con CORS | ⏳ | - |
|
|
||||||
| Error pages | ❌ | - |
|
|
||||||
|
|
||||||
**Observaciones:** Buen progreso, revisar CORS.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Error 401 Unauthorized
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar htpasswd
|
|
||||||
ls -la /etc/nginx/.htpasswd_sftp
|
|
||||||
grep alumno01 /etc/nginx/.htpasswd_sftp
|
|
||||||
htpasswd -v /etc/nginx/.htpasswd_sftp alumno01
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error 403 Forbidden
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar permisos
|
|
||||||
ls -la /home/admin/html/dashboard/
|
|
||||||
ps aux | grep nginx # Usuario debe ser www-data
|
|
||||||
```
|
|
||||||
|
|
||||||
### PHP no procesa
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verificar PHP-FPM
|
|
||||||
sudo systemctl status php*-fpm
|
|
||||||
ls -la /var/run/php/php*-fpm.sock
|
|
||||||
sudo tail -f /var/log/nginx/dashboard-error.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Página en blanco
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test API directamente
|
|
||||||
curl -u alumno01:alumno01 http://dashboard.qu3v3d0.tech/api.php
|
|
||||||
|
|
||||||
# Ver consola navegador (F12)
|
|
||||||
# Verificar que marked.js carga correctamente
|
|
||||||
```
|
|
||||||
|
|
||||||
### Usuario ve datos de otros
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# En api.php, agregar debug temporal:
|
|
||||||
echo "DEBUG: User = $user\n";
|
|
||||||
|
|
||||||
# Verificar que ## username coincide exactamente
|
|
||||||
# (sin espacios extras, case-sensitive)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Extensión: Notificación XMPP
|
|
||||||
|
|
||||||
Notificar cuando profesor actualiza la tabla:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Script watcher
|
|
||||||
sudo tee /usr/local/bin/dashboard-notify.sh > /dev/null << 'EOF'
|
|
||||||
#!/bin/bash
|
|
||||||
WATCH_FILE="/home/admin/html/dashboard/data/tabla-de-seguimiento.md"
|
|
||||||
inotifywait -m -e close_write "$WATCH_FILE" | while read event; do
|
|
||||||
/usr/local/bin/xmpp-notify.py "📊 Tabla actualizada por el profesor"
|
|
||||||
done
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo chmod +x /usr/local/bin/dashboard-notify.sh
|
|
||||||
|
|
||||||
# Servicio systemd
|
|
||||||
sudo tee /etc/systemd/system/dashboard-notify.service > /dev/null << 'EOF'
|
|
||||||
[Unit]
|
|
||||||
Description=Dashboard Update Notifier
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/usr/local/bin/dashboard-notify.sh
|
|
||||||
Restart=always
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable --now dashboard-notify.service
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Checklist de Implementación
|
|
||||||
|
|
||||||
- [ ] htpasswd creado (`/etc/nginx/.htpasswd_sftp`)
|
|
||||||
- [ ] Directorios creados (`/home/admin/html/dashboard/`)
|
|
||||||
- [ ] api.php desplegado (644)
|
|
||||||
- [ ] index.html desplegado (644)
|
|
||||||
- [ ] style.css desplegado (644)
|
|
||||||
- [ ] nginx config creado (`/etc/nginx/sites-available/dashboard`)
|
|
||||||
- [ ] nginx config activado (symlink a sites-enabled)
|
|
||||||
- [ ] nginx reload sin errores (`nginx -t`)
|
|
||||||
- [ ] tabla-de-seguimiento.md inicial (664)
|
|
||||||
- [ ] PHP-FPM corriendo
|
|
||||||
- [ ] Test curl exitoso
|
|
||||||
- [ ] Test navegador alumno01 OK
|
|
||||||
- [ ] Test navegador alumno02 OK
|
|
||||||
- [ ] Cada alumno ve solo sus datos
|
|
||||||
- [ ] Markdown renderiza correctamente
|
|
||||||
- [ ] Responsive en móvil OK
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Resumen
|
|
||||||
|
|
||||||
**Componentes:**
|
|
||||||
- nginx: Auth + routing
|
|
||||||
- PHP: Filtrado por usuario
|
|
||||||
- Markdown: Storage
|
|
||||||
- marked.js: Parsing
|
|
||||||
- Vanilla JS: Cliente
|
|
||||||
|
|
||||||
**Filosofía KISS:**
|
|
||||||
- 3 archivos principales
|
|
||||||
- Sin DB
|
|
||||||
- Sin frameworks
|
|
||||||
- Sin compilación
|
|
||||||
|
|
||||||
**Resultado:** Sistema robusto, mantenible, educativo.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Implementado: 2026-02-13 | qu3v3d0.tech | fenix + Claude*
|
|
||||||
|
|
@ -7,7 +7,7 @@ Wants=network-online.target
|
||||||
What=fenix@qu3v3d0.tech:/var/www/napi/data
|
What=fenix@qu3v3d0.tech:/var/www/napi/data
|
||||||
Where=/home/fenix/napi-data
|
Where=/home/fenix/napi-data
|
||||||
Type=fuse.sshfs
|
Type=fuse.sshfs
|
||||||
Options=reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,IdentityFile=/home/fenix/.ssh/id_rsa,_netdev,allow_other
|
Options=reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,IdentityFile=/home/fenix/.ssh/id_rsa,_netdev,allow_other,auto_cache
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ Wants=network-online.target
|
||||||
What=fenix@qu3v3d0.tech:/var/www/napi2/data
|
What=fenix@qu3v3d0.tech:/var/www/napi2/data
|
||||||
Where=/home/fenix/napi-data2
|
Where=/home/fenix/napi-data2
|
||||||
Type=fuse.sshfs
|
Type=fuse.sshfs
|
||||||
Options=reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,IdentityFile=/home/fenix/.ssh/id_rsa,_netdev,allow_other
|
Options=reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,IdentityFile=/home/fenix/.ssh/id_rsa,_netdev,allow_other,auto_cache
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue