new alert service colors

This commit is contained in:
legop3
2026-01-08 22:44:31 -05:00
parent 55782dabf0
commit acebcd7307
10 changed files with 54 additions and 45 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-D8hW3Mn4.js"></script> <script type="module" crossorigin src="/assets/index-BgRdubNv.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DCrU2sZz.css"> <link rel="stylesheet" crossorigin href="/assets/index-aZHU9GG3.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+1 -9
View File
@@ -1,15 +1,8 @@
const io = require('../globals/io'); const io = require('../globals/io');
const COLORS = {
info: '#2196f3',
success: '#4caf50',
warn: '#f0b651',
error: '#e53935',
};
function sendAlert({ color, title, message, ts = Date.now() }) { function sendAlert({ color, title, message, ts = Date.now() }) {
const payload = { const payload = {
color: color || COLORS.info, color: color || '#2196f3',
title, title,
message, message,
ts, ts,
@@ -19,6 +12,5 @@ function sendAlert({ color, title, message, ts = Date.now() }) {
} }
module.exports = { module.exports = {
COLORS,
sendAlert, sendAlert,
}; };
+4 -3
View File
@@ -1,6 +1,7 @@
const EventEmitter = require('events'); const EventEmitter = require('events');
const io = require('../globals/io'); const io = require('../globals/io');
const { sendAlert, COLORS } = require('./alertService'); const { sendAlert } = require('./alertService');
const ALERT_COLOR = '#ff9800';
const { isAdmin, isLockdownAdmin } = require('./roleService'); const { isAdmin, isLockdownAdmin } = require('./roleService');
const { publishEvent } = require('./eventBus'); const { publishEvent } = require('./eventBus');
@@ -34,7 +35,7 @@ function setMode(nextMode, socket, options = {}) {
} }
currentMode = nextMode; currentMode = nextMode;
sendAlert({ sendAlert({
color: COLORS.info, color: ALERT_COLOR,
title: 'Mode Changed', title: 'Mode Changed',
message: `Server mode set to ${nextMode}`, message: `Server mode set to ${nextMode}`,
}); });
@@ -65,7 +66,7 @@ io.on('connection', (socket) => {
try { try {
setMode(mode, socket); setMode(mode, socket);
} catch (err) { } catch (err) {
sendAlert({ color: COLORS.error, title: 'Mode change failed', message: err.message }); sendAlert({ color: ALERT_COLOR, title: 'Mode change failed', message: err.message });
} }
}); });
}); });
@@ -1,20 +1,21 @@
const roverWSS = require('../globals/ws'); const roverWSS = require('../globals/ws');
const logger = require('../globals/logger').child('roverConnection'); const logger = require('../globals/logger').child('roverConnection');
const roverManager = require('./roverManager'); const roverManager = require('./roverManager');
const { sendAlert, COLORS } = require('./alertService'); const { sendAlert } = require('./alertService');
const ALERT_COLOR = '#00bcd4';
const { handleAck } = require('./commandService'); const { handleAck } = require('./commandService');
function handleMessage(roverId, msg) { function handleMessage(roverId, msg) {
switch (msg.type) { switch (msg.type) {
case 'hello': case 'hello':
roverManager.upsertRover(msg, this); roverManager.upsertRover(msg, this);
sendAlert({ color: COLORS.info, title: 'Rover Connected', message: roverId }); sendAlert({ color: ALERT_COLOR, title: 'Rover Connected', message: roverId });
break; break;
case 'sensor': case 'sensor':
roverManager.handleSensorFrame(roverId, msg); roverManager.handleSensorFrame(roverId, msg);
break; break;
case 'event': case 'event':
sendAlert({ color: COLORS.info, title: `${roverId} event`, message: msg.event }); sendAlert({ color: ALERT_COLOR, title: `${roverId} event`, message: msg.event });
break; break;
default: default:
break; break;
@@ -40,7 +41,7 @@ roverWSS.on('connection', (ws) => {
}); });
roverManager.upsertRover(msg, ws); roverManager.upsertRover(msg, ws);
roverManager.broadcastRoster(); roverManager.broadcastRoster();
sendAlert({ color: COLORS.success, title: 'Rover Online', message: roverId }); sendAlert({ color: ALERT_COLOR, title: 'Rover Online', message: roverId });
return; return;
} }
if (!roverId) return; if (!roverId) return;
@@ -49,14 +50,14 @@ roverWSS.on('connection', (ws) => {
} else if (msg.type === 'ack') { } else if (msg.type === 'ack') {
handleAck(msg); handleAck(msg);
} else if (msg.type === 'event') { } else if (msg.type === 'event') {
sendAlert({ color: COLORS.info, title: `${roverId}`, message: msg.event }); sendAlert({ color: ALERT_COLOR, title: `${roverId}`, message: msg.event });
} }
}); });
ws.on('close', () => { ws.on('close', () => {
if (roverId) { if (roverId) {
roverManager.removeRover(roverId); roverManager.removeRover(roverId);
sendAlert({ color: COLORS.warn, title: 'Rover Offline', message: roverId }); sendAlert({ color: ALERT_COLOR, title: 'Rover Offline', message: roverId });
} }
}); });
}); });
+8 -7
View File
@@ -1,7 +1,8 @@
const EventEmitter = require('events'); const EventEmitter = require('events');
const io = require('../globals/io'); const io = require('../globals/io');
const logger = require('../globals/logger').child('roverManager'); const logger = require('../globals/logger').child('roverManager');
const { sendAlert, COLORS } = require('./alertService'); const { sendAlert } = require('./alertService');
const ALERT_COLOR = '#8bc34a';
const { parseSensorFrame } = require('../helpers/sensorDecoder'); const { parseSensorFrame } = require('../helpers/sensorDecoder');
const { MODES, getMode } = require('./modeManager'); const { MODES, getMode } = require('./modeManager');
const { isAdmin, roleEvents } = require('./roleService'); const { isAdmin, roleEvents } = require('./roleService');
@@ -89,7 +90,7 @@ function lockRover(id, locked, options = {}) {
record.lockReason = reason; record.lockReason = reason;
if (!silent) { if (!silent) {
sendAlert({ sendAlert({
color: COLORS.warn, color: ALERT_COLOR,
title: 'Rover Locked', title: 'Rover Locked',
message: `${id} locked${record.lockReason ? ` (${record.lockReason})` : ''}.`, message: `${id} locked${record.lockReason ? ` (${record.lockReason})` : ''}.`,
}); });
@@ -103,7 +104,7 @@ function lockRover(id, locked, options = {}) {
record.locked = false; record.locked = false;
record.lockReason = null; record.lockReason = null;
if (!silent) { if (!silent) {
sendAlert({ color: COLORS.success, title: 'Rover Unlocked', message: `${id} unlocked.` }); sendAlert({ color: ALERT_COLOR, title: 'Rover Unlocked', message: `${id} unlocked.` });
} }
publishEvent({ publishEvent({
source: 'roverManager', source: 'roverManager',
@@ -218,7 +219,7 @@ function handleIdleUndock(undockedRecord) {
const bumpRecent = const bumpRecent =
suspectRecord.lastBumpAt && now - suspectRecord.lastBumpAt <= DOCK_GUARD_WINDOW_MS; suspectRecord.lastBumpAt && now - suspectRecord.lastBumpAt <= DOCK_GUARD_WINDOW_MS;
sendAlert({ sendAlert({
color: COLORS.warn, color: ALERT_COLOR,
title: 'Dock protection', title: 'Dock protection',
message: `${undockedRecord.id} undocked while idle; stopping ${suspect.roverId}.`, message: `${undockedRecord.id} undocked while idle; stopping ${suspect.roverId}.`,
}); });
@@ -309,7 +310,7 @@ function requestControl(roverId, socket, options = {}) {
socket.emit('controlGranted', { roverId }); socket.emit('controlGranted', { roverId });
managerEvents.emit('driver', { socketId: socket.id, roverId, action: 'add' }); managerEvents.emit('driver', { socketId: socket.id, roverId, action: 'add' });
sendAlert({ sendAlert({
color: COLORS.success, color: ALERT_COLOR,
title: 'Control Granted', title: 'Control Granted',
message: `${socket.id} now driving ${roverId}`, message: `${socket.id} now driving ${roverId}`,
}); });
@@ -464,7 +465,7 @@ io.on('connection', (socket) => {
cb({ success: true, roverId: targetId }); cb({ success: true, roverId: targetId });
} catch (err) { } catch (err) {
logger.warn('Request control failed', socket.id, err.message); logger.warn('Request control failed', socket.id, err.message);
sendAlert({ color: COLORS.warn, title: 'Control denied', message: err.message }); sendAlert({ color: ALERT_COLOR, title: 'Control denied', message: err.message });
cb({ error: err.message }); cb({ error: err.message });
} }
} }
@@ -490,7 +491,7 @@ io.on('connection', (socket) => {
cb({ success: true }); cb({ success: true });
} catch (err) { } catch (err) {
logger.warn('Lock change failed', roverId, err.message); logger.warn('Lock change failed', roverId, err.message);
sendAlert({ color: COLORS.error, title: 'Lock failed', message: err.message }); sendAlert({ color: ALERT_COLOR, title: 'Lock failed', message: err.message });
cb({ error: err.message }); cb({ error: err.message });
} }
} }
+5 -4
View File
@@ -1,6 +1,7 @@
const EventEmitter = require('events'); const EventEmitter = require('events');
const io = require('../globals/io'); const io = require('../globals/io');
const { sendAlert, COLORS } = require('./alertService'); const { sendAlert } = require('./alertService');
const ALERT_COLOR = '#ff5722';
const { MODES, getMode, modeEvents } = require('./modeManager'); const { MODES, getMode, modeEvents } = require('./modeManager');
const driverQueues = new Map(); // roverId -> { queue: [], current: socketId, timer: Timeout | null } const driverQueues = new Map(); // roverId -> { queue: [], current: socketId, timer: Timeout | null }
@@ -159,7 +160,7 @@ function handleIdleTimeout(roverId, expectedDriver) {
stopRover(roverId); stopRover(roverId);
if (skips >= MAX_IDLE_SKIPS) { if (skips >= MAX_IDLE_SKIPS) {
sendAlert({ sendAlert({
color: COLORS.error, color: ALERT_COLOR,
title: 'Driver removed', title: 'Driver removed',
message: `${expectedDriver} removed from ${roverId} after ${skips} idle skips`, message: `${expectedDriver} removed from ${roverId} after ${skips} idle skips`,
}); });
@@ -167,7 +168,7 @@ function handleIdleTimeout(roverId, expectedDriver) {
return; return;
} }
sendAlert({ sendAlert({
color: COLORS.warn, color: ALERT_COLOR,
title: 'Turn skipped', title: 'Turn skipped',
message: `${expectedDriver} skipped on ${roverId} (idle ${skips}/${MAX_IDLE_SKIPS})`, message: `${expectedDriver} skipped on ${roverId} (idle ${skips}/${MAX_IDLE_SKIPS})`,
}); });
@@ -207,7 +208,7 @@ function advanceTurn(roverId) {
queue.current = queue.queue[nextIdx]; queue.current = queue.queue[nextIdx];
setActiveDriver(roverId, queue.current); setActiveDriver(roverId, queue.current);
idleDisarmed.set(roverId, false); idleDisarmed.set(roverId, false);
sendAlert({ color: COLORS.info, title: 'Turn switch', message: `${queue.current} now controls ${roverId}` }); sendAlert({ color: ALERT_COLOR, title: 'Turn switch', message: `${queue.current} now controls ${roverId}` });
stopRover(roverId); stopRover(roverId);
scheduleNextTurn(roverId); scheduleNextTurn(roverId);
scheduleIdleTimer(roverId); scheduleIdleTimer(roverId);
+20 -7
View File
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useSession } from '../context/SessionContext.jsx'; import { useSession } from '../context/SessionContext.jsx';
const LIFETIME_MS = 5000; const LIFETIME_MS = 5000;
const DEFAULT_COLOR = '#2196f3';
function buildKey(alert) { function buildKey(alert) {
if (alert.id) return alert.id; if (alert.id) return alert.id;
@@ -38,15 +39,27 @@ export default function AlertFeed() {
); );
} }
function hexToRgb(hex) {
const safe = typeof hex === 'string' ? hex.trim() : '';
const match = /^#?([0-9a-fA-F]{6})$/.exec(safe);
if (!match) return null;
const value = match[1];
return {
r: parseInt(value.slice(0, 2), 16),
g: parseInt(value.slice(2, 4), 16),
b: parseInt(value.slice(4, 6), 16),
};
}
function AlertToast({ alert }) { function AlertToast({ alert }) {
const colorClasses = const rgb = hexToRgb(alert.color) || hexToRgb(DEFAULT_COLOR);
alert.color === 'error' const backgroundColor = rgb ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.25)` : 'rgba(33, 150, 243, 0.25)';
? 'bg-red-500/30 text-red-100' const borderColor = rgb ? `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.7)` : 'rgba(33, 150, 243, 0.7)';
: alert.color === 'warn'
? 'bg-amber-500/30 text-amber-100'
: 'bg-emerald-500/30 text-emerald-100';
return ( return (
<div className={`pointer-events-auto px-0.5 py-0.5 text-[0.85rem] ${colorClasses}`}> <div
className="pointer-events-auto px-0.5 py-0.5 text-[0.85rem] text-slate-100"
style={{ backgroundColor, border: `1px solid ${borderColor}` }}
>
<p className="text-xs text-slate-200">{alert.title || 'Alert'}</p> <p className="text-xs text-slate-200">{alert.title || 'Alert'}</p>
<p className="text-sm text-white">{alert.message}</p> <p className="text-sm text-white">{alert.message}</p>
</div> </div>
+1 -1
View File
@@ -45,7 +45,7 @@ export default function TurnAlertListener() {
pushAlert({ pushAlert({
title: 'Your turn!', title: 'Your turn!',
message: `You now control ${roverName}.`, message: `You now control ${roverName}.`,
color: 'success', color: '#ff5722',
}); });
}); });
playSound(); playSound();