🟢 Mini CPanel – Mejora: Dashboard, Red y Accesos Profesionales (EN DESARROLLO)

Continuación de: Mini CPanel Profesional – Procedimiento Completo para Raspberry Pi 3B

En este artículo vamos a mejorar nuestro Mini CPanel, haciéndolo más profesional, visual y funcional. Esto incluye la configuración de red, accesos rápidos a herramientas, logs, y un dashboard con estadísticas de servidor.

🔹 Índice de mejoras de este artículo

  1. Dashboard principal
    • Visualización de IP, MAC, estado de la red
    • Estadísticas de servidor (CPU, memoria, almacenamiento, número de archivos, conexiones activas)
    • Graficar tráfico de red (solo al abrir el dashboard)
    • Dashboard secundario
      • Registro de LOGs de conexiones descargable por fechas y auto borrado.
      • Geolocalización de IPs
      • Lista de puertos abiertos
  2. Botones de acceso rápido
    • FileBrowser
    • phpMyAdmin
    • Personalización con iconos adaptativos (logo de la app)
  3. Logs de actividad
    • Inicio de sesión
    • Acciones del panel (reinicio PHP, ajustes, backups)
    • Auto-borrado a 60 días
    • Opción de borrado manual con confirmación
  4. Configuración de red
    • Consultar IP, MAC, DNS y Gateway
    • Test de conectividad (ping y nslookup)
    • Interfaces activas y estadísticas de tráfico
    • Número de conexiones activas y por IP
  5. Acciones rápidas de red
    • Reinicio de interfaces
    • Cambio de IP estática/dinámica
    • Configuración WiFi
    • Escaneo rápido de LAN
  6. Seguridad y buenas prácticas
    • Acceso solo LAN
    • HTTPS opcional con certificado self-signed
    • Ejecución de scripts con sudo seguro
    • Usuario admin separado
  7. Estética y UX
    • Tipografía profesional y colores claros
    • Panel responsivo con tarjetas, tablas e iconos
    • Logo adaptable al tamaño para no interferir en la visualización

🟢 Mini CPanel – Punto 1: Dashboard Principal

Objetivo: Crear un dashboard visual que muestre solo cuando se accede, información relevante como:

  • IP y MAC de la Raspberry Pi
  • Gateway y DNS
  • Uso de CPU, RAM y almacenamiento
  • Número de archivos en /var/www/apps
  • Conexiones activas por IP
  • Gráfico de tráfico de red (opcional con Chart.js)

1️⃣ Crear directorios para el dashboard

Primero asegurémonos de que existan los directorios donde guardaremos los assets (CSS, JS, imágenes):

sudo mkdir -p /var/www/html/assets/css
sudo mkdir -p /var/www/html/assets/js
sudo mkdir -p /var/www/html/assets/img
sudo chown -R www-data:www-data /var/www/html/assets
sudo chmod -R 755 /var/www/html/assets

1️⃣ Modificar el ADMIN.php

<?php
session_start();
$secret_pass = "PASSWORD";

/* ========= LOGIN ========= */
if (isset($_POST['password'])) {
if (hash_equals($secret_pass, $_POST['password'])) {
$_SESSION['auth'] = true;
} else { $error = "Contraseña incorrecta"; }
}

if (!isset($_SESSION['auth'])):
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Login</title>
<link rel="icon" type="image/png" href="assets/img/logo.png">
<style>
body{background:#0f172a;color:#e5e7eb;font-family:sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0}
.login{background:#1e293b;padding:30px;border-radius:12px;width:90%;max-width:400px;text-align:center}
/* Estilo para el logo en login */
.login-logo{width:80px;height:auto;margin-bottom:20px}
input,button{width:100%;padding:12px;margin-top:10px;border-radius:8px;border:none;box-sizing:border-box}
button{background:#3b82f6;color:#fff;font-weight:bold;cursor:pointer}
</style>
</head>
<body>
<div class="login">
<img src="assets/img/logo.png" alt="Logo" class="login-logo">
<form method="post">
<input type="password" name="password" placeholder="Contraseña" autofocus>
<button type="submit">Entrar</button>
<?php if(isset($error)) echo "<p style='color:#ef4444;margin-top:15px'>$error</p>"; ?>
</form>
</div>
</body>
</html>
<?php exit; endif; ?>

<?php
/* ========= CONFIG & INTERFACE ========= */
if(isset($_GET['set_iface'])) $_SESSION['iface'] = $_GET['set_iface'];
$active_iface = $_SESSION['iface'] ?? 'eth0';

$netFile = "/tmp/admin_net_prev.json";
$scriptPath = "/var/www/apps/admin-scripts/";

/* ========= TERMINAL ========= */
if (!isset($_SESSION['terminal'])) $_SESSION['terminal'] = [];
function logTerm($type,$msg){
$_SESSION['terminal'][]=['t'=>date("Y-m-d H:i:s"),'type'=>$type,'msg'=>trim($msg)];
if(count($_SESSION['terminal'])>300) $_SESSION['terminal']=array_slice($_SESSION['terminal'],-300);
}

/* ========= ACTION AJAX ========= */
if(isset($_GET['action'])){
header("Content-Type: application/json");
if($_GET['action']==='run'){
$cmd = $_GET['cmd'] ?? '';
$map = ['reiniciar_php'=>'reiniciar-php.sh','permisos'=>'ajustar-permisos.sh','backup'=>'backup.sh'];
if(isset($map[$cmd])){
$fullPath = $scriptPath . $map[$cmd];
if(file_exists($fullPath)){
$out = shell_exec("sudo -n $fullPath 2>&1 < /dev/null");
logTerm('ok', $out ?: 'OK');
} else { logTerm('err', "No existe: $fullPath"); }
echo json_encode(['ok'=>true]);
}
exit;
}
if($_GET['action']==='clear_term'){ $_SESSION['terminal']=[]; echo json_encode(['ok'=>true]); exit; }
}

/* ========= SISTEMA ========= */
function cpu(){ return round(floatval(shell_exec("top -bn1 | awk '/Cpu/ {print 100-$8}'")),1); }
function mem(){ return round(floatval(shell_exec("free | awk '/Mem:/ {print $3/$2*100}'")),1); }
function disk(){ return intval(shell_exec("df / | awk 'NR==2{gsub(/%/,\"\",$5);print $5}'")); }
function temp(){ return round(@file_get_contents("/sys/class/thermal/thermal_zone0/temp")/1000,1); }

/* ========= RED AVANZADA ========= */
function get_network_details($i) {
$ip = trim(shell_exec("ip -4 addr show $i | awk '/inet /{print $2}' | cut -d/ -f1"));
$mask = trim(shell_exec("ip -4 addr show $i | awk '/inet /{print $2}' | cut -d/ -f2"));
$mac = trim(@file_get_contents("/sys/class/net/$i/address"));
$gateway = trim(shell_exec("ip route | grep default | awk '{print $3}'"));
$dns = trim(shell_exec("grep nameserver /etc/resolv.conf | awk '{print $2}' | head -n 1"));
$state = trim(@file_get_contents("/sys/class/net/$i/operstate"));
$public_ip = trim(@shell_exec("curl -s --max-time 2 https://api.ipify.org") ?: '-');

return [
'interface' => $i,
'ip' => $ip ?: 'Desconectado',
'mask' => $mask ?: '-',
'mac' => $mac ?: '-',
'gateway' => $gateway ?: '-',
'dns' => $dns ?: '-',
'state' => $state ?: 'unknown',
'public' => $public_ip ?: '-',
'rx' => intval(@file_get_contents("/sys/class/net/$i/statistics/rx_bytes")),
'tx' => intval(@file_get_contents("/sys/class/net/$i/statistics/tx_bytes"))
];
}

/* ========= JSON ========= */
if(isset($_GET['json'])){
header("Content-Type: application/json");
$prev = file_exists($netFile) ? json_decode(file_get_contents($netFile),true) : [];
$net = get_network_details($active_iface);

$rx_speed = isset($prev[$active_iface.'_rx']) ? ($net['rx']-$prev[$active_iface.'_rx'])/1024 : 0;
$tx_speed = isset($prev[$active_iface.'_tx']) ? ($net['tx']-$prev[$active_iface.'_tx'])/1024 : 0;

$raw_conns = shell_exec("ss -tun | awk 'NR>1 {print $1, $6}'");
$ip_counts = [];
if($raw_conns){
foreach(explode("\n", trim($raw_conns)) as $line){
$cols = preg_split('/\s+/', trim($line)); if(count($cols) < 2) continue;
$remote_full = $cols[1];
$parts = explode(':', $remote_full); array_pop($parts);
$ip = str_replace(['[',']'], '', implode(':', $parts));
if($ip == "127.0.0.1" || $ip == "::1" || empty($ip) || $ip == "*" || $ip == "0.0.0.0") continue;
$state = ($cols[0] == "ESTAB" ? 'ok' : 'err');
if(!isset($ip_counts[$ip])) $ip_counts[$ip] = ['count'=>0, 'state'=>$state];
$ip_counts[$ip]['count']++;
}
}

$prev[$active_iface.'_rx'] = $net['rx']; $prev[$active_iface.'_tx'] = $net['tx'];
file_put_contents($netFile,json_encode($prev));

echo json_encode([
'cpu'=>cpu(), 'mem'=>mem(), 'disk'=>disk(), 'temp'=>temp(),
'lan'=>$net, 'speed_rx'=>round(max(0,$rx_speed),1), 'speed_tx'=>round(max(0,$tx_speed),1),
'ips'=>$ip_counts, 'total_conns'=>count($ip_counts), 'term'=>$_SESSION['terminal']
]);
exit;
}
$local_ip = $_SERVER['SERVER_ADDR'] ?? '127.0.0.1';
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Admin Dashboard</title>
<link rel="icon" type="image/png" href="assets/img/logo.png">
<script src="assets/js/chart.umd.min.js"></script>
<style>
body{background:#0f172a;color:#e5e7eb;font-family:system-ui;margin:0;padding:20px}
.top-actions{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-bottom:15px}
.top-actions button,.top-actions a{background:#1e293b;color:#fff;border:none;padding:10px 18px;border-radius:8px;cursor:pointer;text-decoration:none;font-weight:bold}
.card{background:#1e293b;padding:20px;border-radius:12px;margin-bottom:20px}
.chart-box{height:250px}
.net-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(130px, 1fr));gap:10px;font-size:11px;color:#94a3b8;margin-bottom:15px;padding-bottom:10px;border-bottom:1px solid #334155}
.net-grid b{color:#facc15;display:block;font-size:9px;text-transform:uppercase}
.terminal{background:#020617;border-radius:12px;padding:10px;margin-bottom:15px;border:2px solid #facc15}
.term-log{height:15em;overflow:auto;font-family:monospace;font-size:12px}
.cmd{color:#38bdf8} .ok{color:#22c55e} .err{color:#ef4444}
.refresh-control{display:flex;gap:15px;align-items:center;margin-bottom:15px;color:#facc15;font-size:13px}
.ip-list{display:flex;flex-wrap:wrap;gap:5px;margin-top:10px}
.ip-tag{padding:2px 6px;border-radius:4px;background:#0f172a;font-family:monospace;font-size:11px;border:1px solid transparent;display:inline-block}
.ip-tag.ok{border-color:#22c55e} .ip-tag.err{border-color:#ef4444}
select{background:#1e293b;color:#facc15;border:1px solid #facc15;padding:4px;border-radius:4px;cursor:pointer}
</style>
</head>
<body>

<div class="terminal">
<b>📟 Terminal de Eventos</b>
<button onclick="clearTerm()" style="float:right;background:#facc15;border:none;padding:2px 8px;border-radius:4px;cursor:pointer;color:#000;font-weight:bold">Limpiar</button>
<div class="term-log" id="term"></div>
</div>

<div class="top-actions">
<a href="adminplus.php" class="btn-auditoria" style="background:#1e293b;color:#fff;border:none;padding:10px 18px;border-radius:8px;text-decoration:none;font-weight:bold">🛡️ Auditoría LAN</a>
<button onclick="runCmd('reiniciar_php')">🔁 PHP</button>
<button onclick="runCmd('permisos')">🔐 Permisos</button>
<button onclick="runCmd('backup')">📦 Backup</button>
<a href="http://<?= $local_ip ?>/phpmyadmin" target="_blank">🖥️ phpMyAdmin</a>
<a href="http://<?= $local_ip ?>:8080" target="_blank">📁 FileBrowser</a>
</div>

<div class="refresh-control">
<span><b>Refresco:</b> <input type="range" min="100" max="5000" step="100" value="1000" id="refreshDial"> <span id="refreshVal">1000ms</span></span>
<button id="togglePauseBtn" style="background:none;border:1px solid #facc15;color:#facc15;padding:2px 8px;border-radius:4px;cursor:pointer">⏯️ Pausar</button>
<span><b>Interfaz:</b>
<select id="ifaceSelect" onchange="location.href='?set_iface='+this.value">
<option value="eth0" <?= $active_iface=='eth0'?'selected':'' ?>>eth0 (Cable)</option>
<option value="wlan0" <?= $active_iface=='wlan0'?'selected':'' ?>>wlan0 (WiFi)</option>
</select>
</span>
</div>

<div class="card">
<h3 style="margin-top:0">📊 Carga de Sistema</h3>
<div class="chart-box"><canvas id="sysChart"></canvas></div>
</div>

<div class="card">
<h3 style="margin-top:0">🌐 Tráfico de Red y LAN</h3>
<div id="netGrid" class="net-grid"></div>
<div class="chart-box"><canvas id="lanChart"></canvas></div>
<h4>🔌 Clientes Conectados (Únicos): <span id="connTotal" style="color:#facc15">0</span></h4>
<div id="ipMonitor" class="ip-list"></div>
</div>

<script>
let paused = false, refreshMs = 1000, timer = null;
const yellowAxis = { ticks:{color:'#facc15', font:{size:10}}, grid:{color:'rgba(250,204,21,0.05)'} };
const chartOpts = { borderWidth: 1.0, pointRadius: 1, tension: 0.3 };

const sysChart = new Chart(document.getElementById('sysChart'),{
type:'line',
data:{labels:[],datasets:[
{label:'CPU %',data:[],borderColor:'#3b82f6', ...chartOpts},
{label:'MEM %',data:[],borderColor:'#10b981', ...chartOpts},
{label:'DISK %',data:[],borderColor:'#f59e0b', ...chartOpts},
{label:'TEMP °C',data:[],borderColor:'#ef4444', ...chartOpts}
]},
options:{responsive:true,maintainAspectRatio:false,animation:false,scales:{x:yellowAxis,y:yellowAxis},plugins:{legend:{labels:{color:'#facc15'}}}}
});

const lanChart = new Chart(document.getElementById('lanChart'),{
type:'line',
data:{labels:[],datasets:[
{label:'RX KB/s',data:[],borderColor:'#22c55e', fill:true, backgroundColor:'rgba(34,197,94,0.05)', ...chartOpts},
{label:'TX KB/s',data:[],borderColor:'#3b82f6', fill:true, backgroundColor:'rgba(59,130,246,0.05)', ...chartOpts}
]},
options:{responsive:true,maintainAspectRatio:false,animation:false,scales:{x:yellowAxis,y:yellowAxis},plugins:{legend:{labels:{color:'#facc15'}}}}
});

function runCmd(c){ fetch(`?action=run&cmd=${c}`).then(()=>update(true)); }
function clearTerm(){ fetch('?action=clear_term').then(()=>update(true)); }
function startTimer(){ timer=setInterval(update,refreshMs); }

document.getElementById('refreshDial').oninput=e=>{
refreshMs=parseInt(e.target.value); document.getElementById('refreshVal').innerText=refreshMs+'ms';
if(!paused){ clearInterval(timer); startTimer(); }
};

document.getElementById('togglePauseBtn').onclick=()=>{
paused=!paused; document.getElementById('togglePauseBtn').innerText=paused?"▶️ Continuar":"⏯️ Pausar";
if(paused) clearInterval(timer); else startTimer();
};

function update(force=false){
if(paused && !force) return;
fetch('?json=1').then(r=>r.json()).then(d=>{
let t=new Date().toLocaleTimeString();
sysChart.data.labels.push(t);
[d.cpu, d.mem, d.disk, d.temp].forEach((v,i)=>{
sysChart.data.datasets[i].data.push(v);
sysChart.data.datasets[i].label = sysChart.data.datasets[i].label.split(':')[0] + ': ' + v;
if(sysChart.data.datasets[i].data.length>50) sysChart.data.datasets[i].data.shift();
});
if(sysChart.data.labels.length>50) sysChart.data.labels.shift();
sysChart.update();

lanChart.data.labels.push(t);
[d.speed_rx, d.speed_tx].forEach((v,i)=>{
lanChart.data.datasets[i].data.push(v);
lanChart.data.datasets[i].label = lanChart.data.datasets[i].label.split(':')[0] + ': ' + v + ' KB/s';
if(lanChart.data.datasets[i].data.length>50) lanChart.data.datasets[i].data.shift();
});
if(lanChart.data.labels.length>50) lanChart.data.labels.shift();
lanChart.update();

document.getElementById('netGrid').innerHTML = `
<div><b>Interface</b>${d.lan.interface} (${d.lan.state})</div>
<div><b>IP Local</b>${d.lan.ip}</div>
<div><b>Máscara</b>/${d.lan.mask}</div>
<div><b>Gateway</b>${d.lan.gateway}</div>
<div><b>DNS</b>${d.lan.dns}</div>
<div><b>IP Pública</b>${d.lan.public}</div>
<div><b>MAC</b>${d.lan.mac}</div>
`;
document.getElementById('connTotal').innerText = d.total_conns;
document.getElementById('ipMonitor').innerHTML = Object.entries(d.ips).map(([ip, info]) =>
`<span class="ip-tag ${info.state}">${ip} ${info.count > 1 ? '(x'+info.count+')' : ''}</span>`
).join('');
document.getElementById('term').innerHTML = d.term.map(l=>`<div class="${l.type}">[${l.t}] ${l.msg}</div>`).join('');
document.getElementById('term').scrollTop = document.getElementById('term').scrollHeight;
});
}
startTimer(); update(true);
</script>
</body>
</html></body>
</html></html>

Dashboard Secundarario (Auditoria LAN):

<?php
session_start();
if (!isset($_SESSION['auth'])) { header("Location: index.php"); exit; }

/* ========= CONFIGURACIÓN DE RUTAS DINÁMICAS ========= */
$logDir = "/var/www/apps/admin-scripts/";
$currentDate = date("Y-m-d");
// Ahora el log principal es el del día actual
$logFile = $logDir . "auditoria_" . $currentDate . ".log";

/* ========= EXPORTACIÓN CSV SELECTIVA (Mejorado) ========= */
if (isset($_GET['export_csv'])) {
// Si viene un archivo específico por GET, lo usamos; si no, el de hoy
$targetLog = isset($_GET['file']) ? $logDir . $_GET['file'] : $logFile;

if (file_exists($targetLog) && strpos(basename($targetLog), 'auditoria_') === 0) {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=' . str_replace('.log', '.csv', basename($targetLog)));

$output = fopen('php://output', 'w');
fputcsv($output, ['Fecha y Hora', 'Descripción del Evento']);

$lines = file($targetLog, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (preg_match('/^\[(.*?)\] (.*)$/', $line, $matches)) {
fputcsv($output, [$matches[1], $matches[2]]);
} else {
fputcsv($output, ['', $line]);
}
}
fclose($output);
exit;
}
}

/* ========= LÓGICA DE ELIMINACIÓN POR BLOQUES ========= */
if (isset($_GET['delete_mode'])) {
$mode = $_GET['delete_mode'];
$files = glob($logDir . "auditoria_*.log");
foreach ($files as $f) {
if ($mode == 'all') @unlink($f);
if ($mode == 'today' && strpos($f, $currentDate) !== false) @unlink($f);
if ($mode == 'week' && filemtime($f) < (time() - (7 * 86400))) @unlink($f);
}
header("Location: adminplus.php"); exit;
}

/* ========= FUNCIONES DE AUDITORÍA (Tus funciones originales) ========= */

function get_ip_geo($ip) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$details = @json_decode(file_get_contents("http://ip-api.com/json/{$ip}?fields=status,country,city,isp"), true);
return ($details && $details['status'] === 'success') ? "📍 {$details['city']}, {$details['country']} ({$details['isp']})" : "🌐 IP Pública";
}
return "🏠 Red Local / VPN";
}

function get_hostname($ip) {
$host = @gethostbyaddr($ip);
return ($host && $host !== $ip) ? "🏷️ $host" : "❓ Desconocido";
}

function write_audit_log($msg) {
global $logFile;
$date = date("Y-m-d H:i:s");
if (is_writable(dirname($logFile)) || (file_exists($logFile) && is_writable($logFile))) {
file_put_contents($logFile, "[$date] $msg\n", FILE_APPEND);
}
}

/* ========= PROCESAMIENTO AJAX (Tu lógica original) ========= */
if (isset($_GET['ajax'])) {
header("Content-Type: application/json");

$raw_conns = shell_exec("ss -tun | awk 'NR>1 {print $6}' | sed 's/\[//g; s/\]//g' | cut -d: -f1 | sort -u");
$geo_data = [];
$lines = explode("\n", trim($raw_conns));

foreach ($lines as $ip) {
$ip = trim($ip);
if (!empty($ip) && !in_array($ip, ['127.0.0.1', '::1', '0.0.0.0', '*'])) {
$geo = get_ip_geo($ip);
$host = get_hostname($ip);
$geo_data[$ip] = ['geo' => $geo, 'host' => $host];

$type = (strpos($geo, '📍') !== false) ? "EXTERNA" : "LOCAL";
write_audit_log("Conexión $type: $ip ($host)");
}
}

$temp = floatval(@file_get_contents("/sys/class/thermal/thermal_zone0/temp") / 1000);
if ($temp > 75) write_audit_log("⚠️ CRÍTICO: Temperatura CPU {$temp}°C");

$disk_usage = intval(shell_exec("df / | awk 'NR==2{gsub(/%/,\"\",$5); print $5}'"));
if ($disk_usage > 90) write_audit_log("⚠️ CRÍTICO: Disco casi lleno ({$disk_usage}%)");

echo json_encode([
'uptime' => shell_exec("uptime -p"),
'cpu_detail' => shell_exec("ps -eo %cpu,%mem,cmd --sort=-%cpu | head -n 10"),
'ports' => shell_exec("netstat -tulnp | awk 'NR>2 {print $4, \"->\", $7}' | sort -u"),
'geo_audit' => $geo_data,
'logs' => file_exists($logFile) ? nl2br(htmlspecialchars(shell_exec("tail -n 25 $logFile"))) : "Sin eventos hoy."
]);
exit;
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Auditoría LAN Plus</title>
<link rel="icon" type="image/png" href="/assets/img/logo.png">

<style>
body{background:#0f172a;color:#e5e7eb;font-family:system-ui;margin:0;padding:20px}
.grid{display:flex; flex-direction: column; gap:20px}
.card{background:#1e293b;padding:20px;border-radius:12px;border:1px solid #334155; width: 100%; box-sizing: border-box;}
.top-actions{display:flex;gap:10px;margin-bottom:20px;justify-content:space-between; align-items: center; flex-wrap: wrap}
.btn-group{display:flex; gap:10px; align-items: center}
.btn{padding:10px 18px;border-radius:8px;text-decoration:none;font-weight:bold;color:#fff;border:none;cursor:pointer; font-size: 14px}
.btn-blue{background:#3b82f6} .btn-red{background:#ef4444} .btn-green{background:#10b981}

.log-box{
background:#020617;
padding:15px;
border-radius:8px;
font-family:monospace;
font-size:12px;
height: 32em;
overflow-y: auto;
border:1px solid #facc15;
color:#facc15;
line-height: 1.6;
}

pre{font-size:12px;color:#38bdf8;background:#0f172a;padding:15px;border-radius:8px;overflow-x:auto; white-space: pre-wrap}
h3{color:#facc15;margin-top:0;display:flex;align-items:center;gap:10px;font-size:18px; border-bottom: 1px solid #334155; padding-bottom: 10px}
.geo-item{padding:12px;border-bottom:1px solid #334155;font-size:13px; display: flex; justify-content: space-between}
select{padding:10px; border-radius:8px; background:#334155; color:#fff; border:1px solid #475569}
</style>
</head>
<body>

<div class="top-actions">
<a href="admin.php" class="btn btn-blue">🔙 Volver</a>
<h2 style="margin:0; font-size:20px">🛡️ Auditoría Full de Pantalla</h2>
<div class="btn-group">
<select id="fileSelector">
<?php
$logFiles = array_reverse(glob($logDir . "auditoria_*.log"));
foreach($logFiles as $f) {
$base = basename($f);
echo "<option value='$base'>$base</option>";
}
?>
</select>
<button onclick="downloadCSV()" class="btn btn-green">📊 Exportar</button>
<button onclick="deleteLogs()" class="btn btn-red">🗑️ Limpiar</button>
</div>
</div>

<div class="grid">
<div class="card">
<h3>⏱️ Historial de Eventos (Día: <?php echo $currentDate; ?>)</h3>
<p style="font-size:14px; margin-bottom: 10px"><b>Uptime:</b> <span id="uptime">...</span></p>
<div class="log-box" id="logContent">Cargando...</div>
</div>

<div class="card">
<h3>🌍 Geolocalización e IPs</h3>
<div id="geoList">Buscando conexiones...</div>
</div>

<div class="card">
<h3>🔥 Procesos Top</h3>
<pre id="procData">...</pre>
</div>

<div class="card">
<h3>🚪 Servicios / Puertos</h3>
<pre id="portData">...</pre>
</div>
</div>

<script>
function downloadCSV() {
const file = document.getElementById('fileSelector').value;
if(file) window.location.href = `?export_csv=1&file=${file}`;
}

function deleteLogs() {
const opc = prompt("¿Qué deseas borrar?\n1: Solo hoy\n2: Historial antiguo (más de 1 semana)\n3: TODO el historial\nEscribe el número:");
if(opc == "1") { if(confirm("¿Borrar log de hoy?")) window.location.href = "?delete_mode=today"; }
else if(opc == "2") { if(confirm("¿Borrar logs de más de 7 días?")) window.location.href = "?delete_mode=week"; }
else if(opc == "3") { if(confirm("¡ALERTA! Esto borrará todos los registros históricos. ¿Continuar?")) window.location.href = "?delete_mode=all"; }
}

function updatePlus() {
fetch('?ajax=1').then(r => r.json()).then(d => {
document.getElementById('uptime').innerText = d.uptime;

const logContainer = document.getElementById('logContent');
logContainer.innerHTML = d.logs;
logContainer.scrollTop = logContainer.scrollHeight;

document.getElementById('procData').innerText = d.cpu_detail;
document.getElementById('portData').innerText = d.ports;

let geoHTML = '';
const entries = Object.entries(d.geo_audit);
if(entries.length > 0) {
for (const [ip, info] of entries) {
geoHTML += `<div style="padding:10px; border-bottom:1px solid #334155; display:flex; justify-content:space-between">
<span><b>${ip}</b><br><small style="color:#94a3b8">${info.host}</small></span>
<span style="text-align: right">${info.geo}</span>
</div>`;
}
} else {
geoHTML = '<p style="padding:15px; color:#94a3b8">Solo conexiones locales.</p>';
}
document.getElementById('geoList').innerHTML = geoHTML;
});
}
setInterval(updatePlus, 3000);
updatePlus();
</script>
</body>
</html></body>
</html>

2️⃣ Crear el archivo del dashboard (pero no lo usaremos 😅)

Archivo: /var/www/html/dashboard.php

sudo nano /var/www/html/dashboard.php
<?php
session_start();
$secret_pass = "TU_PASSWORD_SEGURA"; // Cambiar antes de producción

// Login simple
if(isset($_POST['password'])){
    if($_POST['password'] === $secret_pass){
        $_SESSION['logged_in'] = true;
    } else {
        $error = "Contraseña incorrecta";
    }
}

if(!isset($_SESSION['logged_in'])){
?>
<div class="login panel">
    <img src="/assets/img/logo.png" class="logo" alt="Logo">
    <form method="POST">
        <input type="password" name="password" placeholder="Contraseña">
        <input type="submit" value="Entrar">
        <?php if(isset($error)) echo "<p style='color:red;'>$error</p>"; ?>
    </form>
</div>
<?php exit; }

// Función para ejecutar comandos seguros
function runCommand($cmd){
    $output = shell_exec($cmd . " 2>&1");
    return htmlspecialchars($output);
}

// Recolectar datos del sistema
$ip = runCommand("hostname -I | awk '{print $1}'");
$mac = runCommand("cat /sys/class/net/eth0/address");
$gateway = runCommand("ip route | grep default | awk '{print $3}'");
$dns = runCommand("systemd-resolve --status | grep 'DNS Servers' | head -n1 | awk '{print $3}'");
$cpu_load = runCommand("top -bn1 | grep 'Cpu(s)' | awk '{print $2 + $4}'");
$mem_usage = runCommand("free -m | awk 'NR==2{printf \"%s/%s MB\", $3,$2 }'");
$disk_usage = runCommand("df -h / | awk 'NR==2{printf \"%s/%s\", $3,$2 }'");
$file_count = runCommand("find /var/www/apps -type f | wc -l");
$connections = runCommand("ss -tn | awk 'NR>1 {print $5}' | cut -d':' -f1 | sort | uniq -c | sort -nr");
?>
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Mini CPanel Dashboard</title>
    <link rel="stylesheet" href="assets/css/panel.css">
</head>
<body>
    <div class="panel">
        <img src="assets/img/logo.png" class="logo" alt="Logo">
        <h1>Mini CPanel Dashboard</h1>

        <div class="cards">
            <div class="card"><h2>IP</h2><p><?php echo $ip; ?></p></div>
            <div class="card"><h2>MAC</h2><p><?php echo $mac; ?></p></div>
            <div class="card"><h2>Gateway</h2><p><?php echo $gateway; ?></p></div>
            <div class="card"><h2>DNS</h2><p><?php echo $dns; ?></p></div>
            <div class="card"><h2>CPU %</h2><p><?php echo $cpu_load; ?></p></div>
            <div class="card"><h2>RAM</h2><p><?php echo $mem_usage; ?></p></div>
            <div class="card"><h2>Disco</h2><p><?php echo $disk_usage; ?></p></div>
            <div class="card"><h2>Archivos Apps</h2><p><?php echo $file_count; ?></p></div>
        </div>

        <h2>Conexiones activas</h2>
        <pre style="text-align:left; max-height:200px; overflow:auto; background:#1e293b; padding:10px; border-radius:8px;"><?php echo $connections; ?></pre>
    </div>
</body>
</html>

3️⃣ Crear CSS básico

Archivo: /var/www/html/assets/css/panel.css

sudo nano /var/www/html/assets/css/panel.css
body {
    background: #0f172a;
    color: #e5e7eb;
    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    margin: 0;
}

.logo {
    max-width: 120px;
    height: auto;
    margin-bottom: 20px;
    opacity: 0.9;
}

/* --- SEPARACIÓN DE LOGIN Y PANEL --- */

/* El login se mantiene estrecho y centrado */
.login {
    max-width: 420px;
    margin: 120px auto;
    text-align: center;
    padding: 20px;
}

/* El panel ahora ocupará casi todo el ancho de la pantalla */
.panel {
    max-width: 95%; /* Cambiado de 420px a 95% */
    margin: 40px auto;
    text-align: center;
}

/* --- ELEMENTOS COMUNES --- */

input, button {
    width: 100%;
    padding: 12px;
    margin-top: 10px;
    border-radius: 8px;
    border: none;
    font-size: 14px;
    box-sizing: border-box; /* Importante para que el padding no rompa el ancho */
}

button {
    background: #3b82f6;
    color: white;
    cursor: pointer;
    font-weight: bold;
}

button:hover {
    background: #2563eb;
}

/* Estilos de los enlaces/botones del panel */
.panel a {
    display: inline-block; /* Cambiado de block a inline-block para que respeten el flex de arriba */
    margin: 5px;
    padding: 12px 20px;
    background: #1e293b;
    color: #e5e7eb;
    text-decoration: none;
    border-radius: 8px;
    transition: background 0.3s;
}

.panel a:hover {
    background: #334155;
}

🧯 ERRORES en la EJECUCIÓN de los Scripts

Corrección de Permisos: me he encontrado que los botones que apuntan a los Scripts, no funcionan y ha ocado aplicar estas correcciones a través de PuTTY:

1. Normalización de Permisos de Carpeta
Primero, corregimos la propiedad y los permisos de acceso para que el usuario del servidor web (www-data) pudiera "ver" los scripts.

Bash
# Cambiar el dueño de la carpeta al usuario del servidor web
sudo chown -R www-data:www-data /var/www/apps

# Dar permisos de lectura y ejecución a las carpetas
sudo chmod -R 755 /var/www/apps
2. Activación de Ejecución de Scripts
Este comando fue vital para que Linux permitiera que los archivos .sh funcionaran como programas y no como simples archivos de texto.

Bash
# Convertir los archivos .sh en ejecutables
sudo chmod +x /var/www/apps/admin-scripts/*.sh
3. Configuración de Privilegios Especiales (Sudoers)
Esta es la parte más sensible. Entramos al archivo de configuración de seguridad de Linux para autorizar la ejecución sin contraseña.

Comando para entrar:

Bash
sudo visudo
Líneas añadidas al final del archivo:

Plaintext
www-data ALL=(ALL) NOPASSWD: /var/www/apps/admin-scripts/reiniciar-php.sh
www-data ALL=(ALL) NOPASSWD: /var/www/apps/admin-scripts/ajustar-permisos.sh
www-data ALL=(ALL) NOPASSWD: /var/www/apps/admin-scripts/backup.sh
4. Resolución del Error en /tmp
Para el script de reinicio de PHP, habilitamos el archivo "flag" que el sistema utiliza para comunicarse con el servicio FPM.

Bash
# Crear el archivo manualmente y darle permisos de escritura universal
sudo touch /tmp/restart_php_fpm.flag
sudo chmod 666 /tmp/restart_php_fpm.flag

✅ Resultado esperado

Al abrir http://IP_DE_LA_RASPI/dashboard.php:

  • Solicita contraseña (la misma que admin.php)
  • Muestra tarjetas con IP, MAC, CPU, RAM, disco y número de archivos
  • Lista conexiones activas

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *