first draft on new dashboard
This commit is contained in:
1 parent
ac2b142b7a
commit
5eb8bdd677
2 files changed
+102
-454
No files matched your search
+71
-329
@@ -1,356 +1,98 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
/* ---------- Utility functions ---------- */
|
# ---------- Settings ----------
|
||||||
date_default_timezone_set('America/Los_Angeles');
|
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
|
# ---------- Helper: JSON fetch ----------
|
||||||
$remove_hosts = [];
|
function fetch_json(string $url): ?array
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
|
|
||||||
$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([
|
$ctx = stream_context_create([
|
||||||
'http' => [
|
'http' => ['timeout' => 2]
|
||||||
'timeout' => 2,
|
|
||||||
'header' => "User-Agent: PHP/" . PHP_VERSION . "\r\n"
|
|
||||||
]
|
|
||||||
]);
|
]);
|
||||||
$json = @file_get_contents($url, false, $ctx);
|
$raw = @file_get_contents($url, false, $ctx);
|
||||||
if ($json === false) {
|
if ($raw === false) {
|
||||||
return []; // caller will handle empty case
|
return null;
|
||||||
}
|
}
|
||||||
$data = json_decode($json, true);
|
$data = json_decode($raw, true);
|
||||||
if (!is_array($data)) {
|
return is_array($data) ? $data : null;
|
||||||
return ['fail'];
|
|
||||||
}
|
|
||||||
return $data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Fetch client details ---------- */
|
# ---------- Load data ----------
|
||||||
$apiInfo = $apiConfig[$mode];
|
$api_url = "$api_host:$api_port$api_path";
|
||||||
$clients = fetchClientDetails($apiInfo['bind'], $apiInfo['port']);
|
$payload = fetch_json($api_url);
|
||||||
|
|
||||||
/* ---------- Ensure each client has a short_id ---------- */
|
# ---------- Render ----------
|
||||||
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 ---- */
|
|
||||||
?>
|
?>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Cosmostat - <?= h($selectedHost) ?></title>
|
<title>Sensor Dashboard</title>
|
||||||
<link rel="stylesheet" href="src/styles.css">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Sensor Dashboard</h1>
|
||||||
|
|
||||||
<div class="wrapper">
|
<?php if ($payload === null): ?>
|
||||||
|
<p style="color:red;">⚠️ Could not fetch data from the API.</p>
|
||||||
<!-- Main content -->
|
<?php else: ?>
|
||||||
<?php renderMainContent($mode); ?>
|
<table id="sensor-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
</div> <!-- /wrapper -->
|
<th>Device</th>
|
||||||
|
<th>Comment</th>
|
||||||
<!-- javascript -->
|
<th>Temp (°C)</th>
|
||||||
<script src="socket.io/socket.io.js"></script>
|
<th>Humidity %</th>
|
||||||
<script src="src/system_metrics.js"></script>
|
</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>
|
<script>
|
||||||
// Toggles for hidden cards
|
const socket = io();
|
||||||
document.getElementById('helpToggle').addEventListener('click', function () {
|
|
||||||
const help = document.getElementById('helpText');
|
// Receive updated rows from Redis
|
||||||
help.style.display = help.style.display === 'none' || help.style.display === '' ? 'block' : 'none';
|
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);
|
||||||
});
|
});
|
||||||
document.getElementById('componentDetailToggle').addEventListener('click', function () {
|
|
||||||
const help = document.getElementById('componentDetailText');
|
|
||||||
help.style.display = help.style.display === 'none' || help.style.display === '' ? 'block' : 'none';
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,153 +1,59 @@
|
|||||||
// server.js
|
// server.js – minimal Express + Socket.io + Redis subscriber
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { createClient } = require('redis');
|
|
||||||
const { Server } = require('socket.io');
|
const { Server } = require('socket.io');
|
||||||
const fetch = require('node-fetch'); // npm i node-fetch@2
|
const { createClient } = require('redis');
|
||||||
const fs = require('fs');
|
|
||||||
const yaml = require('js-yaml'); // npm i js-yaml
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const io = new Server(server);
|
const io = new Server(server);
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
// ---------- 1. Settings ----------
|
||||||
/* ---------- 1. Load the YAML configuration file ---------------------- */
|
const REDIS_URL = 'redis://0.0.0.0:6379'; // adjust if needed
|
||||||
/* --------------------------------------------------------------------- */
|
const REDIS_CHANNEL = 'temp_hum'; // channel you subscribed to
|
||||||
let config = {};
|
|
||||||
try {
|
|
||||||
const filePath = '/app/cosmostat_settings.yaml';
|
|
||||||
const file = fs.readFileSync(filePath, 'utf8');
|
|
||||||
config = yaml.load(file);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load config.yaml:', e);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
const API_PORT = config.custom_api_port || 5000; // fallback to 5000
|
|
||||||
const API_HOST = config.api_bind_ip || '192.168.37.1'; // fallback IP
|
|
||||||
const API_BASE = `http://${API_HOST}:${API_PORT}`;
|
|
||||||
console.log('API URL:', API_BASE);
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
// ---------- 2. Serve static files ----------
|
||||||
/* ---------- 2. Socket.io -------------------------------------------- */
|
app.use(express.static('public')); // public contains index.php, src/… etc.
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
io.on('connection', async socket => {
|
|
||||||
console.log('client connected:', socket.id);
|
|
||||||
|
|
||||||
/* ------------- send cached client_summary ------------- */
|
// ---------- 3. Redis subscriber ----------
|
||||||
if (clientSummaryCache.last) {
|
const redisClient = createClient({ url: REDIS_URL });
|
||||||
socket.emit('client_summary', clientSummaryCache.last);
|
redisClient.on('error', err => console.error('Redis error:', err));
|
||||||
console.log('sent cached client_summary to', socket.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------- */
|
async function startRedis() {
|
||||||
/* Call the external API every time a client connects */
|
|
||||||
/* ----------------------------------------------------------------- */
|
|
||||||
try {
|
|
||||||
const resp = await fetch(`${API_BASE}/start_timer`, { method: 'GET' });
|
|
||||||
const data = await resp.json();
|
|
||||||
console.log('API responded to connect:', data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to hit start_timer endpoint:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ----------------------------------------------------------------- */
|
|
||||||
/* Listen for tableRendered event from the client */
|
|
||||||
/* ----------------------------------------------------------------- */
|
|
||||||
socket.on('tableRendered', async () => {
|
|
||||||
console.log('Client reported table rendered - starting timer');
|
|
||||||
try {
|
|
||||||
const resp = await fetch(`${API_BASE}/start_timer`, { method: 'GET' });
|
|
||||||
const text = await resp.text();
|
|
||||||
console.log('Timer endpoint responded:', text);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to hit start_timer:', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
/* ---------- 3. Serve static files ----------------------------------- */
|
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
app.use(express.static('public'));
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
/* ---------- 4. Redis subscriber ------------------------------------- */
|
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
const redisClient = createClient({
|
|
||||||
url: 'redis://0.0.0.0:6379',
|
|
||||||
socket: { keepAlive: 60000, // 60 s TCP keep-alive
|
|
||||||
reconnectStrategy: attempts => Math.min(attempts * 100, 3000) } // back-off
|
|
||||||
});
|
|
||||||
redisClient.on('error', err => console.error('Redis error', err));
|
|
||||||
|
|
||||||
/* --- local cache for client_summary -------------------------------- */
|
|
||||||
const clientSummaryCache = {}; // { last: <payload> }
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
(async () => {
|
|
||||||
await redisClient.connect();
|
await redisClient.connect();
|
||||||
const sub = redisClient.duplicate();
|
const sub = redisClient.duplicate();
|
||||||
await sub.connect();
|
await sub.connect();
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
// Helper: forward a Redis message to all Socket.io clients
|
||||||
/* Helper that re-subscribes to a channel (and re-sends the handler) */
|
const forward = (msg) => {
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
async function safeSubscribe(channel, handler) {
|
|
||||||
try {
|
try {
|
||||||
await sub.subscribe(channel, handler);
|
const payload = JSON.parse(msg);
|
||||||
console.log(`Subscribed to ${channel}`);
|
io.emit('temp_hum', payload); // event name matches the JS listener
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to subscribe to ${channel}`, e);
|
console.error('Failed to parse Redis message:', e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
await sub.subscribe(REDIS_CHANNEL, forward);
|
||||||
/* Subscribe to all required channels */
|
console.log(`Subscribed to Redis channel "${REDIS_CHANNEL}"`);
|
||||||
/* --------------------------------------------------------------------- */
|
}
|
||||||
await safeSubscribe('host_metrics', (msg) => forward('host_metrics', msg));
|
|
||||||
await safeSubscribe('client_summary', (msg) => forward('client_summary', msg));
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
startRedis().catch(console.error);
|
||||||
/* Forward messages to Socket.io */
|
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
function forward(channel, message) {
|
|
||||||
try {
|
|
||||||
const payload = JSON.parse(message);
|
|
||||||
|
|
||||||
/* ----- update cache on client_summary ----- */
|
// ---------- 4. Socket.io ----------
|
||||||
if (channel === 'client_summary') {
|
io.on('connection', socket => {
|
||||||
clientSummaryCache.last = payload;
|
console.log('Socket.io client connected:', socket.id);
|
||||||
}
|
|
||||||
|
|
||||||
io.emit(channel, payload);
|
// Optionally send the most recent snapshot
|
||||||
} catch (e) {
|
// (If you keep a cache, emit it here)
|
||||||
console.error(`Failed to parse message from ${channel}`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
socket.on('disconnect', () => {
|
||||||
/* Re-subscribe automatically when the Redis connection reconnects */
|
console.log('Socket.io client disconnected:', socket.id);
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
sub.on('reconnecting', () => console.log('Redis reconnecting…'));
|
|
||||||
sub.on('ready', async () => {
|
|
||||||
console.log('Redis ready - re-subscribing to all channels');
|
|
||||||
await safeSubscribe('host_metrics', (msg) => forward('host_metrics', msg));
|
|
||||||
await safeSubscribe('client_summary', (msg) => forward('client_summary', msg));
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* Optional: if the connection ends for any reason, close the process */
|
// ---------- 5. Start HTTP server ----------
|
||||||
sub.on('end', () => {
|
|
||||||
console.error('Redis connection closed - exiting');
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
/* ---------- 5. Start the HTTP server --------------------------------- */
|
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
server.listen(PORT, () => {
|
server.listen(PORT, () => {
|
||||||
console.log(`Server listening on http://localhost:${PORT}`);
|
console.log(`Dashboard server listening on http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user