diff --git a/frontend/editor/public/locales/en-US/translation.toml b/frontend/editor/public/locales/en-US/translation.toml index acaaf00d80..5f07f5d462 100644 --- a/frontend/editor/public/locales/en-US/translation.toml +++ b/frontend/editor/public/locales/en-US/translation.toml @@ -6791,23 +6791,6 @@ description = "This view hit an unexpected error. Try again, or pick another sec retry = "Try again" title = "Something went wrong on this page" -[portal.forkWizard] -subtitle = "Clone a proven workflow and tune it — every template ships the same four-stage backbone." -title = "Fork a starter pipeline" - -[portal.forkWizard.action] -cancel = "Cancel" -deploy = "Deploy pipeline" -pickAnother = "Pick another" - -[portal.forkWizard.status] -building = "Building…" -ready = "Ready to deploy" - -[portal.home.chart.empty] -description = "Once documents are processed, your 30-day usage appears here." -title = "No usage yet" - [portal.home.editor] activeUsers = "{{n}} active" invite = "Invite teammates" @@ -6825,25 +6808,6 @@ afternoon = "Good afternoon" evening = "Good evening" morning = "Good morning" -[portal.home.kpis.enterprise] -docs30d = "Docs / 30d" -evalPassRate = "Eval pass rate" -p95Latency = "P95 latency" -slaUptime = "SLA uptime (30d)" - -[portal.home.kpis.free] -agents = "Agents" -docsProcessed = "Docs processed" -docsProcessedDescription = "Free plan cap" -operations = "Operations" -pipelines = "Pipelines" - -[portal.home.kpis.pro] -agentsActive = "Agents active" -docs30d = "Docs / 30d" -evalPassRate = "Eval pass rate" -pipelines = "Pipelines" - [portal.home.onboarding] dismiss = "Dismiss setup" notStarted = "Not started" @@ -6871,25 +6835,6 @@ blurb = "{{connected}} connected · every PDF governed where it lands" chip = "{{connected}} connected" title = "Connect your sources" -[portal.home.productGrid] -ariaLabel = "Process PDFs at scale" - -[portal.home.productGrid.agents] -blurb = "Wire your agent via MCP, REST, or tool definitions. Deterministic operations, scenarios, evals." -cta = "Connect an agent" -title = "Agents" - -[portal.home.productGrid.pipelines] -badge = "Hero" -blurb = "Compose document workflows from typed operations. Upload a sample to get suggestions or start blank." -cta = "Build a pipeline" -title = "Pipelines" - -[portal.home.productGrid.sources] -blurb = "Attach pipelines where PDFs already live — S3, agents, SharePoint, webhooks, batch, email." -cta = "Connect a source" -title = "Sources" - [portal.home.quickActions] subtitle = "Top tasks for today" title = "Quick actions" @@ -6906,18 +6851,6 @@ title = "Connect a source" blurb = "Scoped key with rate limits and IP allowlist" title = "Issue an API key" -[portal.home.quickActions.tryOp] -blurb = "Drop a sample, pick an op, see the JSON" -title = "Try a PDF operation" - -[portal.home.regions] -subtitle = "Real-time status for every deployed Stirling region." -title = "Region health" - -[portal.home.regions.empty] -description = "Once a region is deployed, its health appears here." -title = "No regions yet" - [portal.infrastructure] manageEditorDeployment = "Manage Editor deployment" sectionsAriaLabel = "Infrastructure sections" @@ -7270,51 +7203,6 @@ new_other = "{{count}} new" description = "No new notifications." title = "You're all caught up" -[portal.opRunner] -durationMs = "{{ms}} ms" -featuredOps = "Featured ops" -subtitle = "Drop a sample, pick an op, see what Stirling returns." -title = "Try a PDF operation" - -[portal.opRunner.action] -close = "Close" -openBuilder = "Open the pipeline builder" -run = "Run operation" -runAgain = "Run again" -running = "Running…" - -[portal.opRunner.drop] -hint = "or use a sample document." -pickAnother = "Pick another sample" -replaceHint = "Drop again or pick another sample to replace." -title = "Drop a PDF here" -useSample = "Use a sample" - -[portal.opRunner.empty] -description = "Once operations are published, they'll show up here." -title = "No featured ops yet" - -[portal.opRunner.error] -title = "The operation didn't complete" -unknown = "Unknown error" - -[portal.opRunner.hint] -aSample = "a sample" -press = "Press" -ready = "Ready" -toInvoke = "to invoke" - -[portal.opRunner.hint.runOn] -before = "Run" -middle = "on" - -[portal.opRunner.running] -title = "Running {{label}}…" - -[portal.opRunner.status] -completed = "Completed" -failed = "Failed" - [portal.pipelines] subtitle = "Every automated document pipeline on the backend: an ordered chain of operations over a set of sources, run on a trigger. Click a row for its steps and sources." title = "Pipelines" @@ -7573,9 +7461,6 @@ soon = "Soon" [portal.processingStatus] managePlan = "Manage plan" -pdfsThisMonth = "PDFs this month" -progressLabel = "{{used}} of {{cap}} PDFs used this month" -upgrade = "Upgrade" volumeSuffix = "PDFs processed · last 30 days" [portal.procurement] @@ -7944,8 +7829,8 @@ appEditor = "Editor" appProcessor = "Processor" brandSuffix = "Stirling Processor" docsCount = "{{docs}} docs" -docsProcessed = "Docs processed" linkAccount = "Link Stirling account" +planEditor = "Editor plan" planEnterprise = "Enterprise plan" planProcessor = "Processor plan" primaryNav = "Primary navigation" @@ -8053,40 +7938,6 @@ action = "Sign in again" body = "Your Stirling account session has expired. Sign in again to view billing — your instance stays linked." title = "Session expired" -[portal.usageChart] -defaultLabel = "Docs processed · last 30 days" -delta = "{{pct}}% vs prior 30d" -docsValue = "{{value}} docs" -srAnnounce = "{{date}}: {{value}} docs" - -[portal.useCases] -title = "Popular use cases" -viewAll = "View all pipelines" - -[portal.useCases.items.authenticity] -blurb = "Cryptographic checks at the document boundary — signature validation, tamper detection, signing flows for outbound documents. Trust decisions in the pipeline, not your app code." -cta = "Try authenticity check" -eyebrow = "AUTHENTICITY" -title = "Verify signatures and detect tampering" - -[portal.useCases.items.autoRouting] -blurb = "One classifier reads what arrived — KYC form, invoice, contract, COI — and routes to the right downstream pipeline. No manual triage, no docs in the wrong workflow." -cta = "Build a classifier pipeline" -eyebrow = "AUTO-ROUTING" -title = "Auto-classify and route incoming documents" - -[portal.useCases.items.piiRedaction] -blurb = "Strip sensitive fields before storage, indexing, or LLM processing. Schema-aware, per-field audit, BYOK or HYOK keys. Compliance at the document boundary, not per pipeline." -cta = "See redaction pipelines" -eyebrow = "PII REDACTION" -title = "Redact PII before it leaves your stack" - -[portal.useCases.items.trainingData] -blurb = "Batch-import an archive, redact PII, classify, chunk, and emit ready-to-load JSON for fine-tuning, eval sets, or RAG. Self-completing and replayable." -cta = "Build a training-data pipeline" -eyebrow = "TRAINING DATA" -title = "Turn PDFs into training data" - [portal.users] title = "Users" diff --git a/frontend/editor/src/portal/api/home.ts b/frontend/editor/src/portal/api/home.ts deleted file mode 100644 index 467fd6a66a..0000000000 --- a/frontend/editor/src/portal/api/home.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { apiClient } from "@portal/api/http"; -import type { - ActivityEvent, - KpiEntry, - RegionHealth, - UsageSeriesResponse, -} from "@portal/mocks/home"; -import type { Tier } from "@portal/contexts/TierContext"; - -export type { - ActivityEvent, - ActivityKind, - KpiEntry, - PipelineStage, - PipelineTemplate, - RegionHealth, - UsagePoint, - UsageSeriesResponse, -} from "@portal/mocks/home"; -export { PIPELINE_STAGES, PIPELINE_TEMPLATES } from "@portal/mocks/home"; - -/** GET /v1/analytics/usage?window=30d */ -export async function fetchUsageSeries(): Promise { - return apiClient.local.json( - "/v1/analytics/usage?window=30d", - ); -} - -/** GET /v1/activity?limit=8 */ -export async function fetchRecentActivity(): Promise { - return apiClient.local.json("/v1/activity?limit=8"); -} - -/** GET /v1/home/kpis?tier=… */ -export async function fetchHomeKpis(tier: Tier): Promise { - return apiClient.local.json( - `/v1/home/kpis?tier=${encodeURIComponent(tier)}`, - ); -} - -/** GET /v1/regions/health (Enterprise) */ -export async function fetchRegionHealth(): Promise { - return apiClient.local.json("/v1/regions/health"); -} diff --git a/frontend/editor/src/portal/api/ops.ts b/frontend/editor/src/portal/api/ops.ts deleted file mode 100644 index c8a564f307..0000000000 --- a/frontend/editor/src/portal/api/ops.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { apiClient, HttpError } from "@portal/api/http"; -import type { FeaturedOp, OpResultMap } from "@portal/mocks/ops"; - -export type { FeaturedOp, OpResultMap }; - -/** GET /v1/ops/featured */ -export async function fetchFeaturedOps(): Promise { - return apiClient.local.json("/v1/ops/featured"); -} - -export class UnknownOpError extends Error { - constructor(public readonly opId: string) { - super(`Unknown op: ${opId}`); - this.name = "UnknownOpError"; - } -} - -/** POST /v1/ops/{opId}/run */ -export async function runSingleOp( - opId: string, - sample: string, -): Promise<{ result: OpResultMap; durationMs: number }> { - try { - return await apiClient.local.json<{ - result: OpResultMap; - durationMs: number; - }>(`/v1/ops/${encodeURIComponent(opId)}/run`, { - method: "POST", - body: { sample }, - }); - } catch (err) { - if (err instanceof HttpError && err.status === 404) { - throw new UnknownOpError(opId); - } - throw err; - } -} diff --git a/frontend/editor/src/portal/components/PipelineForkWizard.css b/frontend/editor/src/portal/components/PipelineForkWizard.css deleted file mode 100644 index a126f0dc4f..0000000000 --- a/frontend/editor/src/portal/components/PipelineForkWizard.css +++ /dev/null @@ -1,225 +0,0 @@ -.portal-fork__head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 0.75rem; - margin-bottom: 0.875rem; -} - -.portal-fork__title { - margin: 0; - font-size: 0.9375rem; - font-weight: 600; - color: var(--color-text-1); -} - -.portal-fork__sub { - margin: 0.125rem 0 0; - font-size: 0.75rem; - color: var(--color-text-4); - max-width: 32rem; -} - -/* Template picker */ -.portal-fork__templates { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 0.625rem; -} - -@media (max-width: 48rem) { - .portal-fork__templates { - grid-template-columns: 1fr; - } -} - -.portal-fork__template { - display: flex; - flex-direction: column; - gap: 0.375rem; - padding: 0.75rem 0.875rem; - text-align: left; - background: var(--color-bg-subtle); - border: 1px solid var(--color-border-light); - border-left: 3px solid var(--accent, var(--color-blue)); - border-radius: var(--radius-md); - transition: - background var(--motion-fast), - border-color var(--motion-fast), - transform var(--motion-fast); -} - -.portal-fork__template[data-accent="blue"] { - --accent: var(--color-blue); -} -.portal-fork__template[data-accent="purple"] { - --accent: var(--color-purple); -} -.portal-fork__template[data-accent="green"] { - --accent: var(--color-green); -} -.portal-fork__template[data-accent="amber"] { - --accent: var(--color-amber); -} - -.portal-fork__template:hover { - background: var(--color-bg-hover); - border-color: var(--color-border); - border-left-color: var(--accent); - transform: translateY(-1px); -} - -.portal-fork__template-name { - font-size: 0.875rem; - font-weight: 600; - color: var(--color-text-1); -} - -.portal-fork__template-blurb { - font-size: 0.75rem; - color: var(--color-text-4); - line-height: 1.45; -} - -.portal-fork__template-types { - display: flex; - flex-wrap: wrap; - gap: 0.25rem; - margin-top: 0.125rem; -} - -/* Build / ready state */ -.portal-fork__build-head { - display: flex; - flex-direction: column; - margin-bottom: 0.875rem; -} - -.portal-fork__build-head strong { - font-size: 0.875rem; - color: var(--color-text-1); -} - -.portal-fork__build-head span { - font-size: 0.75rem; - color: var(--color-text-4); -} - -.portal-fork__stages { - list-style: none; - margin: 0; - padding: 0; - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 0.5rem; -} - -@media (max-width: 48rem) { - .portal-fork__stages { - grid-template-columns: 1fr 1fr; - } -} - -.portal-fork__stage { - position: relative; - display: flex; - flex-direction: column; - gap: 0.375rem; - padding: 0.625rem 0.75rem; - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - opacity: 0.55; - transition: - opacity var(--motion-base), - border-color var(--motion-base), - background var(--motion-base); -} - -.portal-fork__stage.is-active, -.portal-fork__stage.is-done { - opacity: 1; -} - -.portal-fork__stage.is-active { - border-color: var(--color-blue); - background: var(--color-blue-light); -} - -.portal-fork__stage.is-done { - border-color: color-mix(in srgb, var(--color-green) 35%, transparent); - background: var(--color-green-light); -} - -.portal-fork__stage-mark { - display: inline-flex; - align-items: center; - justify-content: center; - width: 1.5rem; - height: 1.5rem; - border-radius: 50%; - font-size: 0.75rem; - font-weight: 600; - background: var(--color-bg-subtle); - border: 1px solid var(--color-border); - color: var(--color-text-4); -} - -.portal-fork__stage.is-done .portal-fork__stage-mark { - background: var(--color-green); - border-color: var(--color-green); - color: var(--color-text-on-accent); -} - -.portal-fork__stage.is-active .portal-fork__stage-mark { - border-color: var(--color-blue); - color: var(--color-blue); -} - -.portal-fork__stage-text { - display: flex; - flex-direction: column; -} - -.portal-fork__stage-text strong { - font-size: 0.8125rem; - font-weight: 600; - color: var(--color-text-1); -} - -.portal-fork__stage-text span { - font-size: 0.6875rem; - color: var(--color-text-4); - line-height: 1.4; -} - -.portal-fork__stage-spin { - position: absolute; - top: 0.625rem; - right: 0.625rem; - width: 0.875rem; - height: 0.875rem; - border: 2px solid color-mix(in srgb, var(--color-blue) 30%, transparent); - border-top-color: var(--color-blue); - border-radius: 50%; - animation: portal-fork-spin 0.7s linear infinite; -} - -@keyframes portal-fork-spin { - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: reduce) { - .portal-fork__stage-spin { - animation: none; - } -} - -.portal-fork__build-actions { - display: flex; - justify-content: flex-end; - gap: 0.5rem; - margin-top: 1rem; -} diff --git a/frontend/editor/src/portal/components/PipelineForkWizard.stories.tsx b/frontend/editor/src/portal/components/PipelineForkWizard.stories.tsx deleted file mode 100644 index 8701e8e9c5..0000000000 --- a/frontend/editor/src/portal/components/PipelineForkWizard.stories.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { PipelineForkWizard } from "@portal/components/PipelineForkWizard"; - -const meta: Meta = { - title: "Portal/Home/PipelineForkWizard", - component: PipelineForkWizard, - parameters: { layout: "padded" }, - decorators: [ - (S) => ( -
- -
- ), - ], -}; -export default meta; -type Story = StoryObj; - -/** Pick a template to watch the deterministic four-stage build animation. */ -export const Default: Story = {}; diff --git a/frontend/editor/src/portal/components/PipelineForkWizard.tsx b/frontend/editor/src/portal/components/PipelineForkWizard.tsx deleted file mode 100644 index 3a27b5984a..0000000000 --- a/frontend/editor/src/portal/components/PipelineForkWizard.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Button, Card, Chip, StatusBadge } from "@app/ui"; -import { useView } from "@portal/contexts/ViewContext"; -import { - PIPELINE_STAGES, - PIPELINE_TEMPLATES, - type PipelineTemplate, -} from "@portal/api/home"; -import "@portal/components/PipelineForkWizard.css"; - -/** - * Wizard phases: - * - `pick` — choose a starter template - * - `building` — deterministic stage-by-stage build animation - * - `ready` — all four stages lit; offer deploy - */ -type Phase = "pick" | "building" | "ready"; - -/** Time each build stage stays "in progress" before the next lights up. */ -const STAGE_STEP_MS = 550; - -export function PipelineForkWizard() { - const { t } = useTranslation(); - const { setActiveView } = useView(); - const [phase, setPhase] = useState("pick"); - const [template, setTemplate] = useState(null); - // How many stages have completed. Drives both the animation and the - // pick→building→ready transitions; advanced purely by a fixed-interval timer - // so the sequence is identical on every run (no Math.random / Date.now). - const [builtStages, setBuiltStages] = useState(0); - const timerRef = useRef(null); - - // Advance one stage per tick while building; settle into `ready` once all - // stages are done. The effect re-arms itself on each builtStages change - // rather than holding a single long-lived interval, so cleanup is trivial. - useEffect(() => { - if (phase !== "building") return; - if (builtStages >= PIPELINE_STAGES.length) { - setPhase("ready"); - return; - } - timerRef.current = window.setTimeout(() => { - setBuiltStages((n) => n + 1); - }, STAGE_STEP_MS); - return () => { - if (timerRef.current !== null) window.clearTimeout(timerRef.current); - }; - }, [phase, builtStages]); - - function fork(t: PipelineTemplate) { - setTemplate(t); - setBuiltStages(0); - setPhase("building"); - } - - function reset() { - setPhase("pick"); - setTemplate(null); - setBuiltStages(0); - } - - function deploy() { - // TODO(backend): POST /v1/pipelines { templateId, name } to create the - // forked pipeline. Without a backend, route to the pipelines list. - setActiveView("pipelines"); - } - - return ( - -
-
-

{t("portal.forkWizard.title")}

-

{t("portal.forkWizard.subtitle")}

-
- {phase !== "pick" && template && ( - - {phase === "ready" - ? t("portal.forkWizard.status.ready") - : t("portal.forkWizard.status.building")} - - )} -
- - {phase === "pick" && ( -
- {PIPELINE_TEMPLATES.map((t) => ( - - ))} -
- )} - - {phase !== "pick" && template && ( -
-
- {template.name} - {template.blurb} -
- -
    - {PIPELINE_STAGES.map((stage, i) => { - const done = i < builtStages; - const active = phase === "building" && i === builtStages; - const cls = - "portal-fork__stage" + - (done ? " is-done" : "") + - (active ? " is-active" : ""); - return ( -
  1. - - {done ? "✓" : i + 1} - - - {stage.label} - {stage.detail} - - {active && ( - - )} -
  2. - ); - })} -
- -
- - -
-
- )} -
- ); -} diff --git a/frontend/editor/src/portal/components/PopularUseCases.css b/frontend/editor/src/portal/components/PopularUseCases.css deleted file mode 100644 index af79deb2a9..0000000000 --- a/frontend/editor/src/portal/components/PopularUseCases.css +++ /dev/null @@ -1,87 +0,0 @@ -.portal-usecases { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.portal-usecases__head { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 0.5rem; -} - -.portal-usecases__title { - margin: 0; - font-size: 1.125rem; - font-weight: 600; - color: var(--color-text-1); -} - -.portal-usecases__viewall, -.portal-usecases__cta { - background: none; - border: none; - padding: 0; - cursor: pointer; - font-family: inherit; - font-weight: 500; - display: inline-flex; - align-items: center; - gap: 0.25rem; - transition: opacity var(--motion-fast); -} - -.portal-usecases__viewall { - font-size: 0.8125rem; - color: var(--color-blue); -} - -.portal-usecases__viewall:hover, -.portal-usecases__cta:hover { - opacity: 0.8; -} - -.portal-usecases__grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 0.875rem; -} - -@media (max-width: 50rem) { - .portal-usecases__grid { - grid-template-columns: 1fr; - } -} - -.portal-usecases__card { - display: flex; - flex-direction: column; - gap: 0.375rem; -} - -.portal-usecases__eyebrow { - font-size: 0.625rem; - font-weight: 600; - letter-spacing: 0.08em; -} - -.portal-usecases__card-title { - margin: 0.125rem 0 0; - font-size: 1rem; - font-weight: 600; - color: var(--color-text-1); -} - -.portal-usecases__blurb { - margin: 0; - font-size: 0.8125rem; - line-height: 1.5; - color: var(--color-text-3); -} - -.portal-usecases__cta { - margin-top: 0.5rem; - align-self: flex-start; - font-size: 0.8125rem; -} diff --git a/frontend/editor/src/portal/components/PopularUseCases.tsx b/frontend/editor/src/portal/components/PopularUseCases.tsx deleted file mode 100644 index 85ccc90c72..0000000000 --- a/frontend/editor/src/portal/components/PopularUseCases.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Button, Card, type CardProps } from "@app/ui"; -import { useView } from "@portal/contexts/ViewContext"; -import "@portal/components/PopularUseCases.css"; - -type Accent = NonNullable; - -interface UseCase { - /** Stable key into the useCases.items.* translation table. */ - key: string; - accent: Accent; -} - -const ACCENT_COLOR: Partial> = { - default: "var(--color-blue)", - premium: "var(--color-purple)", - success: "var(--color-green)", - warning: "var(--color-amber)", - danger: "var(--color-red)", -}; - -/** - * Curated landing-page use cases — a teaser, not the full catalogue. The - * exhaustive per-vertical endpoint list lives on the Documents view; here we - * surface the four cross-cutting pipelines people reach for first. The display - * copy (eyebrow, title, blurb, cta) is keyed into useCases.items.. - */ -const USE_CASES: UseCase[] = [ - { key: "autoRouting", accent: "default" }, - { key: "piiRedaction", accent: "danger" }, - { key: "trainingData", accent: "premium" }, - { key: "authenticity", accent: "success" }, -]; - -export function PopularUseCases() { - const { t } = useTranslation(); - const { setActiveView } = useView(); - return ( -
-
-

{t("portal.useCases.title")}

- -
-
- {USE_CASES.map((uc) => ( - - - {t(`portal.useCases.items.${uc.key}.eyebrow`)} - -

- {t(`portal.useCases.items.${uc.key}.title`)} -

-

- {t(`portal.useCases.items.${uc.key}.blurb`)} -

- -
- ))} -
-
- ); -} diff --git a/frontend/editor/src/portal/components/ProcessingStatusStrip.css b/frontend/editor/src/portal/components/ProcessingStatusStrip.css index 911bf99541..63341798f6 100644 --- a/frontend/editor/src/portal/components/ProcessingStatusStrip.css +++ b/frontend/editor/src/portal/components/ProcessingStatusStrip.css @@ -1,39 +1,4 @@ -.portal-statusstrip--free { - display: flex; - flex-direction: column; -} - -/* The Banner body already provides padding; lay out the free meter inside it. */ -.portal-statusstrip--free .sui-banner__body { - display: flex; - flex-direction: column; - gap: 0.375rem; -} - -.portal-statusstrip__free-row { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 0.5rem; -} - -.portal-statusstrip__free-label { - font-size: 0.8125rem; - color: var(--color-text-2); -} - -.portal-statusstrip__free-label strong { - color: var(--color-text-1); - font-weight: 600; -} - -.portal-statusstrip__free-pct { - font-size: 0.75rem; - font-family: var(--font-mono); - color: var(--color-text-4); -} - -/* Paid strip: single inline row */ +/* Single inline row: plan + real processed-PDF volume + manage-plan shortcut. */ .portal-statusstrip--paid { display: flex; align-items: center; diff --git a/frontend/editor/src/portal/components/ProcessingStatusStrip.tsx b/frontend/editor/src/portal/components/ProcessingStatusStrip.tsx index 1ef41f5e0f..af198d488f 100644 --- a/frontend/editor/src/portal/components/ProcessingStatusStrip.tsx +++ b/frontend/editor/src/portal/components/ProcessingStatusStrip.tsx @@ -1,90 +1,23 @@ import { useTranslation } from "react-i18next"; -import { Banner, Button, ProgressBar, Skeleton } from "@app/ui"; +import { Button, Skeleton } from "@app/ui"; import { TIER_INFO, useTier } from "@portal/contexts/TierContext"; import { useView } from "@portal/contexts/ViewContext"; import { useAsync } from "@portal/hooks/useAsync"; -import { fetchHomeKpis, type KpiEntry } from "@portal/api/home"; +import { fetchFleetStats, type FleetStats } from "@portal/api/fleetStats"; import "@portal/components/ProcessingStatusStrip.css"; /** - * Parses the free-tier "used / cap" KPI string (e.g. "247 / 500") into its - * parts. The free meter is the headline KPI value rather than a separate - * endpoint, so the strip reads it from the same `fetchHomeKpis` payload the - * KPI cards use — no duplicate fetch, no second source of truth. + * A thin one-line header above the home content: current plan + the real 30-day + * processed-PDF volume (from the fleet-usage endpoint), with a shortcut to the + * Usage page. Renders "—" while loading or when the backend can't compute the + * figure (e.g. EE auditing disabled) — never a fabricated number. */ -function parseUsage(value: KpiEntry["value"]): { - used: number; - cap: number; -} | null { - const match = String(value).match(/([\d,]+)\s*\/\s*([\d,]+)/); - if (!match) return null; - const used = Number(match[1].replace(/,/g, "")); - const cap = Number(match[2].replace(/,/g, "")); - if (!Number.isFinite(used) || !Number.isFinite(cap) || cap <= 0) return null; - return { used, cap }; -} - export function ProcessingStatusStrip() { const { t } = useTranslation(); const { tier } = useTier(); const { setActiveView } = useView(); - const { data: kpis, loading } = useAsync( - () => fetchHomeKpis(tier), - [tier], - ); + const { data, loading } = useAsync(() => fetchFleetStats(), []); - if (loading) { - return ( -
- - -
- ); - } - - if (tier === "free") { - const usage = parseUsage(kpis?.[0]?.value ?? ""); - const used = usage?.used ?? 0; - const cap = usage?.cap ?? 500; - const ratio = cap > 0 ? used / cap : 0; - const nearCap = ratio >= 0.8; - - return ( - setActiveView("usage")} - > - {t("portal.processingStatus.upgrade")} - - ) : undefined - } - > -
- - {used.toLocaleString()} / {cap.toLocaleString()}{" "} - {t("portal.processingStatus.pdfsThisMonth")} - - - {Math.round(ratio * 100)}% - -
- -
- ); - } - - // Pro / enterprise: plan name + headline volume from the first KPI. - const volume = kpis?.[0]?.value; return (
@@ -99,7 +32,11 @@ export function ProcessingStatusStrip() { · - {volume ?? "—"}{" "} + {loading ? ( + + ) : ( + {data?.pdfsProcessed?.toLocaleString() ?? "—"} + )}{" "} {t("portal.processingStatus.volumeSuffix")} @@ -63,13 +90,13 @@ export function RecentActivity() { /> )} - {events && events.length > 0 && ( + {events.length > 0 && (
    {events.map((event) => (
  1. @@ -77,15 +104,17 @@ export function RecentActivity() { {event.action} - {event.time} + + {event.timestamp} +
    -
    {event.subject}
    +
    {event.target}
    - {event.detail} + {AUDIT_CAT_LABEL[event.category]} · {event.actor} - - {event.status} + + {AUDIT_STATUS_LABEL[event.status]}
diff --git a/frontend/editor/src/portal/components/Sidebar.css b/frontend/editor/src/portal/components/Sidebar.css index fcc8c7e6a8..62d6226151 100644 --- a/frontend/editor/src/portal/components/Sidebar.css +++ b/frontend/editor/src/portal/components/Sidebar.css @@ -91,29 +91,11 @@ gap: 0.5rem; } -.portal-sidebar__usage-label { - color: var(--color-text-4); -} - .portal-sidebar__usage-value { color: var(--color-usage-value); font-weight: 500; } -.portal-sidebar__usage-track { - margin-top: 0.5rem; - height: 0.25rem; - background: var(--color-usage-track); - border-radius: var(--radius-pill); - overflow: hidden; -} - -.portal-sidebar__usage-fill { - height: 100%; - background: var(--grad-blue-btn); - transition: width var(--motion-slow); -} - .portal-sidebar__plan { display: inline-flex; align-items: center; diff --git a/frontend/editor/src/portal/components/Sidebar.tsx b/frontend/editor/src/portal/components/Sidebar.tsx index fdb5a61b03..9bf7c8c373 100644 --- a/frontend/editor/src/portal/components/Sidebar.tsx +++ b/frontend/editor/src/portal/components/Sidebar.tsx @@ -8,7 +8,7 @@ import { useTheme } from "@portal/contexts/ThemeContext"; import { useUI } from "@portal/contexts/UIContext"; import { LinkAccountFooterItem } from "@portal/components/LinkAccountFooterItem"; import { useAsync } from "@portal/hooks/useAsync"; -import { fetchHomeKpis, type KpiEntry } from "@portal/api/home"; +import { fetchFleetStats, type FleetStats } from "@portal/api/fleetStats"; import { EDITOR_URL, EDITOR_IS_SAME_APP } from "@portal/auth/editorUrl"; import markLight from "@app/assets/brand/modern-logo/StirlingPDFLogoNoTextLight.svg"; import markDark from "@app/assets/brand/modern-logo/StirlingPDFLogoNoTextDark.svg"; @@ -24,48 +24,17 @@ import "@portal/components/Sidebar.css"; function UsageFooter() { const { tier } = useTier(); const { t } = useTranslation(); - // Read the same endpoint Home's KPI strip uses so the doc count here can't - // drift from the headline figure. The first KPI is always the doc total. - const { data: kpis, loading } = useAsync( - () => fetchHomeKpis(tier), - [tier], - ); - const docs = loading ? undefined : kpis?.[0]?.value; - - if (tier === "free") { - // The free doc KPI is formatted "used / cap"; parse it for the meter. - const [used, cap] = - typeof docs === "string" - ? docs.split("/").map((s) => Number(s.replace(/[^\d]/g, ""))) - : []; - const pct = used && cap ? (used / cap) * 100 : 0; - return ( -
-
- - {t("portal.shell.sidebar.docsProcessed")} - - {docs ?? "—"} -
-
-
-
-
- ); - } + // Real 30-day processed-PDF count from the fleet-usage endpoint (the same + // source as Home's status strip, so the two can't drift). null → "—". + const { data, loading } = useAsync(() => fetchFleetStats(), []); + const docs = loading ? undefined : (data?.pdfsProcessed ?? undefined); const planLabel = - tier === "pro" - ? t("portal.shell.sidebar.planProcessor", "Processor plan") - : t("portal.shell.sidebar.planEnterprise", "Enterprise plan"); + tier === "free" + ? t("portal.shell.sidebar.planEditor", "Editor plan") + : tier === "pro" + ? t("portal.shell.sidebar.planProcessor", "Processor plan") + : t("portal.shell.sidebar.planEnterprise", "Enterprise plan"); return (
diff --git a/frontend/editor/src/portal/components/SingleOpRunner.css b/frontend/editor/src/portal/components/SingleOpRunner.css deleted file mode 100644 index 47b998388a..0000000000 --- a/frontend/editor/src/portal/components/SingleOpRunner.css +++ /dev/null @@ -1,345 +0,0 @@ -.portal-runner__layout { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); - gap: 1rem; - min-height: 26rem; -} - -@media (max-width: 50rem) { - .portal-runner__layout { - grid-template-columns: 1fr; - } -} - -.portal-runner__left, -.portal-runner__right { - display: flex; - flex-direction: column; - gap: 0.875rem; - min-width: 0; -} - -/* Drop zone */ -.portal-runner__drop { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.625rem; - padding: 1.25rem; - border: 1.5px dashed var(--color-border-input); - border-radius: var(--radius-md); - background: var(--color-bg-subtle); - color: var(--color-text-3); - text-align: center; - transition: - border-color var(--motion-fast), - background var(--motion-fast); -} - -.portal-runner__drop.is-armed, -.portal-runner__drop:hover { - border-color: var(--color-blue); - background: var(--color-blue-light); - color: var(--color-blue); -} - -.portal-runner__drop.has-file { - border-color: var(--color-green); - background: var(--color-green-light); - color: var(--color-text-2); -} - -.portal-runner__drop-icon { - color: var(--color-text-4); -} - -.portal-runner__drop.is-armed .portal-runner__drop-icon, -.portal-runner__drop.has-file .portal-runner__drop-icon { - color: inherit; -} - -.portal-runner__drop-text { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.125rem; - font-size: 0.8125rem; -} - -.portal-runner__drop-text strong { - font-size: 0.875rem; - color: var(--color-text-1); - font-family: var(--font-mono); -} - -.portal-runner__drop-text span { - font-size: 0.75rem; - color: var(--color-text-4); -} - -.portal-runner__sample-btn { - font-size: 0.75rem; - font-weight: 500; - color: var(--color-blue); - padding: 0.25rem 0.5rem; - border-radius: var(--radius-sm); - transition: background var(--motion-fast); -} - -.portal-runner__sample-btn:hover { - background: var(--color-bg-hover); -} - -/* Op picker */ -.portal-runner__section-title { - font-size: 0.6875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--color-section-label); - margin-bottom: 0.5rem; -} - -.portal-runner__ops { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 0.375rem; -} - -.portal-runner__op { - position: relative; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 0.125rem; - padding: 0.5rem 0.625rem; - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); - text-align: left; - transition: - border-color var(--motion-fast), - background var(--motion-fast); -} - -.portal-runner__op:hover { - border-color: var(--color-border-hover); - background: var(--color-bg-hover); -} - -.portal-runner__op.is-selected { - border-color: var(--color-blue); - background: var(--color-blue-light); -} - -.portal-runner__op[data-accent="purple"].is-selected { - border-color: var(--color-purple); - background: var(--color-purple-light); -} -.portal-runner__op[data-accent="green"].is-selected { - border-color: var(--color-green); - background: var(--color-green-light); -} -.portal-runner__op[data-accent="amber"].is-selected { - border-color: var(--color-amber); - background: var(--color-amber-light); -} -.portal-runner__op[data-accent="red"].is-selected { - border-color: var(--color-red); - background: var(--color-red-light); -} - -.portal-runner__op-label { - font-size: 0.8125rem; - font-weight: 600; - color: var(--color-text-1); -} - -.portal-runner__op-endpoint { - font-family: var(--font-mono); - font-size: 0.6875rem; - color: var(--color-text-4); -} - -.portal-runner__op-blurb { - font-size: 0.6875rem; - color: var(--color-text-3); - line-height: 1.35; -} - -.portal-runner__op--skeleton { - display: flex; - flex-direction: column; - gap: 0.375rem; - cursor: default; -} -.portal-runner__op--skeleton:hover { - border-color: var(--color-border-light); - background: transparent; -} - -/* Right column states */ -.portal-runner__right { - border: 1px solid var(--color-border-light); - border-radius: var(--radius-md); - background: var(--color-bg-subtle); - padding: 1.125rem; - overflow: hidden; - min-height: 22rem; -} - -.portal-runner__hint { - color: var(--color-text-3); -} - -.portal-runner__hint-eyebrow { - font-size: 0.6875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--color-blue); - margin-bottom: 0.5rem; -} - -.portal-runner__hint h3 { - margin: 0 0 0.5rem; - font-size: 1rem; - font-weight: 600; - color: var(--color-text-1); -} - -.portal-runner__hint h3 code { - font-family: var(--font-mono); - font-size: 0.9375rem; - color: var(--color-blue); - background: var(--color-blue-light); - padding: 0 0.25rem; - border-radius: var(--radius-xs); -} - -.portal-runner__hint p { - margin: 0; - font-size: 0.8125rem; - line-height: 1.5; -} - -.portal-runner__hint p code { - font-family: var(--font-mono); - font-size: 0.75rem; - color: var(--color-text-2); - background: var(--color-bg-muted); - padding: 0 0.25rem; - border-radius: var(--radius-xs); -} - -.portal-runner__hint kbd { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-bg-muted); - padding: 0.0625rem 0.3125rem; - border-radius: var(--radius-xs); - border: 1px solid var(--color-border); -} - -/* Running state */ -.portal-runner__running { - display: flex; - align-items: center; - gap: 1rem; - padding: 1rem; - background: var(--color-blue-light); - border: 1px solid var(--color-blue-border); - border-radius: var(--radius-md); - color: var(--color-blue); -} - -.portal-runner__running-title { - font-size: 0.875rem; - font-weight: 600; - color: var(--color-text-1); - margin-bottom: 0.25rem; -} - -.portal-runner__running code { - font-family: var(--font-mono); - font-size: 0.75rem; -} - -.portal-runner__spinner, -.portal-runner__spinner-lg { - border-radius: 50%; - border: 2px solid currentColor; - border-right-color: transparent; - animation: spin 0.7s linear infinite; - flex-shrink: 0; -} - -.portal-runner__spinner { - width: 0.875rem; - height: 0.875rem; -} - -.portal-runner__spinner-lg { - width: 1.5rem; - height: 1.5rem; -} - -/* Done state */ -.portal-runner__result { - display: flex; - flex-direction: column; - height: 100%; -} - -.portal-runner__result-head { - display: flex; - align-items: center; - gap: 0.625rem; - padding: 0.5rem 0.75rem; - background: var(--code-bg-header); - border-bottom: 1px solid var(--code-border); - border-radius: var(--radius-md) var(--radius-md) 0 0; - color: var(--code-muted); - font-family: var(--font-mono); - font-size: 0.75rem; -} - -.portal-runner__result-head code { - flex: 1; - font-family: inherit; - color: var(--code-keyword); -} - -.portal-runner__result-meta { - font-size: 0.6875rem; - color: var(--code-dim); -} - -.portal-runner__result-code { - margin: 0; - padding: 0.875rem 1rem; - background: var(--code-bg); - color: var(--code-text); - font-family: var(--font-mono); - font-size: 0.75rem; - line-height: 1.6; - border-radius: 0 0 var(--radius-md) var(--radius-md); - overflow: auto; - white-space: pre; - max-height: 22rem; -} - -/* Footer */ -.portal-runner__footer-status { - margin-right: auto; - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.75rem; - color: var(--color-text-4); - font-family: var(--font-mono); -} - -.portal-runner__footer-status code { - font-family: inherit; -} diff --git a/frontend/editor/src/portal/components/SingleOpRunner.stories.tsx b/frontend/editor/src/portal/components/SingleOpRunner.stories.tsx deleted file mode 100644 index 7f696d1948..0000000000 --- a/frontend/editor/src/portal/components/SingleOpRunner.stories.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { http, HttpResponse, delay } from "msw"; -import { SingleOpRunner } from "@portal/components/SingleOpRunner"; - -const meta: Meta = { - title: "Portal/Home/SingleOpRunner", - component: SingleOpRunner, - parameters: { layout: "fullscreen" }, - args: { open: true, onClose: () => console.log("close") }, - decorators: [ - (S) => ( -
- -
- ), - ], -}; -export default meta; -type Story = StoryObj; - -export const Idle: Story = {}; - -export const PreSelectedOp: Story = { - args: { initialOpId: "redact" }, -}; - -export const RunFailsWithUnknownOp: Story = { - parameters: { - msw: { - handlers: [ - http.post("/v1/ops/:opId/run", () => - HttpResponse.json({ error: "Unknown op" }, { status: 404 }), - ), - ], - }, - }, -}; - -export const SlowOp: Story = { - parameters: { - msw: { - handlers: [ - http.post("/v1/ops/:opId/run", async () => { - await delay(4000); - return HttpResponse.json({ - result: { schema: "demo", note: "took its time" }, - durationMs: 4000, - }); - }), - ], - }, - }, -}; diff --git a/frontend/editor/src/portal/components/SingleOpRunner.tsx b/frontend/editor/src/portal/components/SingleOpRunner.tsx deleted file mode 100644 index 3abb9f9850..0000000000 --- a/frontend/editor/src/portal/components/SingleOpRunner.tsx +++ /dev/null @@ -1,369 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Button, EmptyState, Modal, Skeleton, StatusBadge } from "@app/ui"; -import { useView } from "@portal/contexts/ViewContext"; -import { useAsync, useSectionFlags } from "@portal/hooks/useAsync"; -import { - fetchFeaturedOps, - runSingleOp, - type FeaturedOp, - type OpResultMap, -} from "@portal/api/ops"; -import "@portal/components/SingleOpRunner.css"; - -type Phase = "idle" | "running" | "done" | "error"; - -interface SingleOpRunnerProps { - open: boolean; - onClose: () => void; - /** Optional pre-selected op id (e.g. carousel deep-link). */ - initialOpId?: string; -} - -const SAMPLE_DOCS = [ - "certificate-of-insurance.pdf", - "loss-run-2025.pdf", - "invoice-acme-corp-q1.pdf", - "prior-auth-cigna-12471.pdf", - "contract-acme-msa-v3.pdf", -]; - -interface RunResult { - result: OpResultMap; - durationMs: number; - opId: string; -} - -export function SingleOpRunner({ - open, - onClose, - initialOpId, -}: SingleOpRunnerProps) { - const { t } = useTranslation(); - const { setActiveView } = useView(); - const opsState = useAsync(() => fetchFeaturedOps(), []); - const { data: ops } = opsState; - const { isLoading: opsIsLoading, isEmpty: opsIsEmpty } = - useSectionFlags(opsState); - - const [selectedOpId, setSelectedOpId] = useState( - initialOpId ?? null, - ); - const [phase, setPhase] = useState("idle"); - const [sample, setSample] = useState(null); - const [dragOver, setDragOver] = useState(false); - const [runResult, setRunResult] = useState(null); - const [errorMsg, setErrorMsg] = useState(null); - - // Default selection once the catalogue loads. - useEffect(() => { - if (!selectedOpId && ops && ops.length > 0) { - setSelectedOpId(ops[0].id); - } - }, [ops, selectedOpId]); - - const selectedOp = useMemo( - () => ops?.find((o) => o.id === selectedOpId) ?? null, - [ops, selectedOpId], - ); - - useEffect(() => { - if (open) { - setSelectedOpId(initialOpId ?? ops?.[0]?.id ?? null); - setPhase("idle"); - setSample(null); - setRunResult(null); - setErrorMsg(null); - } - // ops is intentionally not in deps — we don't want to reset state when - // the catalogue arrives while the modal is open. - }, [open, initialOpId]); - - const pickSample = useCallback(() => { - if (!ops) return; - const idx = ops.findIndex((o) => o.id === selectedOpId); - const safeIdx = idx < 0 ? 0 : idx; - setSample(SAMPLE_DOCS[safeIdx % SAMPLE_DOCS.length]); - }, [ops, selectedOpId]); - - async function run() { - if (!selectedOp) return; - let activeSample = sample; - if (!activeSample) { - pickSample(); - // pickSample updates state; use the synchronously-derived value for the - // call so we don't race. - const idx = ops?.findIndex((o) => o.id === selectedOpId) ?? 0; - activeSample = SAMPLE_DOCS[idx % SAMPLE_DOCS.length]; - } - setPhase("running"); - setErrorMsg(null); - try { - const res = await runSingleOp(selectedOp.id, activeSample); - setRunResult({ ...res, opId: selectedOp.id }); - setPhase("done"); - } catch (err) { - setPhase("error"); - setErrorMsg(err instanceof Error ? err.message : String(err)); - } - } - - function reset() { - setPhase("idle"); - setRunResult(null); - setErrorMsg(null); - } - - function buildPipelineWithOp() { - onClose(); - setActiveView("pipelines"); - } - - function onDragOver(e: React.DragEvent) { - e.preventDefault(); - setDragOver(true); - } - function onDragLeave() { - setDragOver(false); - } - function onDrop(e: React.DragEvent) { - e.preventDefault(); - setDragOver(false); - const file = e.dataTransfer.files[0]; - if (file) setSample(file.name); - else pickSample(); - } - - return ( - -
- {phase === "running" && selectedOp && ( - <> - - POST {selectedOp.endpoint} - - )} - {phase === "done" && ( - - 200 OK - - )} - {phase === "error" && ( - - {errorMsg ?? t("portal.opRunner.status.failed")} - - )} -
- - {phase === "done" ? ( - <> - - - - ) : ( - - )} - - } - > -
- {/* Left column: file + op picker */} -
-
-
- - - - -
-
- {sample ? ( - <> - {sample} - {t("portal.opRunner.drop.replaceHint")} - - ) : ( - <> - {t("portal.opRunner.drop.title")} - {t("portal.opRunner.drop.hint")} - - )} -
- -
- -
-
- {t("portal.opRunner.featuredOps")} -
-
- {opsIsLoading && - Array.from({ length: 4 }).map((_, i) => ( -
- - - -
- ))} - {opsIsEmpty && ( - - )} - {ops?.map((op) => ( - - ))} -
-
-
- - {/* Right column: state-driven result panel */} -
- {phase === "idle" && selectedOp && ( -
-
- {t("portal.opRunner.hint.ready")} -
-

- {t("portal.opRunner.hint.runOn.before")}{" "} - {selectedOp.label}{" "} - {t("portal.opRunner.hint.runOn.middle")}{" "} - {sample ?? t("portal.opRunner.hint.aSample")} -

-

- {selectedOp.blurb}. {t("portal.opRunner.hint.press")}{" "} - {t("portal.opRunner.action.run")}{" "} - {t("portal.opRunner.hint.toInvoke")}{" "} - POST {selectedOp.endpoint}. -

-
- )} - {phase === "running" && selectedOp && ( -
-
-
-
- {t("portal.opRunner.running.title", { - label: selectedOp.label, - })} -
- POST {selectedOp.endpoint} -
-
- )} - {phase === "done" && selectedOp && runResult && ( -
-
- - {t("portal.opRunner.status.completed")} - - POST {selectedOp.endpoint} - - {t("portal.opRunner.durationMs", { - ms: runResult.durationMs, - })} - -
-
-                {JSON.stringify(runResult.result, null, 2)}
-              
-
- )} - {phase === "error" && ( -
-
- {t("portal.opRunner.status.failed")} -
-

{t("portal.opRunner.error.title")}

-

{errorMsg ?? t("portal.opRunner.error.unknown")}

-
- )} -
-
-
- ); -} diff --git a/frontend/editor/src/portal/components/UsageAreaChart.css b/frontend/editor/src/portal/components/UsageAreaChart.css deleted file mode 100644 index c6d326c501..0000000000 --- a/frontend/editor/src/portal/components/UsageAreaChart.css +++ /dev/null @@ -1,115 +0,0 @@ -.portal-chart { - position: relative; - display: flex; - flex-direction: column; - gap: 0.75rem; - padding: 1.125rem 1.25rem; - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); -} - -.portal-chart__head { - display: flex; - align-items: flex-end; - justify-content: space-between; - gap: 0.75rem; -} - -.portal-chart__label { - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--color-section-label); -} - -.portal-chart__value { - margin-top: 0.125rem; - font-size: 1.5rem; - font-weight: 700; - color: var(--color-text-1); - font-variant-numeric: tabular-nums; -} - -.portal-chart__delta { - font-size: 0.75rem; - font-weight: 500; - padding: 0.1875rem 0.5rem; - border-radius: var(--radius-pill); -} - -.portal-chart__delta.is-up { - color: var(--color-green-dark); - background: var(--color-green-light); -} - -.portal-chart__delta.is-down { - color: var(--color-red); - background: var(--color-red-light); -} - -.portal-chart__svg { - width: 100%; - height: 15rem; - display: block; - overflow: visible; -} - -.portal-chart__gridline { - stroke: var(--color-border-light); - stroke-width: 1; - stroke-dasharray: 3 3; -} - -.portal-chart__axis-label { - font-family: var(--font-sans); - font-size: 10.5px; - fill: var(--color-text-5); -} - -.portal-chart__line { - fill: none; - stroke: var(--color-blue); - stroke-width: 1.75; - stroke-linecap: round; - stroke-linejoin: round; -} - -.portal-chart__scrub { - stroke: var(--color-text-5); - stroke-width: 1; - stroke-dasharray: 2 3; - opacity: 0.6; -} - -.portal-chart__dot { - fill: var(--color-blue); - stroke: var(--color-surface); - stroke-width: 2; -} - -.portal-chart__tooltip { - position: absolute; - transform: translateX(-50%); - top: 4rem; - padding: 0.4375rem 0.625rem; - background: var(--color-tooltip-bg); - color: var(--color-tooltip-text); - border-radius: var(--radius-sm); - font-size: 0.75rem; - white-space: nowrap; - pointer-events: none; - box-shadow: var(--shadow-md); - z-index: 2; -} - -.portal-chart__tooltip-date { - font-size: 0.6875rem; - opacity: 0.7; -} - -.portal-chart__tooltip-value { - font-weight: 600; - font-variant-numeric: tabular-nums; -} diff --git a/frontend/editor/src/portal/components/UsageAreaChart.stories.tsx b/frontend/editor/src/portal/components/UsageAreaChart.stories.tsx deleted file mode 100644 index 5c1f3e3dd9..0000000000 --- a/frontend/editor/src/portal/components/UsageAreaChart.stories.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; -import { UsageAreaChart } from "@portal/components/UsageAreaChart"; -import { buildUsageSeries } from "@portal/mocks/home"; - -const meta: Meta = { - title: "Portal/Home/UsageAreaChart", - component: UsageAreaChart, - parameters: { layout: "padded" }, - decorators: [ - (S) => ( -
- -
- ), - ], -}; -export default meta; -type Story = StoryObj; - -const series = buildUsageSeries(); -const total = series.reduce((sum, p) => sum + p.value, 0); - -export const Default: Story = { - args: { data: series, totalValue: total.toLocaleString(), deltaPct: 0.12 }, -}; - -export const NegativeDelta: Story = { - args: { data: series, totalValue: total.toLocaleString(), deltaPct: -0.08 }, -}; - -export const NoDelta: Story = { - args: { data: series }, -}; - -export const Flat: Story = { - args: { - data: series.map((p) => ({ ...p, value: 1200 })), - totalValue: "36,000", - }, -}; - -export const HighVolatility: Story = { - args: { - data: series.map((p, i) => ({ - ...p, - value: Math.round(p.value * (1 + Math.sin(i) * 0.6)), - })), - }, -}; - -export const Empty: Story = { - args: { data: [], totalValue: "—" }, -}; diff --git a/frontend/editor/src/portal/components/UsageAreaChart.tsx b/frontend/editor/src/portal/components/UsageAreaChart.tsx deleted file mode 100644 index ff9750d57c..0000000000 --- a/frontend/editor/src/portal/components/UsageAreaChart.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { useMemo, useRef, useState, type KeyboardEvent } from "react"; -import { useTranslation } from "react-i18next"; -import type { UsagePoint } from "@portal/api/home"; -import "@portal/components/UsageAreaChart.css"; - -interface UsageAreaChartProps { - data: UsagePoint[]; - /** Headline metric shown above the chart (e.g. total 30d, current value). */ - totalLabel?: string; - totalValue?: string; - deltaPct?: number; -} - -const PADDING = { top: 16, right: 20, bottom: 28, left: 32 } as const; -const VIEW = { width: 800, height: 240 } as const; - -function formatTick(date: string): string { - return new Date(date).toLocaleDateString(undefined, { - month: "short", - day: "numeric", - }); -} - -function formatNumber(value: number): string { - if (value >= 1000) { - return `${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`; - } - return value.toLocaleString(); -} - -export function UsageAreaChart({ - data, - totalLabel: totalLabelProp, - totalValue, - deltaPct, -}: UsageAreaChartProps) { - const { t } = useTranslation(); - const totalLabel = totalLabelProp ?? t("portal.usageChart.defaultLabel"); - const [hoverIndex, setHoverIndex] = useState(null); - const svgRef = useRef(null); - - const { points, areaPath, linePath, yMax, yTicks, xTickIndices } = - useMemo(() => { - if (data.length === 0) { - return { - points: [] as Array<{ x: number; y: number; raw: UsagePoint }>, - areaPath: "", - linePath: "", - yMax: 0, - yTicks: [] as number[], - xTickIndices: [] as number[], - }; - } - const max = Math.max(...data.map((d) => d.value)); - // Round yMax up to a "nice" number for ticks. Floor at 500 so an - // all-zero series doesn't divide by zero (which would NaN every point). - const niceMax = Math.max(Math.ceil(max / 500) * 500, 500); - const yTicksCalc = [0, niceMax * 0.5, niceMax]; - - const innerW = VIEW.width - PADDING.left - PADDING.right; - const innerH = VIEW.height - PADDING.top - PADDING.bottom; - const xStep = innerW / Math.max(data.length - 1, 1); - - const pts = data.map((raw, i) => ({ - x: PADDING.left + i * xStep, - y: PADDING.top + innerH - (raw.value / niceMax) * innerH, - raw, - })); - - const linePathStr = pts - .map( - (p, i) => - `${i === 0 ? "M" : "L"} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`, - ) - .join(" "); - const baseY = PADDING.top + innerH; - const areaPathStr = `${linePathStr} L ${pts[pts.length - 1].x.toFixed(2)} ${baseY} L ${pts[0].x.toFixed(2)} ${baseY} Z`; - - // X ticks: 5 evenly spread. - const xIdx: number[] = []; - const tickCount = 5; - for (let t = 0; t < tickCount; t++) { - xIdx.push(Math.round((t * (data.length - 1)) / (tickCount - 1))); - } - - return { - points: pts, - areaPath: areaPathStr, - linePath: linePathStr, - yMax: niceMax, - yTicks: yTicksCalc, - xTickIndices: xIdx, - }; - }, [data]); - - function onMouseMove(e: React.MouseEvent) { - if (!svgRef.current || points.length === 0) return; - const rect = svgRef.current.getBoundingClientRect(); - const xRatio = (e.clientX - rect.left) / rect.width; - const svgX = xRatio * VIEW.width; - // Find nearest point - let nearestIdx = 0; - let nearestDist = Infinity; - for (let i = 0; i < points.length; i++) { - const d = Math.abs(points[i].x - svgX); - if (d < nearestDist) { - nearestDist = d; - nearestIdx = i; - } - } - setHoverIndex(nearestIdx); - } - - const hovered = hoverIndex !== null ? points[hoverIndex] : null; - - function onKeyDown(e: KeyboardEvent) { - if (points.length === 0) return; - if (e.key === "ArrowRight") { - e.preventDefault(); - setHoverIndex((idx) => - idx === null ? 0 : Math.min(points.length - 1, idx + 1), - ); - } else if (e.key === "ArrowLeft") { - e.preventDefault(); - setHoverIndex((idx) => - idx === null ? points.length - 1 : Math.max(0, idx - 1), - ); - } else if (e.key === "Home") { - e.preventDefault(); - setHoverIndex(0); - } else if (e.key === "End") { - e.preventDefault(); - setHoverIndex(points.length - 1); - } else if (e.key === "Escape") { - setHoverIndex(null); - } - } - - const displayTotal = - totalValue ?? data.reduce((sum, p) => sum + p.value, 0).toLocaleString(); - - return ( -
-
-
-
{totalLabel}
-
{displayTotal}
-
- {deltaPct !== undefined && ( -
= 0 ? "is-up" : "is-down") - } - > - {deltaPct >= 0 ? "↑" : "↓"} - {t("portal.usageChart.delta", { - pct: Math.abs(Math.round(deltaPct * 100)), - })} -
- )} -
- - 0 ? 0 : -1} - onMouseMove={onMouseMove} - onMouseLeave={() => setHoverIndex(null)} - onKeyDown={onKeyDown} - onBlur={() => setHoverIndex(null)} - > - - - - - - - - {/* Y gridlines + labels */} - {yTicks.map((tick) => { - const innerH = VIEW.height - PADDING.top - PADDING.bottom; - const y = PADDING.top + innerH - (tick / yMax) * innerH; - return ( - - - - {formatNumber(tick)} - - - ); - })} - - {/* Area + line */} - - - - {/* X ticks */} - {xTickIndices.map((idx) => { - const p = points[idx]; - if (!p) return null; - return ( - - {formatTick(p.raw.date)} - - ); - })} - - {/* Hover scrub + dot + tooltip */} - {hovered && ( - <> - - - - )} - - - {hovered && ( -
-
- {new Date(hovered.raw.date).toLocaleDateString(undefined, { - weekday: "short", - month: "short", - day: "numeric", - })} -
-
- {t("portal.usageChart.docsValue", { - value: hovered.raw.value.toLocaleString(), - })} -
-
- )} -
- {hovered - ? t("portal.usageChart.srAnnounce", { - date: new Date(hovered.raw.date).toLocaleDateString(undefined, { - weekday: "short", - month: "short", - day: "numeric", - }), - value: hovered.raw.value.toLocaleString(), - }) - : ""} -
-
- ); -} diff --git a/frontend/editor/src/portal/mocks/handlers/home.ts b/frontend/editor/src/portal/mocks/handlers/home.ts deleted file mode 100644 index 5f295a7267..0000000000 --- a/frontend/editor/src/portal/mocks/handlers/home.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { http, HttpResponse, delay } from "msw"; -import type { Tier } from "@portal/contexts/TierContext"; -import { - buildUsageSeries, - buildUsageSeriesResponse, - enterpriseKpisFor, - FREE_KPIS, - proKpisFor, - RECENT_ACTIVITY, - REGION_HEALTH, - type KpiEntry, -} from "@portal/mocks/home"; - -function kpisFor(tier: Tier): KpiEntry[] { - if (tier === "free") return FREE_KPIS; - const docs30d = buildUsageSeries().reduce((sum, p) => sum + p.value, 0); - if (tier === "enterprise") return enterpriseKpisFor(docs30d); - return proKpisFor(docs30d); -} - -export const homeHandlers = [ - http.get("/v1/analytics/usage", async () => { - await delay(120); - return HttpResponse.json(buildUsageSeriesResponse()); - }), - - http.get("/v1/activity", async () => { - await delay(120); - return HttpResponse.json(RECENT_ACTIVITY); - }), - - http.get("/v1/home/kpis", async ({ request }) => { - await delay(120); - const url = new URL(request.url); - const tier = (url.searchParams.get("tier") ?? "pro") as Tier; - return HttpResponse.json(kpisFor(tier)); - }), - - http.get("/v1/regions/health", async () => { - await delay(120); - return HttpResponse.json(REGION_HEALTH); - }), -]; diff --git a/frontend/editor/src/portal/mocks/handlers/index.ts b/frontend/editor/src/portal/mocks/handlers/index.ts index 9341ce6304..0ce3059ff6 100644 --- a/frontend/editor/src/portal/mocks/handlers/index.ts +++ b/frontend/editor/src/portal/mocks/handlers/index.ts @@ -1,8 +1,6 @@ import { assistantHandlers } from "@portal/mocks/handlers/assistant"; import { authHandlers } from "@portal/mocks/handlers/auth"; -import { homeHandlers } from "@portal/mocks/handlers/home"; import { notificationsHandlers } from "@portal/mocks/handlers/notifications"; -import { opsHandlers } from "@portal/mocks/handlers/ops"; import { searchHandlers } from "@portal/mocks/handlers/search"; import { pipelinesHandlers } from "@portal/mocks/handlers/pipelines"; import { sourcesHandlers } from "@portal/mocks/handlers/sources"; @@ -21,8 +19,6 @@ import { linkHandlers } from "@portal/mocks/handlers/link"; export const handlers = [ ...authHandlers, - ...homeHandlers, - ...opsHandlers, ...notificationsHandlers, ...assistantHandlers, ...searchHandlers, @@ -53,8 +49,6 @@ export const handlers = [ * Everything kept is portal-only. `handlers` above is still the full set. */ export const embeddedDataHandlers = [ - ...homeHandlers, - ...opsHandlers, ...notificationsHandlers, ...assistantHandlers, ...searchHandlers, diff --git a/frontend/editor/src/portal/mocks/handlers/ops.ts b/frontend/editor/src/portal/mocks/handlers/ops.ts deleted file mode 100644 index e963cfcef8..0000000000 --- a/frontend/editor/src/portal/mocks/handlers/ops.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { http, HttpResponse, delay } from "msw"; -import { FEATURED_OPS, OP_RESULTS } from "@portal/mocks/ops"; - -export const opsHandlers = [ - http.get("/v1/ops/featured", async () => { - await delay(120); - return HttpResponse.json(FEATURED_OPS); - }), - - http.post("/v1/ops/:opId/run", async ({ params }) => { - const start = performance.now(); - await delay(800 + Math.random() * 200); - const opId = String(params.opId); - const result = OP_RESULTS[opId]; - if (!result) { - return HttpResponse.json( - { error: `Unknown op: ${opId}` }, - { status: 404 }, - ); - } - const enriched = - opId === "sign-output" - ? { ...result, signed_at: new Date().toISOString() } - : result; - return HttpResponse.json({ - result: enriched, - durationMs: Math.round(performance.now() - start), - }); - }), -]; diff --git a/frontend/editor/src/portal/mocks/home.ts b/frontend/editor/src/portal/mocks/home.ts deleted file mode 100644 index 373ae4adc1..0000000000 --- a/frontend/editor/src/portal/mocks/home.ts +++ /dev/null @@ -1,285 +0,0 @@ -/** - * Home dashboard fixtures and the types api/home.ts shares with them. - * api/home.ts imports the types; the MSW handlers in mocks/handlers/ serve the - * fixture data over the intercepted apiClient.local.json() calls. Components never reach - * into this module directly. - * - * Once a real backend exists, the MSW handlers stop being registered and these - * fixtures can be deleted (or kept as test seeds). - */ - -export interface UsagePoint { - /** ISO date (YYYY-MM-DD). */ - date: string; - /** Docs processed on that day. */ - value: number; -} - -/** - * Server response for the usage-series endpoint. Returning the prior window's - * total alongside the points lets the client derive the headline delta - * deterministically from real data rather than carrying a hardcoded figure. - */ -export interface UsageSeriesResponse { - points: UsagePoint[]; - /** Equivalent docs total from the immediately prior 30-day window. */ - priorTotal: number; -} - -/** Builds 30 daily points ending today. Deterministic per day. */ -export function buildUsageSeries(): UsagePoint[] { - const points: UsagePoint[] = []; - const now = new Date(); - for (let i = 29; i >= 0; i--) { - const d = new Date(now); - d.setDate(now.getDate() - i); - const day = d.getDay(); - // Weekend dip + slow uptrend + bounded noise. - const weekend = day === 0 || day === 6 ? 0.55 : 1; - const trend = 1 + (30 - i) * 0.012; - const wobble = 1 + Math.sin(i * 1.3) * 0.18 + Math.cos(i * 0.6) * 0.09; - const base = 1450 * weekend * trend * wobble; - points.push({ - date: d.toISOString().slice(0, 10), - value: Math.round(base), - }); - } - return points; -} - -/** Builds the full usage payload with a plausible prior-window total. */ -export function buildUsageSeriesResponse(): UsageSeriesResponse { - const points = buildUsageSeries(); - const currentTotal = points.reduce((sum, p) => sum + p.value, 0); - // The current window's series simulates ~12% growth over the prior one. - const priorTotal = Math.round(currentTotal / 1.12); - return { points, priorTotal }; -} - -export type ActivityKind = - | "pipeline-run" - | "deploy" - | "drift" - | "eval" - | "agent" - | "billing"; - -export interface ActivityEvent { - id: string; - kind: ActivityKind; - /** Short action verb shown at the top of the row. */ - action: string; - /** Subject of the action (pipeline / endpoint / agent name). */ - subject: string; - /** One-line detail line under the action. */ - detail: string; - /** Relative-time string. */ - time: string; - status: "success" | "warning" | "danger" | "info"; -} - -export const RECENT_ACTIVITY: ActivityEvent[] = [ - { - id: "act-1", - kind: "pipeline-run", - action: "Pipeline run completed", - subject: "COI Compliance", - detail: "1,287 docs · 0.4% errors · P95 412 ms", - time: "2m ago", - status: "success", - }, - { - id: "act-2", - kind: "deploy", - action: "Deployed", - subject: "Prior Auth v3.1.0", - detail: "Promoted to us-east-1, eu-west-1 · golden set 36/36", - time: "14m ago", - status: "success", - }, - { - id: "act-3", - kind: "drift", - action: "Schema drift detected", - subject: "Invoice v3", - detail: "12 docs in 1h didn't match — confidence ↓ 0.07", - time: "1h ago", - status: "warning", - }, - { - id: "act-4", - kind: "eval", - action: "Eval set passed", - subject: "KYC Processor", - detail: "94% (26/28) — 2 cases sent to review", - time: "3h ago", - status: "success", - }, - { - id: "act-5", - kind: "agent", - action: "Agent escalated", - subject: "Contract Router", - detail: "Low-confidence DPA routed to L2 reviewer pool", - time: "5h ago", - status: "info", - }, - { - id: "act-6", - kind: "pipeline-run", - action: "Pipeline run failed", - subject: "Contract Review", - detail: "8% error rate · 14 docs sent to review queue", - time: "8h ago", - status: "danger", - }, - { - id: "act-7", - kind: "billing", - action: "Approaching cap", - subject: "Monthly usage", - detail: "389k of 500k docs · auto-upgrade disabled", - time: "yesterday", - status: "warning", - }, - { - id: "act-8", - kind: "deploy", - action: "Rolled back", - subject: "COI Compliance v2.3.7", - detail: "Confidence regressions on Carrier supplement", - time: "2d ago", - status: "warning", - }, -]; - -/** - * KPI labels are owned by the client (see `KPI_LABELS_BY_TIER` in Home.tsx) - * because they're product copy that should stay stable across loading / empty - * / ready states. The API only ships values + deltas. - */ -export interface KpiEntry { - value: string | number; - delta?: number; - description?: string; - deltaDirection?: "up" | "down" | "flat"; -} - -export const FREE_KPIS: KpiEntry[] = [ - { value: "247 / 500" }, - { value: 189 }, - { value: 3 }, - { value: 1 }, -]; - -export function proKpisFor(docs30d: number): KpiEntry[] { - return [ - { value: docs30d.toLocaleString(), delta: 0.12 }, - { value: 12, delta: 0.16 }, - { value: 7, delta: 0.4 }, - { value: "94.6%", delta: 0.02 }, - ]; -} - -export function enterpriseKpisFor(docs30d: number): KpiEntry[] { - return [ - { value: docs30d.toLocaleString(), delta: 0.18 }, - { value: "412 ms", delta: -0.05 }, - { value: "96.2%", delta: 0.01 }, - { value: "99.987%" }, - ]; -} - -export interface RegionHealth { - name: string; - status: "healthy" | "degraded" | "down"; - meta: string; -} - -export const REGION_HEALTH: RegionHealth[] = [ - { - name: "us-east-1", - status: "healthy", - meta: "2.1k/min · P95 287ms · 99.99% uptime", - }, - { - name: "eu-west-1", - status: "healthy", - meta: "1.4k/min · P95 312ms · 99.98% uptime", - }, - { - name: "ap-southeast-1", - status: "degraded", - meta: "412/min · P95 521ms · 99.92% uptime · degraded", - }, -]; - -/** - * Starter pipelines offered by the Home fork wizard. Forking clones one of - * these templates as the seed for a new developer pipeline. Every template - * runs the same four canonical stages (Ingest → Validate → Secure → Store); - * the fixtures differ only in framing copy and which document types they target. - */ -export interface PipelineTemplate { - id: string; - /** Display name shown on the template chip and the ready-state header. */ - name: string; - /** One-line description of what the forked pipeline does. */ - blurb: string; - /** Document types this template is tuned for. */ - docTypes: string[]; - accent: "blue" | "purple" | "green" | "amber"; -} - -export const PIPELINE_TEMPLATES: PipelineTemplate[] = [ - { - id: "coi-compliance", - name: "COI Compliance", - blurb: "Validate certificates of insurance against carrier requirements.", - docTypes: ["Certificates of insurance", "Loss runs"], - accent: "blue", - }, - { - id: "accounts-payable", - name: "Accounts Payable", - blurb: "Extract line items, match POs, and flag duplicate invoices.", - docTypes: ["Invoices", "Purchase orders"], - accent: "green", - }, - { - id: "contract-review", - name: "Contract Review", - blurb: "Classify clauses, redact PII, and route to the right reviewer.", - docTypes: ["MSAs", "DPAs", "NDAs"], - accent: "purple", - }, - { - id: "prior-authorization", - name: "Prior Authorization", - blurb: "Read auth requests, check coverage, and assemble payer packets.", - docTypes: ["Auth requests", "Clinical notes"], - accent: "amber", - }, -]; - -/** - * The four canonical stages every forked pipeline runs. Fixed and ordered — - * the wizard's build animation lights them up left-to-right. - */ -export interface PipelineStage { - key: string; - label: string; - /** What the stage does, shown under the label in the ready-state grid. */ - detail: string; -} - -export const PIPELINE_STAGES: PipelineStage[] = [ - { key: "ingest", label: "Ingest", detail: "Accept, normalize, deduplicate" }, - { key: "validate", label: "Validate", detail: "Classify and check schema" }, - { key: "secure", label: "Secure", detail: "Redact PII, encrypt at rest" }, - { key: "store", label: "Store", detail: "Emit JSON, persist, notify" }, -]; - -// The free/subscribed "Finish setting up" checklist is no longer fixture-driven: -// SetupChecklist builds its steps client-side and derives completion from the -// live policies + sources APIs. See components/SetupChecklist.tsx. diff --git a/frontend/editor/src/portal/mocks/ops.ts b/frontend/editor/src/portal/mocks/ops.ts deleted file mode 100644 index fc1bd203d1..0000000000 --- a/frontend/editor/src/portal/mocks/ops.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Mock op catalogue + canned operation results for the single-op runner. - * Only api/ops.ts imports from this file. - */ - -export type OpResultMap = Record; - -export interface FeaturedOp { - id: string; - label: string; - endpoint: string; - accent: "blue" | "purple" | "green" | "amber" | "red"; - /** Shown in the picker — short single line. */ - blurb: string; -} - -export const FEATURED_OPS: FeaturedOp[] = [ - { - id: "extract", - label: "Extract", - endpoint: "/v1/extract", - accent: "blue", - blurb: "Pull structured fields into a typed schema", - }, - { - id: "redact", - label: "Redact PII", - endpoint: "/v1/redact", - accent: "red", - blurb: "Mask SSN, DOB, addresses, accounts before storage", - }, - { - id: "classify", - label: "Classify", - endpoint: "/v1/classify", - accent: "purple", - blurb: "Identify document type with a confidence score", - }, - { - id: "ocr", - label: "OCR", - endpoint: "/v1/ocr", - accent: "green", - blurb: "Text-recognize scanned or image pages", - }, - { - id: "validate", - label: "Schema validate", - endpoint: "/v1/validate", - accent: "blue", - blurb: "Check fields, rules, and coverage against the schema", - }, - { - id: "sign-output", - label: "Sign output", - endpoint: "/v1/sign", - accent: "green", - blurb: "Tamper-evident signature over artifact + run metadata", - }, - { - id: "authenticity", - label: "Authenticity", - endpoint: "/v1/authenticity", - accent: "blue", - blurb: "Verify issuer signature, watermark, and metadata", - }, - { - id: "tamper-check", - label: "Tamper check", - endpoint: "/v1/tamper-check", - accent: "amber", - blurb: "Detect modifications since signing or last-known-good state", - }, - { - id: "encrypt-rest", - label: "Encrypt at rest", - endpoint: "/v1/encrypt", - accent: "purple", - blurb: "AES-256 with Stirling-managed, BYOK, or HYOK keys", - }, - { - id: "smart-redact", - label: "Smart redact", - endpoint: "/v1/smart-redact", - accent: "red", - blurb: "Schema-aware redaction with confidence gating", - }, -]; - -/** Canned JSON for each featured op's runner "done" state. */ -export const OP_RESULTS: Record = { - extract: { - schema: "coi.v2", - fields: { - carrier: "Travelers Casualty", - policy_number: "PHB-1108-2025", - gl_limit: 1_000_000, - umbrella_limit: 5_000_000, - effective: "2026-01-15", - expiry: "2027-01-15", - }, - confidence_avg: 0.96, - }, - redact: { - redacted_pages: 4, - pii_types: ["SSN", "DOB", "ADDRESS", "EMAIL"], - occurrences: 19, - redaction_style: "blackout", - audit_id: "rdct_01HVQ7K3ZA9YJ8C", - }, - classify: { - schema: "invoice.v3", - confidence: 0.94, - alternatives: [ - { schema: "credit_memo.v1", confidence: 0.04 }, - { schema: "purchase_order.v2", confidence: 0.02 }, - ], - processing_ms: 287, - }, - ocr: { - pages: 12, - characters_recognized: 28471, - confidence_avg: 0.987, - languages_detected: ["en"], - processing_ms: 1840, - }, - validate: { - schema: "coi.v2", - passed: true, - checks_run: 14, - warnings: [ - { field: "additional_insured", message: "Optional field empty" }, - ], - }, - "sign-output": { - algorithm: "Ed25519", - key_id: "kx-prod-2026", - manifest_hash: - "0xb3f0c1a9d54fa1c0b8fd4eebd7fa11b1b16c9a3e2d2cc6f1f5a2f0a87e1b7a04", - }, - authenticity: { - verified: true, - issuer: "State of California DMV", - signed_at: "2025-11-04T17:22:00Z", - watermark_match: true, - }, - "tamper-check": { - tampered: false, - hash_match: true, - modifications_detected: 0, - last_known_good: "2026-04-22T09:14:00Z", - }, - "encrypt-rest": { - algorithm: "AES-256-GCM", - key_mode: "BYOK", - key_id: "arn:aws:kms:us-east-1:123:key/abc-…", - object_id: "obj_01HVQ7M9B2", - }, - "smart-redact": { - schema: "coi.v2", - redacted_fields: ["named_insured", "dob"], - occurrences: 7, - confidence_gate: 0.85, - gated_by_confidence: 1, - }, -}; diff --git a/frontend/editor/src/portal/views/Home.css b/frontend/editor/src/portal/views/Home.css index 7a4a1f5a37..70752c714b 100644 --- a/frontend/editor/src/portal/views/Home.css +++ b/frontend/editor/src/portal/views/Home.css @@ -24,43 +24,6 @@ color: var(--color-text-4); } -/* Product card grid */ -.portal-home__product-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 0.875rem; -} - -@media (max-width: 60rem) { - .portal-home__product-grid { - grid-template-columns: 1fr; - } -} - -.portal-home__product-head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; - margin-bottom: 0.5rem; -} - -.portal-home__product-title { - margin: 0; - font-size: 1.0625rem; - font-weight: 600; - color: var(--color-text-1); -} - -.portal-home__product-blurb { - margin: 0 0 1rem; - font-size: 0.8125rem; - line-height: 1.5; - color: var(--color-text-3); -} - -/* Metric strip now uses the shared . */ - /* Two-column dashboard row (activity + quick actions) */ .portal-home__row { display: grid; @@ -154,45 +117,3 @@ .portal-home__quick-row:hover .portal-home__quick-arrow { color: var(--color-blue); } - -/* Enterprise region strip */ -.portal-home__regions { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 0.5rem; -} - -@media (max-width: 50rem) { - .portal-home__regions { - grid-template-columns: 1fr; - } -} - -.portal-home__region { - padding: 0.625rem 0.75rem; - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-md); -} - -.portal-home__region-name { - display: flex; - align-items: center; - gap: 0.375rem; - font-size: 0.8125rem; - font-weight: 500; - color: var(--color-text-1); -} - -.portal-home__region-dot { - width: 0.4375rem; - height: 0.4375rem; - border-radius: 50%; -} - -.portal-home__region-meta { - margin-top: 0.25rem; - font-size: 0.6875rem; - color: var(--color-text-4); - font-family: var(--font-mono); -} diff --git a/frontend/editor/src/portal/views/Home.stories.tsx b/frontend/editor/src/portal/views/Home.stories.tsx index feed3f25da..92d3e2ae2f 100644 --- a/frontend/editor/src/portal/views/Home.stories.tsx +++ b/frontend/editor/src/portal/views/Home.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { http, HttpResponse, delay } from "msw"; +import { http, HttpResponse } from "msw"; import { Home } from "@portal/views/Home"; const meta: Meta = { @@ -39,17 +39,3 @@ export const SubscribedInProcurement: Story = { }, }, }; - -export const SlowUsage: Story = { - globals: { tier: "pro" }, - parameters: { - msw: { - handlers: [ - http.get("/v1/analytics/usage", async () => { - await delay(3000); - return HttpResponse.json({ points: [], priorTotal: 0 }); - }), - ], - }, - }, -}; diff --git a/frontend/editor/src/portal/views/Home.tsx b/frontend/editor/src/portal/views/Home.tsx index ff247271c6..b9a42fbab8 100644 --- a/frontend/editor/src/portal/views/Home.tsx +++ b/frontend/editor/src/portal/views/Home.tsx @@ -1,123 +1,19 @@ -import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { - Button, - Card, - EmptyState, - MetricCard, - MetricStrip, - Skeleton, - StatusBadge, -} from "@app/ui"; -import { useTier, type Tier } from "@portal/contexts/TierContext"; -import { useView, type ViewId } from "@portal/contexts/ViewContext"; -import { useAsync, useSectionFlags } from "@portal/hooks/useAsync"; -import { - fetchHomeKpis, - fetchRegionHealth, - fetchUsageSeries, - type KpiEntry, - type RegionHealth, - type UsageSeriesResponse, -} from "@portal/api/home"; +import { Button, Card } from "@app/ui"; +import { useTier } from "@portal/contexts/TierContext"; +import { useView } from "@portal/contexts/ViewContext"; import { HomeHero } from "@portal/components/HomeHero"; import { HomeGreeting } from "@portal/components/HomeGreeting"; -import { PopularUseCases } from "@portal/components/PopularUseCases"; -import { UsageAreaChart } from "@portal/components/UsageAreaChart"; import { RecentActivity } from "@portal/components/RecentActivity"; -import { SingleOpRunner } from "@portal/components/SingleOpRunner"; import { ProcessingStatusStrip } from "@portal/components/ProcessingStatusStrip"; import { PolicySummary } from "@portal/components/PolicySummary"; -import { PipelineForkWizard } from "@portal/components/PipelineForkWizard"; import "@portal/views/Home.css"; -/* ──────────────────────────────────────────────────────────────────────── */ -/* Product cards (Sources / Pipelines / Agents) */ -/* ──────────────────────────────────────────────────────────────────────── */ - -interface ProductCardProps { - accent: "default" | "premium"; - badge?: string; - title: string; - blurb: string; - cta: string; - target: ViewId; -} - -function ProductCard({ - accent, - badge, - title, - blurb, - cta, - target, -}: ProductCardProps) { - const { setActiveView } = useView(); - return ( - -
-

{title}

- {badge && ( - - {badge} - - )} -
-

{blurb}

- -
- ); -} - -function ProductGrid() { - const { t } = useTranslation(); - return ( -
- - - -
- ); -} - /* ──────────────────────────────────────────────────────────────────────── */ /* Quick actions card */ /* ──────────────────────────────────────────────────────────────────────── */ -function QuickActions({ onTryOp }: { onTryOp: () => void }) { +function QuickActions() { const { t } = useTranslation(); const { setActiveView } = useView(); return ( @@ -131,30 +27,6 @@ function QuickActions({ onTryOp }: { onTryOp: () => void }) {
-