first draft on new dashboard

This commit is contained in:
matt committed 2026-10-14 01:05:36 -07:00
1 parent ac2b142b7a
commit 5eb8bdd677
2 files changed
+116 -468

No files matched your search

+72 -330
View File
@@ -1,356 +1,98 @@
<?php
declare(strict_types=1);
/* ---------- Utility functions ---------- */
# ---------- Settings ----------
date_default_timezone_set('America/Los_Angeles');
$api_host = 'http://0.0.0.0';
$api_port = 5001; // drive‑health API
$api_path = '/details'; // endpoint that returns the JSON you showed
# for drive_health page, removal handler
$remove_hosts = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
if ($_POST['action'] === 'remove') {
// The Remove form sends a comma-separated string of short_id's
if (!empty($_POST['remove_hosts'])) {
$remove_hosts = array_filter(
explode(',', $_POST['remove_hosts']),
fn($s) => $s !== ''
);
} else {
$remove_hosts = [];
}
if (!empty($remove_hosts)){
foreach ($remove_hosts as $host) {
echo "remove ".$host."<br>";
}
removeClient($remove_hosts);
}
}
}
# authelia user handler
$authelia_user = "not-set";
if (isset($_SERVER['HTTP_REMOTE_USER'])) {
$authelia_user = $_SERVER['HTTP_REMOTE_USER'];
}
/* ---------- Helper: remove client details ---------- */
function removeClient($clientList)
# ---------- Helper: JSON fetch ----------
function fetch_json(string $url): ?array
{
$url = "http://0.0.0.0:5001/storage_client_delete";
$payload = [
'API_KEY' => "deadbeef",
'remove_hosts' => $clientList,
];
$json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if ($json === false) {
throw new RuntimeException('JSON encoding failed: ' . json_last_error_msg());
}
$ch = curl_init($url);
if ($ch === false) {
throw new RuntimeException('Unable to initialise cURL.');
}
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $json,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Content-Length: ' . strlen($json),
'Accept: application/json',
],
CURLOPT_RETURNTRANSFER=> true,
CURLOPT_TIMEOUT => 2,
CURLOPT_FOLLOWLOCATION=> true, // follow redirects if any
]);
// Execute curl request
$response = curl_exec($ch);
// cURL error handling
if ($response === false) {
$error = curl_error($ch);
$errno = curl_errno($ch);
curl_close($ch);
throw new RuntimeException("cURL error ({$errno}): {$error}");
}
// Grab HTTP status code
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode < 200 || $httpCode >= 300) {
throw new RuntimeException("API returned HTTP {$httpCode}: {$response}");
}
// Decode the JSON response
$decoded = json_decode($response, true);
if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) {
throw new RuntimeException('Failed to decode JSON response: ' . json_last_error_msg());
}
return $decoded;
}
/**
* Escape HTML
*/
function h(string $s): string
{
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
/**
* Load simple key/value pairs from a YAML file.
* Lines starting with '#' are ignored.
* The function returns an associative array.
*/
function loadYaml(string $path): array
{
if (!file_exists($path)) {
return [];
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$data = [];
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line[0] === '#') {
continue;
}
$pos = strpos($line, ':');
if ($pos === false) {
continue;
}
$key = trim(substr($line, 0, $pos));
$value = trim(substr($line, $pos + 1));
$value = trim($value, "\"'"); // remove surrounding quotes
if ($value === '') {
$value = null;
}
$data[$key] = $value;
}
return $data;
}
/* ---------- Load settings ---------- */
$settingsPath = '/app/cosmostat_settings.yaml';
$settings = loadYaml($settingsPath);
/* ---------- Page mode handling ---------- */
$mode = $_GET['mode'] ?? 'cosmostat'; // default mode
$validModes = ['cosmostat', 'drive_health']; // extend as needed
// 'gali',
if (!in_array($mode, $validModes, true)) {
$mode = 'cosmostat';
}
/* ---------- API configuration per mode ---------- */
$apiConfig = [
'cosmostat' => ['bind' => '10.200.27.20', 'port' => '5000'],
/*'gali' => ['bind' => '10.200.27.20', 'port' => '5000'], // space filler */
'drive_health' => ['bind' => '0.0.0.0', 'port' => '5001'], // drive health API
];
/* ---------- Helper: fetch client details ---------- */
function fetchClientDetails(string $bindIp, string $port, string $path = '/client_details'): array
{
$url = "http://{$bindIp}:{$port}{$path}";
$ctx = stream_context_create([
'http' => [
'timeout' => 2,
'header' => "User-Agent: PHP/" . PHP_VERSION . "\r\n"
]
'http' => ['timeout' => 2]
]);
$json = @file_get_contents($url, false, $ctx);
if ($json === false) {
return []; // caller will handle empty case
$raw = @file_get_contents($url, false, $ctx);
if ($raw === false) {
return null;
}
$data = json_decode($json, true);
if (!is_array($data)) {
return ['fail'];
}
return $data;
$data = json_decode($raw, true);
return is_array($data) ? $data : null;
}
/* ---------- Fetch client details ---------- */
$apiInfo = $apiConfig[$mode];
$clients = fetchClientDetails($apiInfo['bind'], $apiInfo['port']);
# ---------- Load data ----------
$api_url = "$api_host:$api_port$api_path";
$payload = fetch_json($api_url);
/* ---------- Ensure each client has a short_id ---------- */
foreach ($clients as &$client) {
if (!isset($client['short_id'])) {
$client['short_id'] = substr($client['uuid'] ?? '', 0, 8);
}
}
unset($client);
/* ---------- Determine selected hosts (Drive Health only) ---------- */
$selectedHosts = $_GET['hosts'] ?? [];
if ($mode === 'drive_health') {
if (isset($_GET['action'])) {
switch ($_GET['action']) {
case 'all':
$selectedHosts = array_column($clients, 'short_id');
break;
case 'none':
$selectedHosts = [];
break;
// 'apply' - nothing to do; $selectedHosts already contains the posted hosts
}
}
}
if ($mode === 'drive_health' && empty($selectedHosts) && !isset($_GET['action'])) {
$selectedHosts = array_column($clients, 'short_id');
}
/* ---- Determine selected host ---- */
$selectedId = $_GET['host'] ?? '';
$selectedIdx = null;
foreach ($clients as $idx => $client) {
if (isset($client['short_id']) && $client['short_id'] === $selectedId) {
$selectedIdx = $idx;
break;
}
}
if ($selectedIdx === null) {
// Default to the first client (if any)
$selectedIdx = 0;
$selectedId = $clients[$selectedIdx]['short_id'] ?? '';
}
#global $clients, $client, $properties, $systemProperties, $systemComponents, $selectedHost, $selectedHosts, $selectedId, $selectedIdx;
$client = $clients[$selectedIdx] ?? null;
$properties = $client['client_properties'][0] ?? [];
$systemProperties = $properties['system_properties'] ?? [];
$systemComponents = $properties['system_components'] ?? [];
$selectedHost = $clients[$selectedIdx]['hostname'] ?? 'Unknown';
/* ---- Main Content Renderer ---- */
function renderMainContent(string $mode){
global $clients, $client, $properties, $systemProperties, $systemComponents, $selectedHost, $selectedHosts, $remove_hosts, $selectedId, $selectedIdx;
/* ---------- Handle empty / error ---------- */
if ($clients === ['fail']) {
die('<div class="card"><p style="color:red;">Could not retrieve data from the API for mode "' . h($mode) . '".</p></div>');
}
?>
<div class="main">
<?php if ($mode == 'cosmostat'): ?><!-- Header Card -->
<div class="card">
<h2>Matt-Cloud Cosmostat Dashboard</h2>
<p>This dashboard shows the local Matt-Cloud system stats.</p>
<div class="help-link" id="helpToggle">API</div>
</div> <!-- / Header Card -->
<!-- Hidden API Card -->
<div id="helpText" class="card">
<strong>Component Desriptor</strong>
<p>To view the component descriptor, you may <br>
<code>curl -s https://<?= h($_SERVER['SERVER_NAME']) ?>/descriptor</code></p>
This will return the entire JSON descriptor variable.<br>
The endpoint agent uses this descriptor to build out its local System Object.<br>
The agent then reports back to the Cosmostat Server with all the data found in the descriptor.<br>
Full Source Code can be found at its <a target="_blank" rel="noopener noreferrer" href="https://gitea.matt-cloud.com/matt/cosmoserver">Gitea</a> page.
</div> <!-- / Header Card -->
<!-- summary card -->
<div class="card">
<?php if (!empty($systemProperties)): ?>
<h2>System Properties</h2>
<table><tr>
<td>
<ul class="system-list">
<?php foreach ($systemProperties as $prop): ?>
<li><?= h($prop['Property']) ?></li>
<?php endforeach; ?>
</ul>
</td>
<td>
<h2>Live System Metrics</h2>
<div id="host_metrics" class="column">Connecting...</div>
</td>
</tr></table>
<?php endif; ?><br>
<div class="componentDetail-link" id="componentDetailToggle">Toggle Component Details</div>
</div> <!--/summary card -->
<!-- hidden detail card -->
<div id="componentDetailText" class="card">
<?php if (!empty($systemComponents)): ?>
<h2>Components</h2>
<div class="components">
<?php foreach ($systemComponents as $comp): ?>
<div class="component">
<h3><?= h($comp['component_name']) ?></h3>
<ul class="info-list">
<?php foreach ($comp['info_strings'] as $info): ?>
<li><?= h($info) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div> <!--/hidden detail card -->
<?php endif; ?>
<?php if ($mode == 'gali'): ?>
<h3>Shuttle Gali</h3>
<?php endif; ?>
</div> <!-- /main -->
<?php
}
/* ---- Render server dashboard ---- */
# ---------- Render ----------
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cosmostat - <?= h($selectedHost) ?></title>
<title>Sensor Dashboard</title>
<link rel="stylesheet" href="src/styles.css">
<style>
table{border-collapse:collapse;width:100%;margin:1rem 0}
th,td{border:1px solid #ddd;padding:.5rem;text-align:left}
th{background:#f4f4f4}
</style>
</head>
<body>
<div class="card">
<h1>Sensor Dashboard</h1>
<div class="wrapper">
<!-- Main content -->
<?php renderMainContent($mode); ?>
</div> <!-- /wrapper -->
<!-- javascript -->
<script src="socket.io/socket.io.js"></script>
<script src="src/system_metrics.js"></script>
<?php if ($payload === null): ?>
<p style="color:red;">⚠️ Could not fetch data from the API.</p>
<?php else: ?>
<table id="sensor-table">
<thead>
<tr>
<th>Device</th>
<th>Comment</th>
<th>Temp (°C)</th>
<th>Humidity %</th>
</tr>
</thead>
<tbody>
<?php foreach ($payload['sensors'] as $s): ?>
<?php
$temp = $hum = '';
foreach ($s['data']['data'] as $d) {
if ($d['name'] === 'temp') $temp = $d['value'];
if ($d['name'] === 'humidity') $hum = $d['value'];
}
?>
<tr>
<td><?= htmlspecialchars($s['name']) ?></td>
<td><?= htmlspecialchars($s['comment'] ?? '') ?></td>
<td><?= htmlspecialchars($temp) ?></td>
<td><?= htmlspecialchars($hum) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Socket.io client -->
<script src="/socket.io/socket.io.js"></script>
<script>
// Toggles for hidden cards
document.getElementById('helpToggle').addEventListener('click', function () {
const help = document.getElementById('helpText');
help.style.display = help.style.display === 'none' || help.style.display === '' ? 'block' : 'none';
});
document.getElementById('componentDetailToggle').addEventListener('click', function () {
const help = document.getElementById('componentDetailText');
help.style.display = help.style.display === 'none' || help.style.display === '' ? 'block' : 'none';
const socket = io();
// Receive updated rows from Redis
socket.on('temp_hum', data => {
// `data` has the same structure as the API response
const tbody = document.querySelector('#sensor-table tbody');
tbody.innerHTML = ''; // clear existing rows
data.sensors.forEach(s => {
const temp = s.data.data.find(d => d.name==='temp')?.value ?? '';
const hum = s.data.data.find(d => d.name==='humidity')?.value ?? '';
const row = document.createElement('tr');
row.innerHTML = `<td>${s.name}</td><td>${s.comment}</td><td>${temp}</td><td>${hum}</td>`;
tbody.appendChild(row);
});
});
</script>
</body>
</html>