diff --git a/web_dash/web/html/index.php b/web_dash/web/html/index.php index 0925ff9..401c729 100644 --- a/web_dash/web/html/index.php +++ b/web_dash/web/html/index.php @@ -1,356 +1,98 @@ $s !== '' - ); - } else { - $remove_hosts = []; - } - if (!empty($remove_hosts)){ - foreach ($remove_hosts as $host) { - echo "remove ".$host."
"; - } - 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('

Could not retrieve data from the API for mode "' . h($mode) . '".

'); - } - - ?> -
- - -
-

Matt-Cloud Cosmostat Dashboard

-

This dashboard shows the local Matt-Cloud system stats.

- -
- -
- Component Desriptor -

To view the component descriptor, you may
- curl -s https:///descriptor

- This will return the entire JSON descriptor variable.
- The endpoint agent uses this descriptor to build out its local System Object.
- The agent then reports back to the Cosmostat Server with all the data found in the descriptor.
- Full Source Code can be found at its Gitea page. -
- -
- -

System Properties

- - - -
-
    - -
  • - -
-
-

Live System Metrics

-
Connecting...
-
-
- -
- - -
- -

Components

-
- -
-

-
    - -
  • - -
-
- -
- -
- - - - - -

Shuttle Gali

- - - -
- -Cosmostat - <?= h($selectedHost) ?> +Sensor Dashboard + +
+

Sensor Dashboard

-
- - - - - -
- - - - + +

⚠️ Could not fetch data from the API.

+ + + + + + + + + + + + + + + + + + + + + +
DeviceCommentTemp (°C)Humidity %
+ +
+ + - - \ No newline at end of file diff --git a/web_dash/web/node_server/server.js b/web_dash/web/node_server/server.js index 214ac8f..a183e28 100644 --- a/web_dash/web/node_server/server.js +++ b/web_dash/web/node_server/server.js @@ -1,153 +1,59 @@ -// server.js -const http = require('http'); +// server.js – minimal Express + Socket.io + Redis subscriber +const http = require('http'); const express = require('express'); -const { createClient } = require('redis'); const { Server } = require('socket.io'); -const fetch = require('node-fetch'); // npm i node-fetch@2 -const fs = require('fs'); -const yaml = require('js-yaml'); // npm i js-yaml -const path = require('path'); +const { createClient } = require('redis'); -const app = express(); +const app = express(); const server = http.createServer(app); -const io = new Server(server); +const io = new Server(server); -/* --------------------------------------------------------------------- */ -/* ---------- 1. Load the YAML configuration file ---------------------- */ -/* --------------------------------------------------------------------- */ -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); +// ---------- 1. Settings ---------- +const REDIS_URL = 'redis://0.0.0.0:6379'; // adjust if needed +const REDIS_CHANNEL = 'temp_hum'; // channel you subscribed to + +// ---------- 2. Serve static files ---------- +app.use(express.static('public')); // public contains index.php, src/… etc. + +// ---------- 3. Redis subscriber ---------- +const redisClient = createClient({ url: REDIS_URL }); +redisClient.on('error', err => console.error('Redis error:', err)); + +async function startRedis() { + await redisClient.connect(); + const sub = redisClient.duplicate(); + await sub.connect(); + + // Helper: forward a Redis message to all Socket.io clients + const forward = (msg) => { + try { + const payload = JSON.parse(msg); + io.emit('temp_hum', payload); // event name matches the JS listener + } catch (e) { + console.error('Failed to parse Redis message:', e); + } + }; + + await sub.subscribe(REDIS_CHANNEL, forward); + console.log(`Subscribed to Redis channel "${REDIS_CHANNEL}"`); } -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. Socket.io -------------------------------------------- */ -/* --------------------------------------------------------------------- */ -io.on('connection', async socket => { - console.log('client connected:', socket.id); +startRedis().catch(console.error); - /* ------------- send cached client_summary ------------- */ - if (clientSummaryCache.last) { - socket.emit('client_summary', clientSummaryCache.last); - console.log('sent cached client_summary to', socket.id); - } +// ---------- 4. Socket.io ---------- +io.on('connection', socket => { + console.log('Socket.io client connected:', socket.id); - /* ----------------------------------------------------------------- */ - /* 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); - } + // Optionally send the most recent snapshot + // (If you keep a cache, emit it here) - /* ----------------------------------------------------------------- */ - /* 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); - } - }); + socket.on('disconnect', () => { + console.log('Socket.io client disconnected:', socket.id); + }); }); -/* --------------------------------------------------------------------- */ -/* ---------- 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: } - -/* --------------------------------------------------------------------- */ -(async () => { - await redisClient.connect(); - const sub = redisClient.duplicate(); - await sub.connect(); - - /* --------------------------------------------------------------------- */ - /* Helper that re-subscribes to a channel (and re-sends the handler) */ - /* --------------------------------------------------------------------- */ - async function safeSubscribe(channel, handler) { - try { - await sub.subscribe(channel, handler); - console.log(`Subscribed to ${channel}`); - } catch (e) { - console.error(`Failed to subscribe to ${channel}`, e); - } - } - - /* --------------------------------------------------------------------- */ - /* Subscribe to all required channels */ - /* --------------------------------------------------------------------- */ - await safeSubscribe('host_metrics', (msg) => forward('host_metrics', msg)); - await safeSubscribe('client_summary', (msg) => forward('client_summary', msg)); - - /* --------------------------------------------------------------------- */ - /* Forward messages to Socket.io */ - /* --------------------------------------------------------------------- */ - function forward(channel, message) { - try { - const payload = JSON.parse(message); - - /* ----- update cache on client_summary ----- */ - if (channel === 'client_summary') { - clientSummaryCache.last = payload; - } - - io.emit(channel, payload); - } catch (e) { - console.error(`Failed to parse message from ${channel}`, e); - } - } - - /* --------------------------------------------------------------------- */ - /* Re-subscribe automatically when the Redis connection reconnects */ - /* --------------------------------------------------------------------- */ - 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 */ - sub.on('end', () => { - console.error('Redis connection closed - exiting'); - process.exit(1); - }); -})(); - -/* --------------------------------------------------------------------- */ -/* ---------- 5. Start the HTTP server --------------------------------- */ -/* --------------------------------------------------------------------- */ +// ---------- 5. Start HTTP server ---------- const PORT = process.env.PORT || 3000; server.listen(PORT, () => { - console.log(`Server listening on http://localhost:${PORT}`); + console.log(`Dashboard server listening on http://localhost:${PORT}`); }); \ No newline at end of file