diff --git a/apps/web/README.md b/apps/web/README.md index 39de616..35ed52d 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -1,8 +1,9 @@ # Operations Workbench -Minimal Next.js app shell for the simulator-backed Process Sentinel demo. The -Workbench reads from the local FastAPI backend through a small typed API client -in `lib/api-client.ts`. +Next.js operator-console shell for the simulator-backed Process Sentinel demo. +The shell uses a Demo Factory-style steel, dark-nav, and teal-accent palette, +and the Workbench reads from the local FastAPI backend through a small typed +API client in `lib/api-client.ts`. ## Local Startup @@ -76,6 +77,12 @@ is running on a non-default port. ## Routes +The shell uses a persistent sidebar with an embedded status strip instead of a +horizontal top bar. Existing Sentinel demo routes remain available, and the +sidebar includes planned navigation slots for Connections, Protocol Diagnostics, +and Tag/Source Browser. Those planned slots do not add production writeback +controls. + - `/` - Factory overview dashboard with site, line, asset, work order, product, active detection count, pending recommendation count, and primary detection CTA - `/detections` - Process Sentinel detection list with summary, severity, diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 01c3dfb..ed47bb7 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -1,17 +1,29 @@ :root { - --background: #f6f7f4; + --background: #e8eef3; --surface: #ffffff; - --surface-muted: #eef2ed; - --text: #18201c; - --muted: #607066; - --border: #d7ded6; - --accent: #176b52; - --accent-strong: #0f4d3b; - --warning: #8b5a10; - --danger: #9f2d35; - --info: #315f9f; - --draft: #5d6470; - --shadow: 0 18px 50px rgb(24 32 28 / 8%); + --surface-muted: #f5f8fa; + --surface-subtle: #f0f5f8; + --text: #16202a; + --muted: #526579; + --muted-2: #6b7d8f; + --border: #d3dee7; + --border-strong: #b7c5d0; + --accent: #0f766e; + --accent-strong: #0b5d57; + --accent-bg: #d7f4eb; + --nav: #101820; + --nav-2: #1d2b36; + --nav-text: #d8e2ea; + --nav-muted: #91a4b3; + --warning: #b45309; + --warning-bg: #fff3d6; + --danger: #b91c1c; + --danger-bg: #fde2e2; + --info: #1e40af; + --info-bg: #dbeafe; + --draft: #475569; + --focus-ring: #9ee1d9; + --shadow: 0 1px 2px rgb(21 32 42 / 4%); } * { @@ -37,7 +49,9 @@ a { text-decoration: none; } -.shell { +.operator-shell { + display: grid; + grid-template-columns: 276px minmax(0, 1fr); min-height: 100vh; } @@ -57,22 +71,25 @@ a { .skip-link:focus-visible { transform: translateY(0); - outline: 3px solid #cfe7db; -} - -.site-header { - border-bottom: 1px solid var(--border); - background: rgb(255 255 255 / 92%); + outline: 3px solid var(--focus-ring); } -.header-inner { +.operator-sidebar { + position: sticky; + top: 0; display: flex; - align-items: center; - justify-content: space-between; + height: 100vh; + flex-direction: column; gap: 24px; - width: min(1180px, calc(100% - 32px)); - margin: 0 auto; - padding: 18px 0; + border-right: 1px solid #243541; + background: var(--nav); + color: var(--nav-text); + padding: 22px 18px; +} + +.sidebar-branding { + display: grid; + gap: 14px; } .brand { @@ -82,46 +99,125 @@ a { } .brand:focus-visible { - outline: 3px solid #cfe7db; + outline: 3px solid var(--focus-ring); outline-offset: 4px; } .brand-name { + color: #ffffff; font-size: 1rem; font-weight: 760; letter-spacing: 0; } .brand-context { - color: var(--muted); + color: var(--nav-muted); font-size: 0.82rem; font-weight: 560; } .primary-nav { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; + display: grid; + gap: 22px; +} + +.nav-group { + display: grid; + gap: 9px; +} + +.nav-group h2 { + margin: 0; + color: var(--nav-muted); + font-size: 0.72rem; + font-weight: 780; + text-transform: uppercase; +} + +.nav-group-links { + display: grid; gap: 6px; } .nav-link { + display: flex; + min-height: 42px; + align-items: center; + justify-content: space-between; + gap: 10px; border: 1px solid transparent; border-radius: 7px; - color: #35423a; + background: transparent; + color: #b7c4cf; font-size: 0.9rem; font-weight: 650; - padding: 9px 11px; + padding: 10px 11px; } .nav-link:hover, .nav-link:focus-visible { - border-color: var(--border); - background: var(--surface-muted); - outline: 3px solid #cfe7db; + border-color: #2d4352; + background: var(--nav-2); + color: #ffffff; + outline: 3px solid var(--focus-ring); outline-offset: 2px; } +.nav-link-disabled { + color: var(--nav-muted); + cursor: default; +} + +.nav-link-disabled:hover { + background: transparent; + color: var(--nav-muted); +} + +.nav-link-meta { + border: 1px solid #314756; + border-radius: 999px; + background: var(--nav-2); + color: var(--nav-text); + font-size: 0.68rem; + font-weight: 760; + line-height: 1; + padding: 5px 7px; + text-transform: uppercase; +} + +.operator-workspace { + min-width: 0; +} + +.status-strip { + display: grid; + gap: 8px; + margin-top: auto; + border-top: 1px solid #243541; + padding-top: 16px; +} + +.status-strip div { + display: grid; + min-width: 0; + gap: 4px; + border: 1px solid #243541; + border-radius: 7px; + background: var(--nav-2); + padding: 10px 11px; +} + +.status-strip .status-label { + color: var(--nav-muted); +} + +.status-strip strong { + color: #ffffff; + overflow-wrap: anywhere; + font-size: 0.9rem; + line-height: 1.25; +} + .page-shell { width: min(1180px, calc(100% - 32px)); margin: 0 auto; @@ -129,7 +225,7 @@ a { } .page-shell:focus-visible { - outline: 3px solid #cfe7db; + outline: 3px solid var(--focus-ring); outline-offset: 6px; } @@ -176,9 +272,9 @@ a { .demo-label { display: inline-flex; width: fit-content; - border: 1px solid #d9c18c; + border: 1px solid #f5d38d; border-radius: 6px; - background: #fff8e7; + background: var(--warning-bg); color: var(--warning); font-size: 0.78rem; font-weight: 760; @@ -190,9 +286,9 @@ a { display: inline-grid; width: fit-content; gap: 4px; - border: 1px solid #d9c18c; + border: 1px solid #f5d38d; border-radius: 7px; - background: #fff8e7; + background: var(--warning-bg); color: var(--warning); line-height: 1.2; padding: 7px 9px; @@ -204,7 +300,7 @@ a { } .demo-notice-copy { - color: #765011; + color: #7a4f0f; font-size: 0.72rem; font-weight: 650; } @@ -218,7 +314,7 @@ a { border: 1px solid var(--border); border-radius: 999px; background: var(--surface-muted); - color: #35423a; + color: #405263; font-size: 0.78rem; font-weight: 760; line-height: 1.05; @@ -233,26 +329,26 @@ a { } .status-badge-success { - border-color: #96c4ae; - background: #e8f5ee; + border-color: #9bd3ca; + background: var(--accent-bg); color: var(--accent-strong); } .status-badge-warning { - border-color: #d9c18c; - background: #fff8e7; + border-color: #f5d38d; + background: var(--warning-bg); color: var(--warning); } .status-badge-danger { - border-color: #d9a1a7; - background: #fff0f1; + border-color: #f2b6b6; + background: var(--danger-bg); color: var(--danger); } .status-badge-info { border-color: #a8bddc; - background: #eef4ff; + background: var(--info-bg); color: var(--info); } @@ -382,7 +478,7 @@ h3 { .primary-action:hover, .primary-action:focus-visible { background: var(--accent-strong); - outline: 3px solid #cfe7db; + outline: 3px solid var(--focus-ring); outline-offset: 2px; } @@ -393,8 +489,8 @@ h3 { .secondary-action:hover, .secondary-action:focus-visible { - background: #edf6f1; - outline: 3px solid #cfe7db; + background: #e7f3f1; + outline: 3px solid var(--focus-ring); outline-offset: 2px; } @@ -411,10 +507,10 @@ h3 { gap: 18px; align-items: start; margin-top: 18px; - border: 1px solid #b7d4c8; + border: 1px solid #b7ded8; border-left: 4px solid var(--accent); border-radius: 8px; - background: #f3faf6; + background: #e7f3f1; padding: 16px; } @@ -425,7 +521,7 @@ h3 { .api-connection-banner span, .api-connection-details dd { - color: #244337; + color: #21423e; line-height: 1.45; } @@ -440,12 +536,12 @@ h3 { display: grid; gap: 4px; min-width: 0; - border-left: 1px solid #b7d4c8; + border-left: 1px solid #b7ded8; padding-left: 10px; } .api-connection-details dt { - color: #4d665b; + color: var(--muted); font-size: 0.74rem; font-weight: 760; text-transform: uppercase; @@ -543,7 +639,7 @@ h3 { align-items: start; border: 1px solid var(--border); border-radius: 8px; - background: #fbfcfb; + background: var(--surface); padding: 18px; } @@ -592,7 +688,7 @@ h3 { .back-link:hover, .back-link:focus-visible { color: var(--accent-strong); - outline: 3px solid #cfe7db; + outline: 3px solid var(--focus-ring); outline-offset: 3px; } @@ -620,7 +716,7 @@ h3 { gap: 4px; border: 1px solid var(--border); border-radius: 8px; - background: #fbfcfb; + background: var(--surface); padding: 16px; } @@ -643,7 +739,7 @@ h3 { .state-panel { border: 1px solid var(--border); border-radius: 8px; - background: #fbfcfb; + background: var(--surface); } .data-card { @@ -737,12 +833,12 @@ h3 { .what-this-means { border-left: 4px solid var(--accent); - background: #edf6f1; + background: #e7f3f1; padding: 16px; } .what-this-means p { - color: #244337; + color: #21423e; } .timeline-list { @@ -767,7 +863,7 @@ h3 { border: 3px solid var(--accent); border-radius: 50%; background: var(--surface); - box-shadow: 0 0 0 4px #edf6f1; + box-shadow: 0 0 0 4px #e7f3f1; } .timeline-item article { @@ -804,7 +900,7 @@ h3 { border: 1px solid var(--border); border-radius: 6px; background: var(--surface-muted); - color: #35423a; + color: #405263; font-size: 0.74rem; font-weight: 760; line-height: 1; @@ -813,20 +909,20 @@ h3 { } .evidence-process-signal .evidence-type { - border-color: #9dc8ba; - background: #e7f4ee; - color: #0f4d3b; + border-color: #9bd3ca; + background: var(--accent-bg); + color: var(--accent-strong); } .evidence-quality-result .evidence-type { - border-color: #d6ba76; - background: #fff8e7; + border-color: #f5d38d; + background: var(--warning-bg); color: #7a4f0f; } .evidence-correlation-window .evidence-type { border-color: #b9c1dc; - background: #eef1fb; + background: var(--info-bg); color: #35436f; } @@ -911,17 +1007,17 @@ h3 { } .error-panel { - border-color: #e3b7b7; - background: #fff2f2; + border-color: #f2b6b6; + background: var(--danger-bg); } .error-panel strong { - color: #8a2b2b; + color: var(--danger); } .missing-data-panel { - border-color: #d9c18c; - background: #fffaf0; + border-color: #f5d38d; + background: var(--warning-bg); } .missing-data-panel strong { @@ -930,8 +1026,8 @@ h3 { .decision-note { border-left: 4px solid var(--accent); - background: #edf6f1; - color: #244337; + background: #e7f3f1; + color: #21423e; line-height: 1.5; padding: 16px; } @@ -979,7 +1075,7 @@ h3 { .review-form input:focus, .review-form textarea:focus { border-color: var(--accent); - outline: 3px solid #cfe7db; + outline: 3px solid var(--focus-ring); outline-offset: 1px; } @@ -1002,13 +1098,13 @@ h3 { } .review-actions button:nth-child(2) { - border-color: #8a2b2b; - background: #8a2b2b; + border-color: var(--danger); + background: var(--danger); } .review-actions button:nth-child(3) { border-color: #7a4f0f; - background: #8b5a10; + background: var(--warning); } .review-actions button:disabled { @@ -1017,7 +1113,7 @@ h3 { } .review-actions button:focus-visible { - outline: 3px solid #cfe7db; + outline: 3px solid var(--focus-ring); outline-offset: 2px; } @@ -1031,7 +1127,7 @@ h3 { .draft-copy button:focus-visible, .review-actions button:focus-visible { - outline: 3px solid #cfe7db; + outline: 3px solid var(--focus-ring); outline-offset: 2px; } @@ -1039,27 +1135,27 @@ h3 { display: grid; gap: 5px; border-left: 4px solid var(--accent); - background: #edf6f1; - color: #244337; + background: #e7f3f1; + color: #21423e; line-height: 1.5; padding: 16px; } .api-panel { border-left: 4px solid var(--accent); - background: #edf6f1; + background: #e7f3f1; padding: 16px; } .api-panel p { margin-bottom: 0; - color: #244337; + color: #21423e; line-height: 1.5; } code { border-radius: 5px; - background: #e4ebe5; + background: #e8eef4; color: var(--accent-strong); font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; font-size: 0.9em; @@ -1067,18 +1163,24 @@ code { } @media (max-width: 880px) { - .header-inner, - .hero { - grid-template-columns: 1fr; + .operator-shell { + display: block; } - .header-inner { - align-items: flex-start; - flex-direction: column; + .operator-sidebar { + position: relative; + height: auto; + border-right: 0; + border-bottom: 1px solid var(--border); } - .primary-nav { - justify-content: flex-start; + .status-strip { + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin-top: 0; + } + + .hero { + grid-template-columns: 1fr; } .api-connection-banner { @@ -1121,8 +1223,7 @@ code { } @media (min-width: 881px) and (max-width: 1240px) { - .page-shell, - .header-inner { + .page-shell { width: min(1100px, calc(100% - 28px)); } @@ -1140,6 +1241,14 @@ code { } @media (max-width: 560px) { + .operator-sidebar { + padding: 18px 16px; + } + + .status-strip { + grid-template-columns: 1fr; + } + .content-grid { grid-template-columns: 1fr; } diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 653cb5e..452af3f 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import type { ReactNode } from "react"; import { DemoDataBadge } from "./components/demo-state"; +import { getApiBaseUrl } from "../lib/api-client"; import "./globals.css"; export const metadata: Metadata = { @@ -10,40 +11,92 @@ export const metadata: Metadata = { description: "Simulator-backed Factory Intelligence Platform workbench shell.", }; -const navItems = [ - { href: "/", label: "Overview" }, - { href: "/detections", label: "Detections" }, - { href: "/recommendations", label: "Recommendations" }, - { href: "/rca-capa-draft", label: "RCA/CAPA Draft" }, +const navGroups = [ + { + items: [ + { href: "/", label: "Overview" }, + { href: "/detections", label: "Detections" }, + { href: "/recommendations", label: "Recommendations" }, + { href: "/rca-capa-draft", label: "RCA/CAPA Draft" }, + ], + label: "Sentinel workflows", + }, + { + items: [ + { label: "Connections", status: "Planned" }, + { label: "Protocol Diagnostics", status: "Planned" }, + { label: "Tag/Source Browser", status: "Planned" }, + ], + label: "Protocol operations", + }, ]; export default function RootLayout({ children }: { children: ReactNode }) { return (
-