Enhance console layout and health handling

This commit is contained in:
Alexa Amundson
2025-11-20 20:55:57 -06:00
parent a26ebb2c8f
commit d702effb22
4 changed files with 68 additions and 43 deletions

View File

@@ -1,14 +1,23 @@
FROM node:18-alpine AS base FROM node:18-alpine AS builder
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm install RUN npm install
COPY . . COPY . .
RUN npm run build RUN npm run build
FROM node:18-alpine AS runner
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/next.config.* ./
COPY --from=builder /app/tsconfig.json ./
ENV PORT=8080 ENV PORT=8080
EXPOSE 8080 EXPOSE 8080
CMD ["npm", "start"] CMD ["npm", "start"]

View File

@@ -3,7 +3,7 @@ import { ReactNode } from 'react';
import AppShell from '@/components/layout/AppShell'; import AppShell from '@/components/layout/AppShell';
export const metadata = { export const metadata = {
title: 'Prism Console', title: 'BlackRoad OS Prism Console',
description: 'Operational console for BlackRoad OS' description: 'Operational console for BlackRoad OS'
}; };

View File

@@ -28,8 +28,11 @@ export default function Home() {
</div> </div>
</div> </div>
<div className="card" style={{ gridColumn: 'span 2' }}>
<h2>System Status</h2>
<p className="muted">Live and static readiness signals for the Prism Console.</p>
<div className="grid">
<LiveHealthCard /> <LiveHealthCard />
<div className="card"> <div className="card">
<h3>Configuration Snapshot</h3> <h3>Configuration Snapshot</h3>
<p className="muted">Resolved URLs from server/public configuration.</p> <p className="muted">Resolved URLs from server/public configuration.</p>
@@ -50,6 +53,23 @@ export default function Home() {
</tbody> </tbody>
</table> </table>
</div> </div>
<div className="card">
<h3>Dependency Checklist</h3>
<p className="muted">Configuration readiness across Prism Console dependencies.</p>
<ul>
{serviceHealth.map((service) => (
<li key={service.key}>
{service.name}:{' '}
<span className={service.configured ? 'status-ok' : 'status-bad'}>
{service.configured ? 'Configured' : 'Missing'}
</span>{' '}
<span className="muted">{service.url || 'not set'}</span>
</li>
))}
</ul>
</div>
</div>
</div>
<div className="card" style={{ gridColumn: 'span 2' }}> <div className="card" style={{ gridColumn: 'span 2' }}>
<h3>Services</h3> <h3>Services</h3>
@@ -66,22 +86,6 @@ export default function Home() {
</ul> </ul>
</div> </div>
<div className="card">
<h3>System Status</h3>
<p className="muted">Configuration readiness across Prism Console dependencies.</p>
<ul>
{serviceHealth.map((service) => (
<li key={service.key}>
{service.name}:{' '}
<span className={service.configured ? 'status-ok' : 'status-bad'}>
{service.configured ? 'Configured' : 'Missing'}
</span>{' '}
<span className="muted">{service.url || 'not set'}</span>
</li>
))}
</ul>
</div>
<div className="card"> <div className="card">
<h3>Operator Queue</h3> <h3>Operator Queue</h3>
<p className="muted">Placeholder for pending operator tasks, incidents, or approvals.</p> <p className="muted">Placeholder for pending operator tasks, incidents, or approvals.</p>

View File

@@ -17,6 +17,8 @@ type HealthResponse = {
export function LiveHealthCard() { export function LiveHealthCard() {
const [health, setHealth] = useState<HealthResponse | null>(null); const [health, setHealth] = useState<HealthResponse | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [lastChecked, setLastChecked] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -31,10 +33,14 @@ export function LiveHealthCard() {
if (!cancelled) { if (!cancelled) {
setHealth(payload); setHealth(payload);
setError(null); setError(null);
setLastChecked(payload.ts);
setLoading(false);
} }
} catch (err) { } catch (err) {
if (!cancelled) { if (!cancelled) {
setError(err instanceof Error ? err.message : 'Unable to load health'); setError(err instanceof Error ? err.message : 'Unable to load health');
setLastChecked(new Date().toISOString());
setLoading(false);
} }
} }
}; };
@@ -47,14 +53,16 @@ export function LiveHealthCard() {
}; };
}, []); }, []);
const statusText = health?.ok ? 'ONLINE' : 'OFFLINE'; const statusText = loading ? 'CHECKING' : health?.ok ? 'ONLINE' : 'OFFLINE';
const statusClass = loading ? 'badge' : health?.ok ? 'badge' : 'status-bad';
const checkedAt = health?.ts || lastChecked;
return ( return (
<div className="card"> <div className="card">
<h3>System Status</h3> <h3>System Status</h3>
<p className="muted">Live ping against <code>/api/health</code> for {serviceConfig.SERVICE_NAME}.</p> <p className="muted">Live ping against <code>/api/health</code> for {serviceConfig.SERVICE_NAME}.</p>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span className={health?.ok ? 'badge' : 'status-bad'}>Status: {statusText}</span> <span className={statusClass}>Status: {statusText}</span>
<span className="muted">Service: {serviceConfig.SERVICE_ID}</span> <span className="muted">Service: {serviceConfig.SERVICE_ID}</span>
</div> </div>
{health && ( {health && (
@@ -64,6 +72,10 @@ export function LiveHealthCard() {
<div className="muted">Last checked: {new Date(health.ts).toLocaleString()}</div> <div className="muted">Last checked: {new Date(health.ts).toLocaleString()}</div>
</div> </div>
)} )}
{!health && loading && <p className="muted">Requesting live health...</p>}
{checkedAt && !health && !loading && (
<p className="muted">Last checked: {new Date(checkedAt).toLocaleString()}</p>
)}
{error && <p className="status-bad">{error}</p>} {error && <p className="status-bad">{error}</p>}
</div> </div>
); );