api.php dashboard ...>>> ?
This commit is contained in:
parent
e8869005da
commit
6528a069cb
|
|
@ -0,0 +1,148 @@
|
||||||
|
# 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).
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 948 KiB |
35
TASKS.org
35
TASKS.org
|
|
@ -18,4 +18,37 @@ es decir, mencionar el/la autor/a de la entrega por ambos, nombre y apellido
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
* esto
|
* 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
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
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' ?
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?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);
|
||||||
|
|
@ -0,0 +1,622 @@
|
||||||
|
# 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*
|
||||||
Loading…
Reference in New Issue