diff --git a/apps/tracking-console/src/app/layout/app-shell.tsx b/apps/tracking-console/src/app/layout/app-shell.tsx index 1ae22dc..bb1b958 100644 --- a/apps/tracking-console/src/app/layout/app-shell.tsx +++ b/apps/tracking-console/src/app/layout/app-shell.tsx @@ -3,11 +3,9 @@ import { NavLink, Outlet } from 'react-router-dom'; import { getAdminApiToken, setAdminApiToken } from '../../shared/lib/admin-api-token'; const links = [ - { to: '/', label: 'Overview' }, + { to: '/dashboard', label: 'Dashboard' }, { to: '/events', label: 'Events' }, - { to: '/debugger', label: 'Event Debugger' }, - { to: '/flow', label: 'Flow Explorer' }, - { to: '/phase-7', label: 'Phase 7 Cutover' } + { to: '/debugger', label: 'Debugger' } ]; export function AppShell() { @@ -19,65 +17,56 @@ export function AppShell() { return (
-
-
+
-
-
-

Admin Access

-

- Saved locally in this browser and attached only to /admin/* requests. Leave it empty when - the API is intentionally open in development. -

-
- -
-

{adminToken.trim() ? 'Token saved in localStorage.' : 'No token saved.'}

-
+ + +
+
+

Admin Access

+

Browser-local token.

+
+ +
+

{adminToken.trim() ? 'Token stored locally.' : 'Token not set.'}

-
-
- - + + -
+
+
+

System Workspace

+

Health, events, debugger, flow.

+
diff --git a/apps/tracking-console/src/app/router.tsx b/apps/tracking-console/src/app/router.tsx index 7247698..0bf0a0f 100644 --- a/apps/tracking-console/src/app/router.tsx +++ b/apps/tracking-console/src/app/router.tsx @@ -1,9 +1,9 @@ -import { createBrowserRouter } from 'react-router-dom'; +import { createBrowserRouter, Navigate } from 'react-router-dom'; import { AppShell } from './layout/app-shell'; +import { DashboardPage } from '../pages/dashboard-page'; import { DebuggerPage } from '../pages/debugger-page'; import { EventsPage } from '../pages/events-page'; import { FlowPage } from '../pages/flow-page'; -import { OverviewPage } from '../pages/overview-page'; import { Phase7Page } from '../pages/phase7-page'; export const appRouter = createBrowserRouter([ @@ -11,7 +11,8 @@ export const appRouter = createBrowserRouter([ path: '/', element: , children: [ - { index: true, element: }, + { index: true, element: }, + { path: 'dashboard', element: }, { path: 'events', element: }, { path: 'events/:eventId', element: }, { path: 'debugger', element: }, diff --git a/apps/tracking-console/src/pages/dashboard-page.tsx b/apps/tracking-console/src/pages/dashboard-page.tsx new file mode 100644 index 0000000..062ea10 --- /dev/null +++ b/apps/tracking-console/src/pages/dashboard-page.tsx @@ -0,0 +1,282 @@ +import { useQuery } from '@tanstack/react-query'; +import { Link } from 'react-router-dom'; +import type { AdminEventItem } from '../entities/tracking/model/admin-events'; +import { trackingApi } from '../shared/api/tracking-api'; +import { formatNumber } from '../shared/lib/format'; + +function formatTimestamp(value: string): string { + const date = new Date(value); + if (Number.isNaN(date.getTime())) { + return value; + } + + return date.toLocaleString(); +} + +function formatHealthState(status: string | undefined): string { + if (!status) { + return 'Unreachable'; + } + + return status.replace(/_/g, ' '); +} + +function getHeartbeatTone(status: string | undefined): 'ok' | 'warn' { + return status === 'ok' || status === 'ready' ? 'ok' : 'warn'; +} + +function getDeliveryMix(items: AdminEventItem[]) { + return items.reduce( + (summary, item) => { + summary[item.deliveryOverallStatus] += 1; + return summary; + }, + { success: 0, failed: 0, queued: 0 } + ); +} + +function getTopSources(items: AdminEventItem[]) { + const counts = new Map(); + for (const item of items) { + const key = item.source?.trim() || 'unknown'; + counts.set(key, (counts.get(key) ?? 0) + 1); + } + + return [...counts.entries()] + .sort((left, right) => right[1] - left[1]) + .slice(0, 4); +} + +export function DashboardPage() { + const healthQuery = useQuery({ queryKey: ['health'], queryFn: trackingApi.health, retry: 1, refetchInterval: 15_000 }); + const readyQuery = useQuery({ queryKey: ['ready'], queryFn: trackingApi.ready, retry: 1, refetchInterval: 15_000 }); + const metricsQuery = useQuery({ queryKey: ['metrics'], queryFn: trackingApi.metrics, retry: 1, refetchInterval: 15_000 }); + const eventsQuery = useQuery({ + queryKey: ['dashboard-recent-events'], + queryFn: () => trackingApi.listRecentEvents({ limit: 8, offset: 0, sortBy: 'createdAt', sortDirection: 'desc' }), + refetchInterval: 15_000 + }); + + const counters = metricsQuery.data?.counters; + const recentEvents = eventsQuery.data?.items ?? []; + const deliveryMix = getDeliveryMix(recentEvents); + const topSources = getTopSources(recentEvents); + const totalRequests = counters?.track_requests_total ?? 0; + const acceptedRequests = counters?.track_success_total ?? 0; + const deduplicatedRequests = counters?.track_deduplicated_total ?? 0; + const authFailures = counters?.track_auth_failed_total ?? 0; + const rateLimited = counters?.track_rate_limited_total ?? 0; + const validationErrors = counters?.track_validation_error_total ?? 0; + const internalErrors = counters?.track_internal_error_total ?? 0; + const acceptedRate = totalRequests > 0 ? `${Math.round((acceptedRequests / totalRequests) * 100)}%` : '-'; + const heartbeatTone = getHeartbeatTone(healthQuery.data?.status); + + return ( +
+
+
+

Dashboard

+

Tracking system

+

Health, traffic, replay.

+
+
+ +
+
+
Health
+
{formatHealthState(healthQuery.data?.status)}
+
+
+
Readiness
+
{formatHealthState(readyQuery.data?.status)}
+
+
+
Heartbeat
+
{heartbeatTone === 'ok' ? 'Live' : 'Down'}
+
+
+
+
+ +
+
+ Accepted + {formatNumber(acceptedRequests)} + Live +
+
+ Rate + {acceptedRate} + {formatNumber(totalRequests)} total +
+
+ Dedup + {formatNumber(deduplicatedRequests)} + `event_id` matched +
+
+ Guardrails + {formatNumber(authFailures + rateLimited)} + Auth + rate limit +
+
+ +
+
+
+
+
+

Pipeline Status

+

Pipeline

+
+ + Events + +
+
+
+ 00 +

Heartbeat

+

App health

+
+ + {heartbeatTone === 'ok' ? 'Healthy' : 'Unavailable'} +
+
+
+ 01 +

Ingress

+

API probes

+
+
+
Health
+
{formatHealthState(healthQuery.data?.status)}
+
+
+
Readiness
+
{formatHealthState(readyQuery.data?.status)}
+
+
+
+
+ 02 +

Validation

+

Payload checks

+
+
+
Validation errors
+
{formatNumber(validationErrors)}
+
+
+
Internal errors
+
{formatNumber(internalErrors)}
+
+
+
+
+ 03 +

Delivery

+

Recent outcomes

+
+
+
Success
+
{formatNumber(deliveryMix.success)}
+
+
+
Queued / failed
+
+ {formatNumber(deliveryMix.queued)} / {formatNumber(deliveryMix.failed)} +
+
+
+
+
+
+ +
+
+
+

Live Event Feed

+

Latest events

+
+ + Debugger + +
+ {eventsQuery.isLoading ?

Loading…

: null} + {!eventsQuery.isLoading && recentEvents.length === 0 ? ( +

No data.

+ ) : null} + {recentEvents.length > 0 ? ( +
+ {recentEvents.map((item) => ( + +
+ {item.eventName} + {item.eventId} +
+
+ {item.source ?? 'unknown source'} + {item.deliveryOverallStatus} + +
+ + ))} +
+ ) : null} +
+
+ + +
+
+ ); +} diff --git a/apps/tracking-console/src/pages/overview-page.tsx b/apps/tracking-console/src/pages/overview-page.tsx index 722f9b0..99730b5 100644 --- a/apps/tracking-console/src/pages/overview-page.tsx +++ b/apps/tracking-console/src/pages/overview-page.tsx @@ -1,22 +1,5 @@ -import { SystemHealthPanel } from '../widgets/system-health/ui/system-health-panel'; -import { Phase7Board } from '../widgets/phase7/ui/phase7-board'; -import { Card } from '../shared/ui/card'; +import { DashboardPage } from './dashboard-page'; export function OverviewPage() { - return ( -
- -
    -
  • Observe runtime status of tracking-api and readiness probes.
  • -
  • Debug event contract quickly by posting test payloads.
  • -
  • Track Phase 7 cutover gate status in one place for go/no-go decisions.
  • -
-
- - -
- ); + return ; } diff --git a/apps/tracking-console/src/styles.css b/apps/tracking-console/src/styles.css index 1746b06..be963ae 100644 --- a/apps/tracking-console/src/styles.css +++ b/apps/tracking-console/src/styles.css @@ -1,16 +1,22 @@ -@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=IBM+Plex+Mono:wght@400;600&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=IBM+Plex+Mono:wght@400;500;600&display=swap'); :root { - --bg: #f3f0e8; - --bg-panel: #fffdf7; - --ink: #18120f; - --muted: #5f564e; - --line: #d8ccba; - --brand: #0b7a75; - --brand-soft: #d8f1ef; - --danger: #b63d35; - --warning: #9b6a13; - --ok: #1e8f4d; + --bg: #f4f7fb; + --bg-soft: #eef3f9; + --bg-panel: rgba(255, 255, 255, 0.84); + --bg-panel-strong: #ffffff; + --ink: #102033; + --muted: #63758a; + --muted-2: #8192a5; + --line: rgba(25, 55, 86, 0.1); + --line-strong: rgba(25, 55, 86, 0.18); + --brand: #0d9488; + --brand-strong: #0f766e; + --danger: #d1435b; + --warning: #b7791f; + --ok: #1f9d61; + --shadow-lg: 0 24px 64px rgba(31, 50, 81, 0.12); + --shadow-md: 0 14px 34px rgba(31, 50, 81, 0.08); } * { @@ -21,446 +27,692 @@ body { margin: 0; min-height: 100vh; background: - radial-gradient(circle at 6% 0%, #fff5b9 0%, transparent 22%), - radial-gradient(circle at 100% 10%, #cfeef2 0%, transparent 25%), - var(--bg); + radial-gradient(circle at top left, rgba(13, 148, 136, 0.08), transparent 24%), + radial-gradient(circle at top right, rgba(59, 130, 246, 0.08), transparent 28%), + linear-gradient(180deg, #f8fbff 0%, #f4f7fb 100%); color: var(--ink); - font-family: 'Space Grotesk', sans-serif; + font-family: 'Manrope', sans-serif; +} + +a { + color: inherit; +} + +button, +input, +select { + font: inherit; } h1, h2, h3, -p { +p, +dl, +dd, +dt { margin: 0; } -.app-shell { - max-width: 1120px; - margin: 0 auto; - padding: 24px 18px 40px; +code, +pre, +.event-link span, +.feed-row-main span { + font-family: 'IBM Plex Mono', monospace; } -.topbar { +.app-shell { display: grid; - gap: 8px; - padding: 18px; - border: 1px solid #22314d; - border-radius: 16px; - background: linear-gradient(125deg, #101a2d 0%, #1f3356 100%); - color: #edf5ff; + grid-template-columns: 280px minmax(0, 1fr); + min-height: 100vh; } -.eyebrow { - font-size: 0.74rem; - letter-spacing: 0.07em; - text-transform: uppercase; - color: #9fc3ff; +.app-sidebar { + position: sticky; + top: 0; + display: grid; + align-content: start; + gap: 24px; + height: 100vh; + padding: 24px 20px; + border-right: 1px solid var(--line); + background: rgba(249, 251, 255, 0.82); + backdrop-filter: blur(16px); +} + +.brand-block, +.app-main-head, +.page-grid, +.detail-stack, +.events-grid, +.dashboard-shell, +.dashboard-hero-copy, +.dashboard-hero-side, +.surface-head, +.pipeline-grid, +.workspace-grid, +.feed-list, +.action-stack, +.source-list, +.guardrail-list, +.hero-summary-list, +.status-list, +.flow-wrap, +.form-grid, +.events-filter-form, +.events-filter-grid, +.table-skeleton, +.detail-skeleton, +.timeline-list, +.replay-block, +.replay-inline, +.code-block-wrap, +.detail-section-head, +.detail-meta-grid { + display: grid; } -.topbar h1 { - font-size: clamp(1.25rem, 2.2vw, 2rem); +.brand-block { + gap: 10px; } -.topbar-note { - color: #d9e9ff; - font-size: 0.95rem; +.eyebrow, +.section-kicker, +.detail-label { + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--brand-strong); +} + +.brand-block h1 { + font-size: clamp(1.5rem, 2vw, 2rem); + line-height: 1; + letter-spacing: -0.05em; +} + +.sidebar-note, +.topbar-note, +.card-head p, +.detail-section-head p, +.muted-small, +.dashboard-lead, +.pipeline-stage p, +.hero-summary-list dt, +.token-panel-head p, +.dashboard-empty, +.metric-tile span, +.metric-tile small, +.pipeline-stage dt, +.guardrail-list span, +.source-row span, +.kpi-card span { + color: var(--muted); } -.nav-grid { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin: 14px 0 18px; +.sidebar-nav { + display: grid; + gap: 8px; } .nav-chip { + display: flex; + align-items: center; + min-height: 46px; + padding: 0 14px; + border: 1px solid transparent; + border-radius: 14px; text-decoration: none; - color: #234; - border: 1px solid var(--line); - background: #f5ebdb; - padding: 8px 12px; - border-radius: 999px; - font-weight: 600; + color: #34465a; + transition: + transform 180ms ease, + border-color 180ms ease, + background-color 180ms ease, + color 180ms ease; } -.nav-chip.is-active { - background: var(--brand); - color: #fff; - border-color: var(--brand); +.nav-chip:hover { + transform: translateX(3px); + border-color: var(--line); + background: rgba(13, 148, 136, 0.06); } -.page-grid { - display: grid; - gap: 14px; +.nav-chip.is-active { + border-color: rgba(13, 148, 136, 0.2); + background: rgba(13, 148, 136, 0.1); + color: var(--brand-strong); } -.card { +.token-panel, +.card, +.surface-block { border: 1px solid var(--line); - border-radius: 14px; + border-radius: 22px; background: var(--bg-panel); - padding: 16px; + box-shadow: var(--shadow-md); + backdrop-filter: blur(16px); } -.card-head { - margin-bottom: 12px; +.token-panel { + gap: 14px; + padding: 16px; } -.card-head h2 { - font-size: 1.08rem; +.token-panel-head, +.app-main-head { + gap: 6px; } -.card-head p { - margin-top: 6px; - color: var(--muted); - font-size: 0.95rem; - line-height: 1.45; +.token-panel-head h2, +.card-head h2, +.surface-head h2 { + font-size: 1rem; + letter-spacing: -0.03em; } -.plain-list { - margin: 0; - padding-left: 18px; +.token-field { display: grid; gap: 8px; - line-height: 1.45; } -.ordered { - list-style: decimal; -} - -.kpi-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); - gap: 10px; +.token-field span, +.form-grid label, +.events-filter-grid label, +.filters-row label { + color: var(--ink); + font-size: 0.9rem; } -.kpi-card { - border: 1px solid #d7cdbf; +.token-field input, +.form-grid input, +.events-filter-grid input, +.events-filter-grid select, +.filters-row input, +.filters-row select { + height: 42px; border-radius: 12px; + border: 1px solid var(--line); background: #fff; - padding: 10px; + color: var(--ink); + padding: 0 12px; } -.kpi-card span { - display: block; - color: var(--muted); - font-size: 0.86rem; +.token-field input::placeholder, +.form-grid input::placeholder, +.events-filter-grid input::placeholder { + color: var(--muted-2); } -.kpi-card strong { - font-size: 1.1rem; +.token-panel-foot, +.toolbar, +.toolbar-actions, +.detail-actions, +.timeline-item-head, +.event-row-head, +.pipeline-stage dl div, +.guardrail-list div, +.source-row, +.feed-row-meta { + display: flex; + align-items: center; + gap: 10px; } -.status-list { - display: grid; - gap: 8px; - margin: 0; - padding: 0; +.token-panel-foot, +.toolbar, +.timeline-item-head, +.event-row-head, +.pipeline-stage dl div, +.guardrail-list div, +.source-row, +.feed-row-meta { + justify-content: space-between; } -.status-item { - list-style: none; - border: 1px solid var(--line); - border-radius: 10px; - padding: 9px 11px; +.app-main { display: grid; - gap: 4px; + align-content: start; + gap: 18px; + padding: 24px; +} + +.page-grid, +.detail-stack, +.events-grid, +.dashboard-shell, +.workspace-main, +.workspace-side, +.feed-list, +.action-stack, +.source-list, +.guardrail-list, +.status-list, +.flow-wrap, +.events-filter-form, +.timeline-list, +.replay-block, +.code-block-wrap, +.detail-section-head { + gap: 14px; } -.status-item.done { - border-left: 6px solid var(--ok); - background: #edf9f2; +.workspace-main > .surface-block + .surface-block { + margin-top: 14px; } -.status-item.todo { - border-left: 6px solid var(--warning); - background: #fff8e8; +.workspace-side > .surface-block + .surface-block { + margin-top: 14px; } -.flow-wrap { +.dashboard-hero { display: grid; - gap: 8px; + grid-template-columns: minmax(0, 1.4fr) minmax(260px, 0.7fr); + gap: 18px; + padding: 20px 22px; + border: 1px solid var(--line); + border-radius: 28px; + background: + radial-gradient(circle at top left, rgba(13, 148, 136, 0.1), transparent 34%), + radial-gradient(circle at bottom right, rgba(59, 130, 246, 0.08), transparent 36%), + linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(248, 251, 255, 0.92)); + box-shadow: var(--shadow-lg); } -.flow-node { - border: 1px solid #dbc9b0; - border-radius: 12px; - padding: 10px; - background: #fff; +.dashboard-hero-copy { + gap: 12px; } -.flow-node p { - margin-top: 4px; - color: var(--muted); +.dashboard-hero-copy h1 { + font-size: clamp(1.9rem, 3vw, 3rem); + line-height: 0.98; + letter-spacing: -0.07em; + max-width: 9ch; } -.form-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 10px; +.dashboard-lead { + font-size: 0.95rem; +} + +.dashboard-hero-side { + align-content: center; + gap: 12px; + justify-items: end; } -.form-grid label { +.signal-cluster { + position: relative; + width: 132px; + height: 132px; display: grid; - gap: 6px; - font-weight: 500; - color: #2f2622; + place-items: center; + isolation: isolate; } -.form-grid input { - height: 38px; - border-radius: 10px; - border: 1px solid #ccbca6; - padding: 0 10px; - font: inherit; - background: #fff; +.signal-core, +.signal-beam { + position: absolute; + border-radius: 999px; } -.primary-btn { - align-self: end; - height: 38px; - border: 0; - border-radius: 10px; +.signal-core { + width: 14px; + height: 14px; background: var(--brand); - color: #fff; - font: inherit; - font-weight: 600; - cursor: pointer; + box-shadow: 0 0 0 10px rgba(13, 148, 136, 0.08); + animation: heartbeat-core 1.35s cubic-bezier(0.4, 0, 0.2, 1) infinite; } -.primary-btn:disabled { - opacity: 0.65; - cursor: wait; +.signal-beam { + border: 1px solid rgba(13, 148, 136, 0.16); + animation: heartbeat-ring 1.35s cubic-bezier(0.2, 0.7, 0.2, 1) infinite; } -.code-block-wrap { - margin-top: 12px; +.signal-cluster.is-warn .signal-core { + background: var(--warning); + box-shadow: 0 0 0 10px rgba(183, 121, 31, 0.1); } -.code-block-wrap h3 { - font-size: 0.95rem; - margin-bottom: 6px; +.signal-cluster.is-warn .signal-beam { + border-color: rgba(183, 121, 31, 0.2); } -.code-block-wrap pre { - margin: 0; - max-height: 280px; - overflow: auto; - padding: 10px; - border-radius: 10px; - border: 1px solid #cdbca6; - background: #1d1a16; - color: #d2f7dc; - font-family: 'IBM Plex Mono', monospace; - font-size: 0.83rem; - line-height: 1.45; +.signal-beam-a { + width: 132px; + height: 132px; } -.hint { - margin-top: 10px; - font-weight: 500; +.signal-beam-b { + width: 84px; + height: 84px; + animation-delay: 0.2s; } -.hint.warn { - color: var(--warning); +.hero-summary-list { + gap: 8px; + width: min(100%, 240px); } -.hint.danger { - color: var(--danger); +.hero-summary-list div { + display: flex; + justify-content: space-between; + gap: 14px; + padding-top: 10px; + border-top: 1px solid var(--line); } -.hint.success { - color: var(--ok); +.hero-summary-list dd, +.metric-tile strong, +.pipeline-stage dd, +.guardrail-list strong, +.source-row strong, +.kpi-card strong { + color: var(--ink); + font-weight: 700; } -.toolbar { - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; - margin-bottom: 10px; +.metric-rail { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; } -.toolbar-actions { - display: flex; - gap: 8px; +.compact-rail { + grid-template-columns: repeat(4, minmax(140px, 1fr)); } -.ghost-btn { - height: 34px; - padding: 0 10px; - border-radius: 8px; - border: 1px solid #c8baa8; - background: #fff; - font: inherit; - cursor: pointer; +.metric-tile { + display: grid; + gap: 6px; + padding: 14px; + border: 1px solid var(--line); + border-radius: 18px; + background: rgba(255, 255, 255, 0.72); + box-shadow: var(--shadow-md); } -.ghost-btn:disabled { - opacity: 0.45; - cursor: not-allowed; +.metric-tile strong { + font-size: clamp(1.4rem, 2vw, 2rem); + letter-spacing: -0.05em; } -.muted-small { - color: var(--muted); - font-size: 0.9rem; +.workspace-grid { + grid-template-columns: minmax(0, 1.5fr) minmax(260px, 0.72fr); + gap: 16px; + align-items: start; } -.events-grid { - display: grid; - gap: 10px; +.surface-block, +.card { + padding: 16px; } -.events-workbench { - display: grid; - grid-template-columns: minmax(0, 1.3fr) minmax(320px, 0.9fr); - gap: 14px; - align-items: start; +.surface-head, +.card-head { + gap: 8px; + margin-bottom: 16px; +} + +.surface-head { + grid-template-columns: minmax(0, 1fr) auto; + align-items: end; } -.event-row { - border: 1px solid #d8cbb8; +.text-action { + align-self: start; + padding: 9px 12px; border-radius: 12px; - background: #fff; - padding: 10px; + background: rgba(13, 148, 136, 0.08); + color: var(--brand-strong); + text-decoration: none; } -.event-row-head { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 8px; +.pipeline-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; } -.event-meta { - margin-top: 8px; - display: flex; - flex-wrap: wrap; - gap: 8px; - color: var(--muted); - font-size: 0.9rem; +.pipeline-stage, +.kpi-card, +.flow-node, +.detail-meta-grid > div, +.detail-section, +.status-item, +.timeline-item, +.event-row, +.events-table-wrap, +.source-row, +.guardrail-list div, +.feed-row { + border: 1px solid var(--line); + border-radius: 18px; + background: var(--bg-panel-strong); } -.delivery-list { - margin-top: 10px; - display: flex; - flex-wrap: wrap; - gap: 6px; +.pipeline-stage, +.kpi-card, +.flow-node, +.status-item, +.timeline-item, +.feed-row, +.detail-meta-grid > div, +.detail-section { + padding: 12px; +} + +.pipeline-stage { + gap: 10px; +} + +.pipeline-index { + color: var(--brand-strong); + font-size: 0.74rem; + letter-spacing: 0.16em; +} + +.pipeline-stage h3 { + font-size: 1rem; } -.status-pill { +.heartbeat-stage { + align-content: start; +} + +.heartbeat-pill { display: inline-flex; align-items: center; + gap: 8px; + width: fit-content; + padding: 8px 10px; border-radius: 999px; - padding: 3px 9px; - border: 1px solid; - font-size: 0.82rem; - font-weight: 600; + background: rgba(13, 148, 136, 0.08); + color: var(--brand-strong); +} + +.heartbeat-pill.is-warn { + background: rgba(183, 121, 31, 0.1); + color: #9a6418; +} + +.heartbeat-dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: currentColor; + animation: heartbeat-dot 1.35s cubic-bezier(0.4, 0, 0.2, 1) infinite; +} + +.pipeline-stage dl, +.guardrail-list, +.source-list, +.detail-meta-grid, +.events-filter-grid, +.form-grid { + gap: 10px; } -.status-pill.success { - color: #0b6f36; - background: #e8f8ee; - border-color: #9fddbb; +.feed-row { + text-decoration: none; + transition: + transform 180ms ease, + border-color 180ms ease, + box-shadow 180ms ease; } -.status-pill.failed { - color: #972e2e; - background: #ffeded; - border-color: #efb1b1; +.feed-row:hover, +.text-action:hover, +.primary-btn:hover, +.ghost-btn:hover, +.replay-btn:hover { + transform: translateY(-1px); } -.status-pill.queued { - color: #8a5e08; - background: #fff7e3; - border-color: #e8cd91; +.feed-row:hover { + border-color: var(--line-strong); + box-shadow: var(--shadow-md); } -.filters-row { +.feed-row { display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 10px; - margin-bottom: 10px; - align-items: end; + grid-template-columns: minmax(0, 1fr) auto; + gap: 16px; + align-items: center; } -.filters-row label { +.feed-row-main { display: grid; - gap: 6px; + gap: 4px; +} + +.feed-row-main span, +.event-link span { color: var(--muted); - font-size: 0.9rem; + font-size: 0.8rem; } -.filters-row input, -.filters-row select { - height: 36px; - border: 1px solid #c8baa8; - border-radius: 8px; - padding: 0 10px; - font: inherit; - background: #fff; +.dashboard-empty { + padding: 12px 0 0; } -.status-summary-grid { - display: flex; - flex-wrap: wrap; +.plain-list { + margin: 0; + padding-left: 18px; + display: grid; gap: 8px; - margin-bottom: 12px; + line-height: 1.55; } -.summary-chip { - border-radius: 999px; - padding: 4px 10px; - border: 1px solid; - font-size: 0.85rem; - font-weight: 600; +.ordered { + list-style: decimal; } -.summary-chip.success { - color: #0b6f36; - background: #e8f8ee; - border-color: #9fddbb; +.kpi-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; } -.summary-chip.failed { - color: #972e2e; - background: #ffeded; - border-color: #efb1b1; +.kpi-card strong { + display: block; + margin-top: 8px; + font-size: 1.15rem; } -.summary-chip.queued { - color: #8a5e08; - background: #fff7e3; - border-color: #e8cd91; +.status-list { + margin: 0; + padding: 0; } -.events-filter-form { - display: grid; - gap: 12px; +.status-item { + list-style: none; } -.events-filter-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); - gap: 10px; +.status-item.done { + box-shadow: inset 3px 0 0 var(--ok); } -.events-filter-grid label { - display: grid; - gap: 6px; - color: var(--muted); - font-size: 0.9rem; +.status-item.todo { + box-shadow: inset 3px 0 0 var(--warning); } -.events-filter-grid input, -.events-filter-grid select { - height: 36px; - border: 1px solid #c8baa8; - border-radius: 8px; - padding: 0 10px; - font: inherit; +.flow-node p { + margin-top: 6px; +} + +.form-grid { + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); +} + +.primary-btn, +.ghost-btn, +.replay-btn { + height: 40px; + border-radius: 12px; + padding: 0 14px; + border: 1px solid transparent; + cursor: pointer; + transition: + transform 180ms ease, + border-color 180ms ease, + background-color 180ms ease, + box-shadow 180ms ease, + opacity 180ms ease; +} + +.primary-btn { + align-self: end; + background: var(--brand); + color: #fff; + font-weight: 700; +} + +.primary-btn:disabled { + opacity: 0.6; + cursor: wait; +} + +.ghost-btn, +.replay-btn { + border-color: var(--line); background: #fff; + color: var(--ink); +} + +.ghost-btn:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.hint { + margin-top: 10px; + font-weight: 500; +} + +.hint.warn { + color: var(--warning); +} + +.hint.danger, +.timeline-error { + color: var(--danger); +} + +.hint.success { + color: var(--ok); +} + +.muted-small { + font-size: 0.9rem; +} + +.events-workbench { + display: grid; + grid-template-columns: minmax(0, 1.34fr) minmax(320px, 0.9fr); + gap: 16px; + align-items: start; +} + +.events-filter-grid { + grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); } .event-toolbar { @@ -469,21 +721,18 @@ p { .events-table-wrap { overflow: auto; - border: 1px solid #d8cbb8; - border-radius: 12px; - background: #fff; } .events-table { width: 100%; - border-collapse: collapse; min-width: 860px; + border-collapse: collapse; } .events-table th, .events-table td { - padding: 12px 10px; - border-bottom: 1px solid #efe5d4; + padding: 14px 12px; + border-bottom: 1px solid rgba(25, 55, 86, 0.08); text-align: left; vertical-align: top; } @@ -491,19 +740,19 @@ p { .events-table thead th { position: sticky; top: 0; - background: #f8f3ea; - color: #5b4d44; - font-size: 0.82rem; - letter-spacing: 0.04em; + background: #f7fafd; + color: #71859a; + font-size: 0.76rem; + letter-spacing: 0.12em; text-transform: uppercase; } .events-table tbody tr:hover { - background: #fff8ec; + background: rgba(13, 148, 136, 0.04); } .events-table tbody tr.is-selected { - background: #eef8f5; + background: rgba(13, 148, 136, 0.08); } .event-link { @@ -513,29 +762,42 @@ p { text-decoration: none; } -.event-link span { - color: var(--muted); - font-size: 0.83rem; - font-family: 'IBM Plex Mono', monospace; +.status-pill, +.summary-chip { + display: inline-flex; + align-items: center; + width: fit-content; + border-radius: 999px; + padding: 4px 10px; + border: 1px solid; + font-size: 0.78rem; + font-weight: 700; + text-transform: capitalize; } -.detail-stack { - display: grid; - gap: 14px; +.status-pill.success, +.summary-chip.success { + color: #0f7a4b; + background: rgba(31, 157, 97, 0.1); + border-color: rgba(31, 157, 97, 0.18); } -.detail-meta-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; +.status-pill.failed, +.summary-chip.failed { + color: #b8324a; + background: rgba(209, 67, 91, 0.08); + border-color: rgba(209, 67, 91, 0.16); } -.detail-meta-grid > div, -.detail-section { - border: 1px solid #d8cbb8; - border-radius: 12px; - background: #fff; - padding: 12px; +.status-pill.queued, +.summary-chip.queued { + color: #9a6418; + background: rgba(183, 121, 31, 0.08); + border-color: rgba(183, 121, 31, 0.16); +} + +.detail-meta-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); } .detail-meta-grid strong, @@ -544,71 +806,30 @@ p { } .detail-label { - color: var(--muted); - font-size: 0.78rem; - text-transform: uppercase; - letter-spacing: 0.06em; margin-bottom: 4px; } -.detail-actions { - display: flex; - flex-wrap: wrap; - gap: 10px; - align-items: center; -} - -.detail-section-head { - display: grid; - gap: 4px; - margin-bottom: 10px; -} - -.detail-section-head h3 { - font-size: 0.98rem; -} - -.detail-section-head p { - color: var(--muted); - font-size: 0.88rem; -} - -.detail-pre { +.detail-pre, +.code-block-wrap pre { margin: 0; max-height: 340px; overflow: auto; - padding: 12px; - border-radius: 10px; - border: 1px solid #cdbca6; - background: #1d1a16; - color: #d2f7dc; - font-family: 'IBM Plex Mono', monospace; - font-size: 0.83rem; - line-height: 1.45; + padding: 14px; + border-radius: 16px; + border: 1px solid var(--line); + background: #f7fafd; + color: #14314d; + font-size: 0.82rem; + line-height: 1.55; } .timeline-list { margin: 0; padding: 0; - display: grid; - gap: 10px; } .timeline-item { list-style: none; - border: 1px solid #ecdcc8; - border-radius: 10px; - background: #fcfaf6; - padding: 10px; - display: grid; - gap: 8px; -} - -.timeline-item-head { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 10px; } .timeline-item-meta { @@ -618,36 +839,12 @@ p { font-size: 0.88rem; } -.timeline-error { - color: var(--danger); -} - -.replay-block, -.replay-inline { - display: grid; - gap: 6px; -} - -.replay-inline { - display: inline-grid; -} - -.replay-btn { - width: fit-content; -} - -.table-skeleton, -.detail-skeleton { - display: grid; - gap: 10px; -} - .skeleton-line, .skeleton-block { - border-radius: 10px; - background: linear-gradient(90deg, #eee3d1 25%, #f6efe5 37%, #eee3d1 63%); + border-radius: 14px; + background: linear-gradient(90deg, #eef3f9 25%, #f8fbff 37%, #eef3f9 63%); background-size: 400% 100%; - animation: skeleton-pulse 1.3s ease-in-out infinite; + animation: skeleton-pulse 1.4s ease-in-out infinite; } .skeleton-line { @@ -658,6 +855,59 @@ p { min-height: 180px; } +@keyframes heartbeat-core { + 0%, + 30%, + 100% { + transform: scale(1); + } + 10% { + transform: scale(1.45); + } + 18% { + transform: scale(0.92); + } + 24% { + transform: scale(1.24); + } +} + +@keyframes heartbeat-ring { + 0% { + transform: scale(0.72); + opacity: 0; + } + 12% { + opacity: 0.45; + } + 34% { + opacity: 0.18; + } + 100% { + transform: scale(1.12); + opacity: 0; + } +} + +@keyframes heartbeat-dot { + 0%, + 30%, + 100% { + transform: scale(1); + opacity: 0.82; + } + 10% { + transform: scale(1.35); + } + 18% { + transform: scale(0.88); + } + 24% { + transform: scale(1.18); + opacity: 1; + } +} + @keyframes skeleton-pulse { 0% { background-position: 100% 50%; @@ -667,20 +917,57 @@ p { } } -@media (max-width: 680px) { +@media (max-width: 1180px) { .app-shell { - padding: 16px 12px 30px; + grid-template-columns: 1fr; } - .nav-chip { - flex: 1 1 48%; - text-align: center; + .app-sidebar { + position: static; + height: auto; + border-right: 0; + border-bottom: 1px solid var(--line); } + .metric-rail, + .workspace-grid, + .dashboard-hero, + .pipeline-grid, .events-workbench { grid-template-columns: 1fr; } + .dashboard-hero-side { + justify-items: start; + } + + .feed-row { + grid-template-columns: 1fr; + } + + .feed-row-meta { + justify-content: flex-start; + } +} + +@media (max-width: 720px) { + .app-main, + .app-sidebar { + padding: 16px 12px; + } + + .dashboard-hero, + .surface-block, + .card, + .token-panel { + padding: 14px; + } + + .dashboard-hero-copy h1 { + max-width: none; + } + + .metric-rail, .detail-meta-grid { grid-template-columns: 1fr; }