cosmostat working

This commit is contained in:
2026-03-29 09:39:43 -07:00
parent 97fdb3d5d8
commit 4c4d9e4d6f
19 changed files with 813 additions and 491 deletions

View File

@ -2,7 +2,7 @@
1. Global settings & color palette
------------------------------------------------- */
:root {
/* Dark theme body & card backgrounds */
/* Dark theme - body & card backgrounds */
--bg-body: #2c3e50; /* main page background */
--bg-card: #34495e; /* card / panel background */
--bg-sidebar: #3d566e; /* sidebar background (slightly lighter) */
@ -30,14 +30,23 @@ a { color: var(--clr-accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* -------------------------------------------------
2. Layout wrapper, sidebar, main
2. Layout - wrapper, sidebar, main
------------------------------------------------- */
.wrapper { display: flex; min-height: 100vh; }
.sidebar {
width: 200px;
background: var(--bg-sidebar);
position: fixed; /* keep sidebar visible during scroll */
top: 0; /* stick to the top of the viewport */
left: 0; /* align to the left edge */
height: 100vh; /* full viewport height */
/* ---- size & spacing ------------------------------------------- */
width: 200px; /* same as before */
padding: 1rem;
overflow-y: auto; /* allow sidebar content to scroll if needed */
/* ---- look ------------------------------------------------------- */
background: var(--bg-sidebar);
/* optional: keep it above other content */
z-index: 1000;
}
.sidebar h3 { margin: 0 0 .4rem 0; font-size: 1.1rem; }
.sidebar ul { list-style: none; padding: 0; margin: 0; }
@ -46,8 +55,13 @@ a:hover { text-decoration: underline; }
.sidebar a { color: var(--clr-accent); }
.sidebar a.active { font-weight: bold; }
.main { flex: 1; padding: 1rem; }
.main{
flex: 1;
padding: 1rem;
padding-left: 200px; /* space for the fixed sidebar */
/* optional: avoid accidental horizontal overflow */
overflow-x: hidden;
}
/* -------------------------------------------------
3. Card component
------------------------------------------------- */
@ -105,7 +119,7 @@ li { margin-bottom: 10px; color: var(--clr-text); }
.component h3 { margin: 0 0 5px; }
/* -------------------------------------------------
7. Help toggle / modal
7. Panel toggles / modal
------------------------------------------------- */
.help-link {
cursor: pointer;
@ -116,8 +130,117 @@ li { margin-bottom: 10px; color: var(--clr-text); }
.help-link:hover { text-decoration: underline; }
#helpText { display: none; }
.componentDetail-link {
cursor: pointer;
user-select: none;
color: var(--clr-accent);
text-align: left;
}
.componentDetail-link:hover { text-decoration: underline; }
#componentDetailText { display: none; }
/* -------------------------------------------------
8. Misc helpers
------------------------------------------------- */
/* Hide numeric markers in metric columns (if any) */
#host_metrics_column td { list-style: none; padding-left: 0; margin-left: 0; }
#host_metrics_column td { list-style: none; padding-left: 0; margin-left: 0; }
.host-status {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-left: 6px;
margin-right: 8px;
vertical-align: middle;
background: #808080; /* default unknown / no timestamp */
transition: background-color 1s linear; /* smooth fade */
}
/* -------------------------------------------------
9. Mobile adjustments
------------------------------------------------- */
@media (max-width: 768px) {
/* 1. Make the whole page a column */
.wrapper {
flex-direction: column;
}
/* 2. Hide the sidebar initially */
.sidebar {
position: relative; /* take it out of the flow */
width: 100%;
max-height: 0; /* collapsed */
overflow: hidden;
transition: max-height 0.3s ease-out;
background: var(--bg-sidebar);
padding: 0; /* remove padding */
}
.sidebar.show {
max-height: 500px; /* enough for all items */
padding: 1rem;
}
/* 3. Move the toggle button into the header */
.mobile-toggle {
display: block;
font-size: 1.5rem;
background: transparent;
border: none;
color: var(--clr-accent);
padding: 0.5rem;
margin-bottom: 0.5rem;
}
/* 4. Main content no left padding */
.main {
padding-left: 0;
padding-right: 1rem;
}
/* 5. Table scroll on small screens */
.card table {
width: 100%;
table-layout: fixed;
}
.card table thead,
.card table tbody,
.card table tr,
.card table td,
.card table th {
display: block;
}
.card table tbody {
overflow-x: auto;
white-space: nowrap;
}
.card table td,
.card table th {
display: inline-block;
vertical-align: top;
width: 30%;
}
/* 6. Adjust the host status dot positioning */
.host-status {
margin-left: 2px;
margin-right: 12px;
}
}
/* Optional: style the drawer indicator */
.sidebar.show::before {
content: "✕ Close";
display: block;
padding: 0.5rem 1rem;
background: var(--bg-sidebar);
color: var(--clr-accent);
font-weight: bold;
}

View File

@ -61,65 +61,66 @@ io.on('connection', async socket => {
/* ---------- 3. Serve static files ----------------------------------- */
/* --------------------------------------------------------------------- */
app.use(express.static('public'));
/* --- 4. Redis subscriber (patched) --------------------------------- */
const redisClient = createClient({
url: 'redis://192.168.37.1:6379',
socket: { keepAlive: 60000, // 60s TCP keep-alive
reconnectStrategy: attempts => Math.min(attempts * 100, 3000) } // back-off
});
/* --------------------------------------------------------------------- */
/* ---------- 4. Redis subscriber ------------------------------------- */
/* --------------------------------------------------------------------- */
const redisClient = createClient({ url: 'redis://192.168.37.1:6379' });
redisClient.on('error', err => console.error('Redis error', err));
(async () => {
await redisClient.connect();
const sub = redisClient.duplicate(); // duplicate to keep separate pub/sub
const sub = redisClient.duplicate();
await sub.connect();
// Subscribe to the channel that sends host stats
await sub.subscribe(
['host_metrics'],
(message, channel) => {
let payload;
try {
payload = JSON.parse(message); // message is a JSON string
} catch (e) {
console.error(`Failed to parse ${channel}`, e);
return;
}
io.emit(channel, payload);
// --------------------------------------------------------------------
// 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 the channel that sends host stats
await sub.subscribe(
['client_summary'],
(message, channel) => {
let payload;
try {
payload = JSON.parse(message); // message is a JSON string
} catch (e) {
console.error(`Failed to parse ${channel}`, e);
return;
}
// ---------------------------------------------------------------
// 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);
io.emit(channel, payload);
} catch (e) {
console.error(`Failed to parse message from ${channel}`, e);
}
);
}
// Subscribe to the channel that sends host stats
await sub.subscribe(
['client_hostnames'],
(message, channel) => {
let payload;
try {
payload = JSON.parse(message); // message is a JSON string
} catch (e) {
console.error(`Failed to parse ${channel}`, e);
return;
}
io.emit(channel, payload);
}
);
// ----------------------------------------------------------------
// 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));
});
sub.on('error', err => console.error('Subscriber error', err));
// Optional: if the connection ends for any reason, close the process
sub.on('end', () => {
console.error('Redis connection closed - exiting');
process.exit(1);
});
})();
/* --------------------------------------------------------------------- */

View File

@ -11,7 +11,7 @@ listen 80;
server_name localhost;
# ---------------------------------------
# The API only /descriptor
# API Endpoints
# ---------------------------------------
location = /descriptor {
proxy_pass http://192.168.37.1:5000/descriptor;
@ -20,6 +20,20 @@ server_name localhost;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /update_client {
proxy_pass http://192.168.37.1:5000/update_client;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /create_client {
proxy_pass http://192.168.37.1:5000/create_client;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# ---------------------------------------
# WebSocket endpoint