Files
cosmoserver/files/docker/web/html/index.php

509 lines
17 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
/* ---------- Utility functions ---------- */
date_default_timezone_set('America/Los_Angeles');
# 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_ids
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)
{
$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'], // same as cosmostat*/
'drive_health' => ['bind' => '172.25.1.18', 'port' => '5001'], // new 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"
]
]);
$json = @file_get_contents($url, false, $ctx);
if ($json === false) {
return []; // caller will handle empty case
}
$data = json_decode($json, true);
if (!is_array($data)) {
return ['fail'];
}
return $data;
}
/* ---------- Fetch client details ---------- */
$apiInfo = $apiConfig[$mode];
$clients = fetchClientDetails($apiInfo['bind'], $apiInfo['port']);
/* ---------- 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';
/* ---- Sidebar Renderer ---- */
function renderSidebar(string $mode){
global $clients, $client, $properties, $systemProperties, $systemComponents, $selectedHost, $selectedHosts, $selectedId, $selectedIdx, $remove_hosts;
$modes = [
'cosmostat' => 'Cosmostat',
/* 'gali' => 'Shuttle Gali',*/
'drive_health' => 'Drive Health',
];
?>
<nav class="sidebar">
<form method="get" id="modeForm">
<label for="modeSelect">Mode:</label>
<select class="select-dark" name="mode" id="modeSelect" onchange="this.form.submit()">
<?php foreach ($modes as $key => $label): ?><option value="<?= h($key) ?>" <?= $mode === $key ? 'selected' : '' ?>><?= h($label) ?></option>
<?php endforeach; ?></select>
</form>
<p>
<?php if ($mode === 'drive_health'): ?>
<?php
if ( !is_array($clients) || empty($clients) ) {
// Graceful “no data” handling
echo '<p style="margin:1rem 0; font-style:italic;">'
. 'No hosts are available to manage at this time.'
. '</p>';
}
?>
<?php if (is_array($clients) && !empty($clients)): ?>
<h3>Hosts</h3>
<form method="get" id="driveHealthForm">
<input type="hidden" name="mode" value="drive_health">
<ul>
<?php foreach ($clients as $c): ?>
<?php $id = $c['short_id']; ?>
<li>
<label>
<input type="checkbox" name="hosts[]" value="<?= h($id) ?>" <?= in_array($id, $selectedHosts, true) ? 'checked' : '' ?>><?= h($c['name']) ?>
</label>
</li>
<?php endforeach; ?>
</ul><p>
<button class="btn btn-primary" type="submit" name="action" value="apply">Apply</button><p>
<button class="btn btn-primary" type="submit" name="action" value="all">Select All</button><p>
<button class="btn btn-primary" type="submit" name="action" value="none">Select None</button><p>
</form>
<!-- Remove host button (POST) -->
<form method="post" id="removeForm">
<input type="hidden" name="mode" value="drive_health">
<input type="hidden" name="action" value="remove">
<input type="hidden" name="remove_hosts" id="remove_hosts_input" value="">
<button class="btn btn-primary" type="submit">Remove</button>
</form>
<?php endif; ?>
<?php endif; ?>
<?php if ($mode == 'gali'): ?>
<h3>Shuttle Gali</h3>
<?php endif; ?>
</nav>
<?php
}
/* ---- 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>');
}
if ($mode === 'drive_health') {
// If nothing is selected, show a friendly message
if (empty($selectedHosts)) {
echo '<div class="card"><p>No hosts selected.</p></div>';
return;
}
echo '
<div class="storage_client">';
foreach ($selectedHosts as $sid) {
// Find the client that matches this short_id
$c = null;
foreach ($clients as $cl) {
if ($cl['short_id'] === $sid) {
$c = $cl;
break;
}
}
if ($c === null) continue; // safety
$hostname = $c['name'] ?? 'Unknown';
echo '
<div class="card">
<h2>Drive Health - ' . h($hostname) . '</h2>
<h3>IP: '.h($c['ip']).'<br>
Timestamp: '.date('F j, Y g:i a', (int) $c['timestamp']).' </h3>
';
if (isset($c['drives']) && is_array($c['drives']) && count($c['drives']) > 0) {
echo '
<table id="host_metrics_table">
<thead><tr>
<th>Drive Letter</th><th>Disk ID</th><th>Health Metrics</th><th>Model</th><th>Serial</th><th>Capacity</th>
</tr></thead>
<tbody>';
foreach ($c['drives'] as $drive) {
$wear_gb = '';
if (isset($drive['host_writes'])){
$wear_gb = $drive['host_writes'].' wear';
}
echo '
<tr>
<td>' . h($drive['drive_letter'] ?? '') . '</td>
<td>' . h("".$drive['disk_id'] ?? '') . '</td>
<td>' .
h($drive['health_status'] ?? '').'<br>'.
h($drive['power_on_hours'] ?? '').'<br>'.
h($wear_gb ?? '').
'</td>
<td>' . h($drive['model'] ?? '') . '</td>
<td>' . h($drive['serial'] ?? '') . '</td>
<td>' . h($drive['capacity'] ?? '') . '</td>
</tr>';
}
echo '
</tbody></table> ';
} else {
echo '<p>No drive data available for this host.</p>';
}
echo '
</div>';
}
echo '
</div>';
return;
}
?>
<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 ---- */
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cosmostat - <?= h($selectedHost) ?></title>
<link rel="stylesheet" href="src/styles.css">
</head>
<body>
<div class="wrapper">
<!-- Sidebar -->
<?php renderSidebar($mode); ?>
<!-- Main content -->
<?php renderMainContent($mode); ?>
</div> <!-- /wrapper -->
<?php if ($mode != 'drive_health'): ?>
<!-- cosmostat javascript -->
<script src="socket.io/socket.io.js"></script>
<script src="src/system_metrics.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';
});
</script>
<?php endif; ?>
<?php if ($mode == 'drive_health'): ?>
<!-- drive health javascript -->
<script>
// Removal Handler
document.getElementById('removeForm').addEventListener('submit', function (e) {
// Grab all checked checkboxes from the Drive-Health form
const checked = document.querySelectorAll('#driveHealthForm input[type="checkbox"][name="hosts[]"]:checked');
const ids = Array.from(checked).map(cb => cb.value);
// Pass the comma-separated list to the hidden input
document.getElementById('remove_hosts_input').value = ids.join(',');
});
</script>
<?php endif; ?>
</body>
</html>