From 6f0054c87854352b2e102bd95cd332dbadd9ad6c Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Sun, 2 Aug 2026 03:27:05 +0200 Subject: [PATCH] feat(ui): redesign operations dashboard --- frontend/src/pages/Dashboard.tsx | 198 +++++++++++++++++-------------- 1 file changed, 111 insertions(+), 87 deletions(-) diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index cef6170..a916817 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,110 +1,134 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { api } from "../api/client"; -import type { Dashboard as DashboardData } from "../api/types"; +import type { Dashboard as DashboardData, KnowledgeHealth } from "../api/types"; import { SeverityBadge, StatusBadge } from "../components/Badge"; +import { Icon } from "../components/Icons"; +import { ErrorState, IntegrationMark, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome"; -const METRIC_LABELS: Record = { - available: "Available", - rented: "Rented", - cleaning: "Cleaning", - maintenance: "Maintenance", - blocked: "Blocked", - open_quality_issues: "Open quality issues", - pending_or_failed_workflows: "Pending/failed workflows", -}; +const FLEET_METRICS: Array<{ key: keyof DashboardData["metrics"]; label: string; tone: string }> = [ + { key: "available", label: "Available", tone: "ready" }, + { key: "rented", label: "Rented", tone: "neutral" }, + { key: "cleaning", label: "Cleaning", tone: "neutral" }, + { key: "maintenance", label: "Maintenance", tone: "warning" }, + { key: "blocked", label: "Blocked", tone: "critical" }, +]; + +function localTime(value: string, withDate = false) { + return new Date(value).toLocaleString("en-GB", { + ...(withDate ? { day: "2-digit", month: "short" } : {}), + hour: "2-digit", + minute: "2-digit", + timeZone: "Europe/Brussels", + }); +} export function Dashboard() { const [data, setData] = useState(null); + const [knowledge, setKnowledge] = useState(null); const [error, setError] = useState(null); + const [severity, setSeverity] = useState("all"); + const [query, setQuery] = useState(""); useEffect(() => { - api - .get("/api/v1/dashboard") - .then(setData) - .catch(() => setError("Dashboard data is unavailable right now.")); + api.get("/api/v1/dashboard").then(setData).catch(() => setError("Dashboard data is unavailable right now.")); + api.get("/api/v1/knowledge/status").then(setKnowledge).catch(() => setKnowledge(null)); }, []); - if (error) return

{error}

; - if (!data) return

Loading dashboard…

; + const attention = useMemo(() => data?.attention_items.filter((item) => { + const matchesSeverity = severity === "all" || item.severity === severity; + const haystack = `${item.title} ${item.detail} ${item.link_ref}`.toLowerCase(); + return matchesSeverity && haystack.includes(query.toLowerCase()); + }) ?? [], [data, query, severity]); + + if (error) return ; + if (!data) return ; + + const latestRun = data.recent_automation[0]; + const n8nState = !latestRun ? "No delivery yet" : latestRun.status === "failed" ? "Needs attention" : latestRun.status; return ( -
-

Dashboard

+
+ View fleet} /> -
-

Operational metrics

-
    - {(Object.keys(METRIC_LABELS) as (keyof DashboardData["metrics"])[]).map((key) => ( -
  • - {data.metrics[key]} - {METRIC_LABELS[key]} -
  • +
    +
    + +

    Fleet readiness

    Live from persisted vehicle state

    +
    +
    + {FLEET_METRICS.map((metric) => ( +
    +
    {metric.label}
    {data.metrics[metric.key]}
    +
    ))} -
+ + Open fleet
-
-

Attention required

- {data.attention_items.length === 0 &&

Nothing needs attention right now.

} -
    - {data.attention_items.map((item, index) => ( -
  • - -
    -

    - {item.issue_ref ? ( - {item.title} - ) : item.link_type === "vehicle" ? ( - {item.title} - ) : ( - item.title - )} -

    -

    {item.detail}

    -
    -
  • - ))} -
-
+
+
+ Review queue } /> +
+ + +
+ {attention.length === 0 ?

No issues match this filter.

: ( +
    + {attention.slice(0, 6).map((item, index) => ( +
  • + +
    +

    + {item.issue_ref ? {item.title} : item.link_type === "vehicle" ? {item.title} : item.title} +

    +

    {item.detail}

    +
    + {item.link_ref} + +
  • + ))} +
+ )} +
-
-

Today

- {data.today.length === 0 &&

No departures or returns scheduled today.

} -
    - {data.today.map((item) => ( -
  • - {item.kind === "departure" ? "Departure" : "Return"} - {item.booking_ref} - {item.vehicle_ref} - -
  • - ))} -
-
+
+ All bookings } /> + {data.today.length === 0 ?

No movements scheduled today.

: ( +
    + {data.today.slice(0, 6).map((item) => ( +
  1. + + +
    {item.kind}{item.booking_ref}{item.vehicle_ref}
    +
  2. + ))} +
+ )} +
+
-
-

Recent automation

- {data.recent_automation.length === 0 &&

No automation runs recorded yet.

} -
    - {data.recent_automation.map((run) => ( -
  • - - {run.event_type} - {run.aggregate_ref} - -
  • - ))} -
-
+
+
+ System detail } /> +
    +
  • n8n delivery{latestRun ? `Latest event ${latestRun.aggregate_ref}` : "No workflow evidence recorded"}
  • +
  • RAGcore knowledge{knowledge ? `${knowledge.document_count} procedures indexed` : "Health check unavailable"}
  • +
  • MCP HubNo active adapter in this PoC
  • +
+
+ +
+ + {data.recent_automation.length === 0 ?

No automation activity recorded.

: ( +
    + {data.recent_automation.slice(0, 4).map((run) => ( +
  • {run.event_type.replace(/_/g, " ")}{run.aggregate_ref}
  • + ))} +
+ )} +
+
); }