13 KiB
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
- Abrir:
http://dashboard.qu3v3d0.tech - Login:
alumno01/alumno01 - Debe mostrar solo la sección de alumno01
- 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