napi/dashboard-seguimiento.md

13 KiB

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

# 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

# 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

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

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

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

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

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:

## alumno01
- [ ] Config nginx básica
- [ ] JSON con CORS
- [ ] Error pages
- [ ] Caché estático

Paso 8: Verificación

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

# 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

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:

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

# Verificar htpasswd
ls -la /etc/nginx/.htpasswd_sftp
grep alumno01 /etc/nginx/.htpasswd_sftp
htpasswd -v /etc/nginx/.htpasswd_sftp alumno01

Error 403 Forbidden

# Verificar permisos
ls -la /home/admin/html/dashboard/
ps aux | grep nginx  # Usuario debe ser www-data

PHP no procesa

# 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

# 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

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

# 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