mirror of
https://github.com/mivodev/mivo.git
synced 2026-01-26 05:25:42 +07:00
357 lines
17 KiB
PHP
357 lines
17 KiB
PHP
<?php
|
|
require_once ROOT . '/app/Views/layouts/header_main.php';
|
|
?>
|
|
|
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-8 gap-4">
|
|
<div>
|
|
<h1 class="text-2xl font-bold tracking-tight" data-i18n="common.dashboard">Dashboard</h1>
|
|
<p class="text-accents-5"><span data-i18n="common.session">Session</span>: <strong class="text-foreground"><?= $session ?></strong></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<!-- System Info Card -->
|
|
<div class="card space-y-5">
|
|
<div class="flex items-center gap-2">
|
|
<i data-lucide="cpu" class="w-5 h-5"></i>
|
|
<h3 class="font-semibold text-lg" data-i18n="dashboard.system_info">System Info</h3>
|
|
</div>
|
|
<div class="text-sm space-y-2">
|
|
<div class="flex justify-between border-b border-accents-2 pb-2">
|
|
<span class="text-accents-5" data-i18n="dashboard.model">Model</span>
|
|
<span class="font-medium"><?= $routerboard['model'] ?? '-' ?></span>
|
|
</div>
|
|
<div class="flex justify-between border-b border-accents-2 pb-2">
|
|
<span class="text-accents-5" data-i18n="dashboard.board_name">Board Name</span>
|
|
<span class="font-medium"><?= $resource['board-name'] ?? '-' ?></span>
|
|
</div>
|
|
<div class="flex justify-between border-b border-accents-2 pb-2">
|
|
<span class="text-accents-5" data-i18n="dashboard.router_os">RouterOS</span>
|
|
<span class="font-medium"><?= $resource['version'] ?? '-' ?></span>
|
|
</div>
|
|
<div class="flex justify-between border-b border-accents-2 pb-2">
|
|
<span class="text-accents-5" data-i18n="dashboard.architecture">Architecture</span>
|
|
<span class="font-medium"><?= $resource['architecture-name'] ?? '-' ?></span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-accents-5" data-i18n="dashboard.uptime">Uptime</span>
|
|
<span class="font-medium"><?= \App\Helpers\FormatHelper::elapsedTime($resource['uptime'] ?? '-') ?></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Resources Card -->
|
|
<div class="card space-y-5">
|
|
<div class="flex items-center gap-2">
|
|
<i data-lucide="hard-drive" class="w-5 h-5"></i>
|
|
<h3 class="font-semibold text-lg" data-i18n="dashboard.resources">Resources</h3>
|
|
</div>
|
|
|
|
<!-- CPU Config (simple progress not calculated here for cpu-load as it fluctuates, just text) -->
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between text-sm">
|
|
<span data-i18n="dashboard.cpu_load">CPU Load</span>
|
|
<span class="font-bold"><?= $resource['cpu-load'] ?? 0 ?>%</span>
|
|
</div>
|
|
<div class="h-2 w-full bg-accents-2 rounded-full overflow-hidden">
|
|
<div class="h-full bg-foreground" style="width: <?= $resource['cpu-load'] ?? 0 ?>%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between text-sm">
|
|
<span data-i18n="dashboard.memory">Memory</span>
|
|
<span class="text-accents-5"><?= \App\Helpers\FormatHelper::formatBytes($resource['free-memory']??0, 1) ?> <span data-i18n="dashboard.free">Free</span></span>
|
|
</div>
|
|
<div class="h-2 w-full bg-accents-2 rounded-full overflow-hidden">
|
|
<?php
|
|
$totalMem = ($resource['total-memory']??1);
|
|
$freeMem = ($resource['free-memory']??0);
|
|
$usedMemP = (($totalMem - $freeMem) / $totalMem) * 100;
|
|
?>
|
|
<div class="h-full bg-blue-600 dark:bg-blue-500" style="width:<?= $usedMemP ?>%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between text-sm">
|
|
<span data-i18n="dashboard.hdd">HDD</span>
|
|
<span class="text-accents-5"><?= \App\Helpers\FormatHelper::formatBytes($resource['free-hdd-space']??0, 1) ?> <span data-i18n="dashboard.free">Free</span></span>
|
|
</div>
|
|
<div class="h-2 w-full bg-accents-2 rounded-full overflow-hidden">
|
|
<?php
|
|
$totalHdd = ($resource['total-hdd-space']??1);
|
|
$freeHdd = ($resource['free-hdd-space']??0);
|
|
$usedHddP = (($totalHdd - $freeHdd) / $totalHdd) * 100;
|
|
?>
|
|
<div class="h-full bg-foreground" style="width:<?= $usedHddP ?>%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hotspot Stats -->
|
|
<div class="col-span-full md:col-span-1 lg:col-span-1 card flex flex-col justify-center space-y-5">
|
|
<div class="flex items-center gap-2">
|
|
<i data-lucide="wifi" class="w-5 h-5"></i>
|
|
<h3 class="font-semibold text-lg" data-i18n="hotspot_menu.hotspot">Hotspot</h3>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 md:grid-cols-1 xl:grid-cols-2 gap-4">
|
|
<!-- Active Hotspot -->
|
|
<div class="sub-card text-center group relative aspect-square flex flex-col justify-center items-center w-full max-w-[140px] mx-auto">
|
|
<a href="/<?= htmlspecialchars($session) ?>/hotspot/active" class="absolute inset-0 z-10" title="View Active Users"></a>
|
|
<div class="flex justify-center mb-2 text-blue-500 dark:text-blue-400 group-hover:scale-110 transition-transform">
|
|
<i data-lucide="activity" class="w-6 h-6"></i>
|
|
</div>
|
|
<div class="text-2xl font-bold text-foreground"><?= $hotspot_active ?></div>
|
|
<div class="text-xs text-accents-5 uppercase tracking-wide font-semibold mt-1" data-i18n="status_menu.active">Active</div>
|
|
</div>
|
|
|
|
<!-- Users -->
|
|
<div class="sub-card text-center group relative aspect-square flex flex-col justify-center items-center w-full max-w-[140px] mx-auto">
|
|
<a href="/<?= htmlspecialchars($session) ?>/hotspot/users" class="absolute inset-0 z-10" title="Manage Users"></a>
|
|
<div class="flex justify-center mb-2 text-purple-500 dark:text-purple-400 group-hover:scale-110 transition-transform">
|
|
<i data-lucide="users" class="w-6 h-6"></i>
|
|
</div>
|
|
<div class="text-2xl font-bold text-foreground"><?= htmlspecialchars($hotspot_users['count'] ?? 0) ?></div>
|
|
<div class="text-xs text-accents-5 uppercase tracking-wide font-semibold mt-1" data-i18n="hotspot_menu.users">Users</div>
|
|
</div>
|
|
|
|
<!-- Income -->
|
|
<div class="sub-card text-center col-span-2 group">
|
|
<div class="flex justify-center mb-2 text-yellow-500 dark:text-yellow-400 group-hover:scale-110 transition-transform">
|
|
<i data-lucide="dollar-sign" class="w-6 h-6"></i>
|
|
</div>
|
|
<div class="text-2xl font-bold text-foreground">0</div>
|
|
<div class="text-xs text-accents-5 uppercase tracking-wide font-semibold mt-1" data-i18n="dashboard.income_today">Income Today</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Traffic Monitor -->
|
|
<div class="col-span-full card space-y-4">
|
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div class="flex flex-col sm:flex-row sm:items-center gap-4 w-full sm:w-auto">
|
|
<div class="flex items-center gap-2">
|
|
<i data-lucide="activity" class="w-5 h-5 text-blue-500"></i>
|
|
<h3 class="font-semibold text-lg" data-i18n="dashboard.traffic_monitor">Traffic Monitor</h3>
|
|
</div>
|
|
<div class="relative w-full sm:w-auto">
|
|
<select id="traffic-interface" class="custom-select w-full sm:w-48">
|
|
<option value="" disabled selected>Loading...</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2 text-xs text-accents-5 self-end sm:self-auto">
|
|
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-blue-500"></span> <span data-i18n="dashboard.rx_download">Rx (Download)</span></span>
|
|
<span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-green-500"></span> <span data-i18n="dashboard.tx_upload">Tx (Upload)</span></span>
|
|
</div>
|
|
</div>
|
|
<div class="relative h-64 w-full">
|
|
<canvas id="trafficChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/assets/js/chart.min.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const ctx = document.getElementById('trafficChart').getContext('2d');
|
|
const labels = Array(20).fill('');
|
|
const rxData = Array(20).fill(0);
|
|
const txData = Array(20).fill(0);
|
|
|
|
// Chart Configuration
|
|
const chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: window.i18n ? window.i18n.t('dashboard.rx_download') : 'Rx (Download)',
|
|
data: rxData,
|
|
borderColor: '#3b82f6', // blue-500
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
borderWidth: 2,
|
|
tension: 0.4,
|
|
fill: true,
|
|
pointRadius: 0
|
|
},
|
|
{
|
|
label: window.i18n ? window.i18n.t('dashboard.tx_upload') : 'Tx (Upload)',
|
|
data: txData,
|
|
borderColor: '#22c55e', // green-500
|
|
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
|
borderWidth: 2,
|
|
tension: 0.4,
|
|
fill: true,
|
|
pointRadius: 0
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
animation: false, // Disable animation for smoother realtime updates
|
|
interaction: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
},
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return context.dataset.label + ': ' + formatBits(context.raw);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: false,
|
|
grid: { display: false }
|
|
},
|
|
y: {
|
|
border: { display: false },
|
|
grid: { color: 'rgba(128, 128, 128, 0.1)' },
|
|
ticks: {
|
|
callback: function(value) {
|
|
return formatBits(value);
|
|
}
|
|
},
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Helper: Format Bits
|
|
function formatBits(bits) {
|
|
if (bits === 0) return '0 bps';
|
|
const units = ['bps', 'Kbps', 'Mbps', 'Gbps'];
|
|
const i = Math.floor(Math.log(bits) / Math.log(1024));
|
|
return parseFloat((bits / Math.pow(1024, i)).toFixed(1)) + ' ' + units[i];
|
|
}
|
|
|
|
// Fetch Data
|
|
const session = '<?= htmlspecialchars($session) ?>';
|
|
let currentInterface = null; // Will be set after fetching interfaces
|
|
|
|
async function fetchInterfaces() {
|
|
try {
|
|
const response = await fetch(`/${session}/traffic/interfaces`);
|
|
if (!response.ok) return;
|
|
|
|
const interfaces = await response.json();
|
|
const select = document.getElementById('traffic-interface');
|
|
select.innerHTML = ''; // access clean
|
|
|
|
if (Array.isArray(interfaces)) {
|
|
interfaces.forEach(iface => {
|
|
const option = document.createElement('option');
|
|
option.value = iface.name;
|
|
option.textContent = iface.name; // Simple name, can add type if needed
|
|
select.appendChild(option);
|
|
});
|
|
|
|
// Set default (ether1 or first one)
|
|
// Priority: Configured Interface > ether1 > First available
|
|
const configInterface = '<?= $interface ?>'; // From Controller
|
|
let defaultIface = null;
|
|
|
|
if (configInterface && interfaces.find(i => i.name === configInterface)) {
|
|
defaultIface = configInterface;
|
|
} else if (interfaces.find(i => i.name === 'ether1')) {
|
|
defaultIface = 'ether1';
|
|
} else {
|
|
defaultIface = interfaces[0]?.name;
|
|
}
|
|
|
|
if (defaultIface) {
|
|
select.value = defaultIface;
|
|
currentInterface = defaultIface;
|
|
}
|
|
|
|
// Refresh Custom Select UI
|
|
if (typeof CustomSelect !== 'undefined' && CustomSelect.instances) {
|
|
const instance = CustomSelect.instances.find(i => i.originalSelect.id === 'traffic-interface');
|
|
if (instance) instance.refresh();
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error("Interfaces fetch error:", err);
|
|
document.getElementById('traffic-interface').innerHTML = '<option>Error</option>';
|
|
}
|
|
}
|
|
|
|
// Handle Change
|
|
document.getElementById('traffic-interface').addEventListener('change', (e) => {
|
|
currentInterface = e.target.value;
|
|
// Clear chart for visual feedback? Or just let it transition
|
|
rxData.fill(0);
|
|
txData.fill(0);
|
|
chart.update();
|
|
});
|
|
|
|
async function fetchTraffic() {
|
|
if (!currentInterface) return;
|
|
|
|
try {
|
|
// Encode interface name to handle special chars / spaces
|
|
const response = await fetch(`/${session}/traffic/monitor?interface=${encodeURIComponent(currentInterface)}`);
|
|
if (!response.ok) return; // Silent fail
|
|
|
|
const data = await response.json();
|
|
|
|
if (data && !data.error) {
|
|
// Update Data (Shift and Push)
|
|
chart.data.datasets[0].data.push(parseInt(data['rx-bits-per-second']));
|
|
chart.data.datasets[0].data.shift();
|
|
|
|
chart.data.datasets[1].data.push(parseInt(data['tx-bits-per-second']));
|
|
chart.data.datasets[1].data.shift();
|
|
|
|
chart.update('none'); // Update without animation
|
|
}
|
|
} catch (err) {
|
|
console.error("Traffic fetch error:", err);
|
|
}
|
|
}
|
|
|
|
// Init
|
|
fetchInterfaces().then(() => {
|
|
// Start Polling after interfaces loaded
|
|
const reloadInterval = <?= ($reload_interval ?? 5) * 1000 ?>; // Convert sec to ms
|
|
setInterval(fetchTraffic, reloadInterval);
|
|
fetchTraffic();
|
|
});
|
|
|
|
// Localization Support
|
|
const updateChartLabels = () => {
|
|
if (window.i18n && window.i18n.isLoaded) {
|
|
const rxLabel = window.i18n.t('dashboard.rx_download');
|
|
const txLabel = window.i18n.t('dashboard.tx_upload');
|
|
|
|
// Only update if changed
|
|
if (chart.data.datasets[0].label !== rxLabel || chart.data.datasets[1].label !== txLabel) {
|
|
chart.data.datasets[0].label = rxLabel;
|
|
chart.data.datasets[1].label = txLabel;
|
|
chart.update('none');
|
|
}
|
|
}
|
|
};
|
|
|
|
// Listen for language changes
|
|
if (window.Mivo) {
|
|
window.Mivo.on('languageChanged', updateChartLabels);
|
|
}
|
|
window.addEventListener('languageChanged', updateChartLabels);
|
|
|
|
// Try initial update after a short delay to ensure i18n is ready if race condition
|
|
setTimeout(updateChartLabels, 500);
|
|
});
|
|
</script>
|
|
|
|
<?php require_once ROOT . '/app/Views/layouts/footer_main.php'; ?>
|