feat(cameras): health indicator on list page (green/yellow/red dot + status badge)

This commit is contained in:
Mitchell R 2026-05-23 01:29:05 +02:00
parent 592bdad10b
commit 2d157e900d
No known key found for this signature in database
2 changed files with 27 additions and 14 deletions

View file

@ -489,10 +489,12 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
const user = event.context.user!; const user = event.context.user!;
const cameras = deps.repo.listCameras(); const cameras = deps.repo.listCameras();
const streamCounts = new Map<number, number>(); const streamCounts = new Map<number, number>();
const activeKiosks = new Map<number, number>(); // camera_id → count of kiosks rendering
for (const cam of cameras) { for (const cam of cameras) {
streamCounts.set(cam.id, deps.repo.listCameraStreams(cam.id).length); streamCounts.set(cam.id, deps.repo.listCameraStreams(cam.id).length);
activeKiosks.set(cam.id, deps.repo.listKiosksRenderingCamera(cam.id).length);
} }
return htmlPage(CamerasPage({ user: user.username, cameras, streamCounts })); return htmlPage(CamerasPage({ user: user.username, cameras, streamCounts, activeKiosks }));
}); });
app.get("/admin/cameras/new", (event) => { app.get("/admin/cameras/new", (event) => {

View file

@ -114,6 +114,7 @@ interface CamerasProps {
user: string; user: string;
cameras: Camera[]; cameras: Camera[];
streamCounts: Map<number, number>; streamCounts: Map<number, number>;
activeKiosks: Map<number, number>;
} }
export function CamerasPage(props: CamerasProps) { export function CamerasPage(props: CamerasProps) {
@ -137,19 +138,29 @@ export function CamerasPage(props: CamerasProps) {
{props.cameras.length === 0 ? ( {props.cameras.length === 0 ? (
<tr><td colspan="4" style="text-align:center; color:#999; padding:2rem">No cameras configured</td></tr> <tr><td colspan="4" style="text-align:center; color:#999; padding:2rem">No cameras configured</td></tr>
) : ( ) : (
props.cameras.map((cam) => ( props.cameras.map((cam) => {
<tr> const streams = props.streamCounts.get(cam.id) ?? 0;
<td><a href={`/admin/cameras/${cam.id}`}><strong>{cam.name}</strong></a></td> const active = props.activeKiosks.get(cam.id) ?? 0;
<td><span class="badge badge-blue">{cam.type.toUpperCase()}</span></td> const health = !cam.enabled ? "gray"
<td>{String(props.streamCounts.get(cam.id) ?? 0)}</td> : streams === 0 ? "red"
<td> : active > 0 ? "green"
{cam.enabled : "yellow";
? <span class="badge badge-green">Enabled</span> const healthLabel = !cam.enabled ? "Disabled"
: <span class="badge badge-gray">Disabled</span> : streams === 0 ? "No streams"
} : active > 0 ? `Live (${active} kiosk${active > 1 ? "s" : ""})`
</td> : "Idle";
</tr> return (
)) <tr>
<td>
<span style={`display:inline-block;width:8px;height:8px;border-radius:50%;background:${health === "green" ? "#2a2" : health === "yellow" ? "#ca0" : health === "red" ? "#c22" : "#888"};margin-right:8px;vertical-align:middle`} />
<a href={`/admin/cameras/${cam.id}`}><strong>{cam.name}</strong></a>
</td>
<td><span class="badge badge-blue">{cam.type.toUpperCase()}</span></td>
<td>{String(streams)}</td>
<td><span class={`badge badge-${health}`}>{healthLabel}</span></td>
</tr>
);
})
)} )}
</tbody> </tbody>
</table> </table>