230 lines
7.5 KiB
PHP
230 lines
7.5 KiB
PHP
<?php
|
||
require_once __DIR__ . '/includes/auth.php';
|
||
require_login();
|
||
require_once __DIR__ . '/includes/functions.php';
|
||
|
||
$nodes = get_all_nodes();
|
||
$stats = global_stats();
|
||
$t_ips = top_ips();
|
||
$t_ports = top_ports();
|
||
$recent = recent_connections(DASH_RECENT_LIMIT);
|
||
$node_count = count($nodes);
|
||
$enabled_count = count(array_filter($nodes, fn($n) => (bool)$n['enabled']));
|
||
|
||
// Optional per-node filter
|
||
$filter_node = isset($_GET['node']) ? (int)$_GET['node'] : null;
|
||
if ($filter_node) {
|
||
$t_ips = top_ips_by_node($filter_node);
|
||
$recent = recent_connections(DASH_RECENT_LIMIT, $filter_node);
|
||
}
|
||
|
||
$max_ip_cnt = $t_ips ? max(array_column($t_ips, 'cnt')) : 1;
|
||
$max_port_cnt = $t_ports ? max(array_column($t_ports, 'cnt')) : 1;
|
||
|
||
$upstream_version = fetch_upstream_version();
|
||
$update_available = $upstream_version && is_newer_version($upstream_version, APP_VERSION);
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta http-equiv="refresh" content="30">
|
||
<title>Dashboard – portspoof concentrator</title>
|
||
<style>
|
||
<?php include __DIR__ . '/includes/style.php'; ?>
|
||
</style>
|
||
<script>document.documentElement.setAttribute('data-theme',localStorage.getItem('theme')||'dark')</script>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>portspoof<span>concentrator</span></h1>
|
||
<nav>
|
||
<a href="index.php" class="active">Dashboard</a>
|
||
<a href="nodes.php">Nodes</a>
|
||
<a href="settings.php">Settings</a>
|
||
<?php if (auth_enabled()): ?>
|
||
<a href="logout.php" style="color:var(--muted)">Sign out</a>
|
||
<?php endif; ?>
|
||
<?php include __DIR__ . '/includes/theme_toggle.php'; ?>
|
||
</nav>
|
||
</header>
|
||
<main>
|
||
|
||
<?php if ($update_available): ?>
|
||
<div class="alert warn">
|
||
Update available: <strong>v<?= h($upstream_version) ?></strong> —
|
||
you are running <strong>v<?= h(APP_VERSION) ?></strong>.
|
||
<a href="https://git.ny.daprogs.com/DAProgs/portspoof_concentrator" target="_blank">View release ↗</a>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Stat cards -->
|
||
<div class="stats-row">
|
||
<div class="stat-card">
|
||
<div class="label">Total connections</div>
|
||
<div class="value"><?= number_format($stats['total']) ?></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">Last <?= RATE_WINDOW_SECONDS ?>s</div>
|
||
<div class="value"><?= number_format($stats['recent']) ?></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">Nodes</div>
|
||
<div class="value"><?= $enabled_count ?> <span class="muted" style="font-size:1rem">/ <?= $node_count ?></span></div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="label">Last connection</div>
|
||
<div class="value" style="font-size:1rem"><?= $stats['last'] ? h(substr($stats['last'], 0, 19)) : '—' ?></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Node filter -->
|
||
<?php if (count($nodes) > 1): ?>
|
||
<div class="card" style="padding:.75rem 1.25rem">
|
||
<span class="muted" style="font-size:.8rem">Filter by node:</span>
|
||
<a href="index.php" <?= !$filter_node ? 'style="color:var(--accent)"' : '' ?>>All</a>
|
||
<?php foreach ($nodes as $n): ?>
|
||
<a href="index.php?node=<?= (int)$n['id'] ?>"
|
||
<?= $filter_node === (int)$n['id'] ? 'style="color:var(--accent)"' : '' ?>>
|
||
<?= h($n['name']) ?>
|
||
</a>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Top IPs + Top Ports -->
|
||
<div class="two-col">
|
||
<div class="card">
|
||
<h2>
|
||
Top source IPs <small class="muted" style="font-size:.7rem;font-weight:400">(24 h)</small>
|
||
<a href="api/frequent_ips.php"
|
||
style="float:right;font-size:.75rem;font-weight:400;color:var(--accent);text-decoration:none"
|
||
target="_blank">Frequent IPs ↗</a>
|
||
</h2>
|
||
<?php if (empty($t_ips)): ?>
|
||
<p class="muted">No data yet.</p>
|
||
<?php else: ?>
|
||
<table>
|
||
<thead><tr><th>IP</th><th>Connections</th><th></th></tr></thead>
|
||
<tbody>
|
||
<?php foreach ($t_ips as $row): ?>
|
||
<tr>
|
||
<td><code><?= h($row['src_ip']) ?></code></td>
|
||
<td><?= number_format($row['cnt']) ?></td>
|
||
<td style="width:40%">
|
||
<div class="bar-wrap">
|
||
<div class="bar" style="width:<?= round($row['cnt'] / $max_ip_cnt * 100) ?>%"></div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h2>Top targeted ports <small class="muted" style="font-size:.7rem;font-weight:400">(24 h)</small></h2>
|
||
<?php if (empty($t_ports)): ?>
|
||
<p class="muted">No data yet.</p>
|
||
<?php else: ?>
|
||
<table>
|
||
<thead><tr><th>Port</th><th>Connections</th><th></th></tr></thead>
|
||
<tbody>
|
||
<?php foreach ($t_ports as $row): ?>
|
||
<tr>
|
||
<td><code><?= (int)$row['dst_port'] ?></code></td>
|
||
<td><?= number_format($row['cnt']) ?></td>
|
||
<td style="width:40%">
|
||
<div class="bar-wrap">
|
||
<div class="bar" style="width:<?= round($row['cnt'] / $max_port_cnt * 100) ?>%"></div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Recent connections -->
|
||
<div class="card">
|
||
<h2>
|
||
Recent connections <?= $filter_node ? '(filtered)' : '' ?>
|
||
<a href="api/connections.php<?= $filter_node ? '?node_id=' . $filter_node : '' ?>"
|
||
style="float:right;font-size:.75rem;font-weight:400;color:var(--accent);text-decoration:none"
|
||
target="_blank">JSON API ↗</a>
|
||
</h2>
|
||
<?php if (empty($recent)): ?>
|
||
<p class="muted">No connections ingested yet. Make sure at least one node is configured and the fetch cron is running.</p>
|
||
<?php else: ?>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Time (UTC)</th>
|
||
<th>Node</th>
|
||
<th>Source IP</th>
|
||
<th>Src port</th>
|
||
<th>Dst port</th>
|
||
<th>Banner len</th>
|
||
<th>Banner preview</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($recent as $c): ?>
|
||
<tr>
|
||
<td><?= h(substr($c['occurred_at'], 0, 19)) ?></td>
|
||
<td><?= h($c['node_name']) ?></td>
|
||
<td><code><?= h($c['src_ip']) ?></code></td>
|
||
<td><?= (int)$c['src_port'] ?></td>
|
||
<td><code><?= (int)$c['dst_port'] ?></code></td>
|
||
<td><?= (int)$c['banner_len'] ?></td>
|
||
<td>
|
||
<?php if ($c['banner_hex']): ?>
|
||
<code title="<?= h($c['banner_hex']) ?>" style="font-size:.75rem">
|
||
<?= h(mb_substr(banner_text($c['banner_hex']), 0, 40)) ?>
|
||
</code>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- Node status table -->
|
||
<div class="card">
|
||
<h2>Node status</h2>
|
||
<?php if (empty($nodes)): ?>
|
||
<p class="muted">No nodes configured. <a href="nodes.php">Add one</a>.</p>
|
||
<?php else: ?>
|
||
<table>
|
||
<thead>
|
||
<tr><th>Name</th><th>API URL</th><th>Enabled</th><th>Last fetched</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($nodes as $n): ?>
|
||
<tr>
|
||
<td><a href="index.php?node=<?= (int)$n['id'] ?>"><?= h($n['name']) ?></a></td>
|
||
<td><code><?= h($n['api_url']) ?></code></td>
|
||
<td><?= $n['enabled'] ? '<span class="badge ok">yes</span>' : '<span class="badge">no</span>' ?></td>
|
||
<td><?= $n['last_fetched_at'] ? h($n['last_fetched_at']) : '<span class="muted">never</span>' ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<p class="muted" style="font-size:.75rem; text-align:right">
|
||
Auto-refreshes every 30 s · <?= date('Y-m-d H:i:s') ?> server time
|
||
</p>
|
||
|
||
</main>
|
||
<?php include __DIR__ . '/includes/footer.php'; ?>
|
||
</body>
|
||
</html>
|