From 648e60175dcf98c69bebe3d4c8ac8f95fc96e534 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 13 Aug 2026 20:51:07 +0000 Subject: [PATCH] feat(frontend): judge shortcuts, CSV export, and print sheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add /fleet keyboard shortcuts (1–4, u, g, ?), client-side CSV export of already-fetched runs, and a print stylesheet for the judge sheet. Document python -m verifleet ingest --help without duplicating the CLI. Co-authored-by: AlonsoAlviraa --- README.es.md | 4 + README.md | 4 +- frontend/src/app/fleet/page.tsx | 43 +++++++++-- frontend/src/app/globals.css | 35 +++++++++ frontend/src/components/fleet/control-bar.tsx | 16 +++- .../src/components/fleet/fixture-grid.tsx | 9 ++- .../src/components/fleet/ingest-panel.tsx | 2 + frontend/src/components/fleet/recent-runs.tsx | 22 +++++- .../components/fleet/shortcut-cheatsheet.tsx | 72 ++++++++++++++++++ frontend/src/components/shell/app-shell.tsx | 9 ++- frontend/src/hooks/use-fleet-shortcuts.ts | 76 +++++++++++++++++++ frontend/src/lib/fleet-csv.ts | 48 ++++++++++++ frontend/src/lib/i18n.ts | 30 ++++++++ 13 files changed, 354 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/fleet/shortcut-cheatsheet.tsx create mode 100644 frontend/src/hooks/use-fleet-shortcuts.ts create mode 100644 frontend/src/lib/fleet-csv.ts diff --git a/README.es.md b/README.es.md index d7390fb..1738403 100644 --- a/README.es.md +++ b/README.es.md @@ -15,7 +15,11 @@ cd frontend && npm install && npm run dev UI del concurso: http://localhost:3000/fleet ```bash +python -m verifleet ingest --help python -m verifleet ingest frontend/public/demo-fixtures/valid_invoice.json +python -m verifleet ingest path/to/factura.pdf --tenant enterprise-demo --role issuer ``` +`python -m verifleet ingest --help` muestra path, `--tenant` y `--role`. Misma ingesta local que `/fleet`. Imprime `SIGNED` y un hash acortado (`first8…last8`). Nunca imprime API keys. + Disclosure y credenciales de juez: `CONTEST.md` diff --git a/README.md b/README.md index 0135b60..2a0418e 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,12 @@ Open **http://127.0.0.1:3000/fleet** — that is the operator console. Landing ( Browser `/api/v1` is proxied by `frontend/src/app/api/v1/[...path]/route.ts` (not a `next.config` rewrite). After pulling this, **restart `npm run dev`** so the rewrite is gone, and **recycle uvicorn** so the FIFO + list-drain code loads. ```bash +python -m verifleet ingest --help python -m verifleet ingest frontend/public/demo-fixtures/valid_invoice.json +python -m verifleet ingest path/to/factura.pdf --tenant enterprise-demo --role issuer ``` -Prints `SIGNED` and a shortened hash (`first8…last8`). Same local ingest as `/fleet`. Never prints API keys. +`python -m verifleet ingest --help` lists path, `--tenant`, and `--role`. A JSON fixture or PDF runs the same local ingest as `/fleet`. Prints `SIGNED` and a shortened hash (`first8…last8`). Never prints API keys. This repo does **not** ship a hosted Cloud Run URL. Deploy is a human step (`infra/README.md`) after you have a billed project. diff --git a/frontend/src/app/fleet/page.tsx b/frontend/src/app/fleet/page.tsx index a9823d6..acacb2c 100644 --- a/frontend/src/app/fleet/page.tsx +++ b/frontend/src/app/fleet/page.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import apiClient, { TENANT_STORAGE_KEY, formatApiError } from "@/lib/api-client"; import { AppShell } from "@/components/shell/app-shell"; +import { AgentIdentity } from "@/components/fleet/agent-identity"; import { ControlBar } from "@/components/fleet/control-bar"; import { FixtureGrid } from "@/components/fleet/fixture-grid"; import { AgentRoster } from "@/components/fleet/agent-roster"; @@ -12,7 +13,9 @@ import { IngestPanel } from "@/components/fleet/ingest-panel"; import { RecentRuns, type FleetRunView } from "@/components/fleet/recent-runs"; import { ReplayHint } from "@/components/fleet/replay-hint"; import { ResultCard } from "@/components/fleet/result-card"; +import { ShortcutCheatsheet } from "@/components/fleet/shortcut-cheatsheet"; import { useLocale } from "@/components/i18n/locale-provider"; +import { useFleetShortcuts } from "@/hooks/use-fleet-shortcuts"; import { JUDGE_BANNER, type MessageKey } from "@/lib/i18n"; type FleetRun = FleetRunView & { @@ -339,6 +342,16 @@ export default function FleetPage() { }; }, [history]); + const toggleBackground = useCallback(() => { + setBackground((value) => !value); + }, []); + + const { cheatsheetOpen, setCheatsheetOpen } = useFleetShortcuts({ + busy, + onDispatch: loadFixture, + onToggleBackground: toggleBackground, + }); + return ( setCheatsheetOpen(true)} />
{error && ( -

+

{error.kind === "key" ? t(error.key, error.vars) : formatApiError(error.err, locale) || t(error.fallback)}

)} {role === "auditor" && ( -

+

{t("auditor.banner", { tools: (identity?.denied_tools || []).join(", ") || "invoice.sign" })}

)}
-
-
+
+
+ setCheatsheetOpen(false)} />
); } diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index d931c21..e7fa60c 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -208,3 +208,38 @@ body { transform: none !important; } } + +.vf-print-only { + display: none; +} + +@media print { + @page { + margin: 12mm; + } + + .vf-print-only { + display: block !important; + } + + .vf-fleet-page .vf-app-nav, + .vf-fleet-page .vf-app-footer, + .vf-no-print { + display: none !important; + } + + .vf-fleet-page { + background: #ffffff !important; + color: #111111; + } + + .vf-fleet-grid { + display: block !important; + } + + .vf-card, + [data-slot="card"] { + break-inside: avoid; + box-shadow: none; + } +} diff --git a/frontend/src/components/fleet/control-bar.tsx b/frontend/src/components/fleet/control-bar.tsx index 32a5024..39742ad 100644 --- a/frontend/src/components/fleet/control-bar.tsx +++ b/frontend/src/components/fleet/control-bar.tsx @@ -34,6 +34,7 @@ export function ControlBar({ background202, onBackground202Change, userId, + onOpenShortcuts, }: { tenant: string; onTenantChange: (tenant: string) => void; @@ -42,11 +43,12 @@ export function ControlBar({ background202: boolean; onBackground202Change: (value: boolean) => void; userId?: string; + onOpenShortcuts?: () => void; }) { const { t } = useLocale(); return ( -
+
{t("control.tenant")} @@ -107,6 +109,18 @@ export function ControlBar({ + + {onOpenShortcuts ? ( + + ) : null}
); diff --git a/frontend/src/components/fleet/fixture-grid.tsx b/frontend/src/components/fleet/fixture-grid.tsx index 99ae67d..bd314d1 100644 --- a/frontend/src/components/fleet/fixture-grid.tsx +++ b/frontend/src/components/fleet/fixture-grid.tsx @@ -62,7 +62,7 @@ export function FixtureGrid({

{t("fixtures.heading")}

- {FIXTURES.map((fixture) => { + {FIXTURES.map((fixture, index) => { const Icon = fixture.icon; const active = busy && activeJob === fixture.path; return ( @@ -77,7 +77,12 @@ export function FixtureGrid({

{t(fixture.labelKey)}

-

{fixture.expect}

+

+ {fixture.expect} + + {index + 1} + +

{active ? ( diff --git a/frontend/src/components/fleet/ingest-panel.tsx b/frontend/src/components/fleet/ingest-panel.tsx index 65e5b1b..c757b68 100644 --- a/frontend/src/components/fleet/ingest-panel.tsx +++ b/frontend/src/components/fleet/ingest-panel.tsx @@ -5,6 +5,7 @@ import { Layers, UploadCloud } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { useLocale } from "@/components/i18n/locale-provider"; +import { FLEET_DROPZONE_ID } from "@/hooks/use-fleet-shortcuts"; export function IngestPanel({ busy, @@ -32,6 +33,7 @@ export function IngestPanel({ return (
run.run_id === live.run_id) ? [live, ...runs] : runs; const visible = rows.slice(0, 12); + function exportCsv() { + downloadTextFile("verifleet-runs.csv", runsToCsv(rows)); + } + return (
-
+

{t("recent.title")}

- {t("recent.inSession", { n: runs.length })} +
+ {t("recent.inSession", { n: runs.length })} + +
{visible.length === 0 ? ( diff --git a/frontend/src/components/fleet/shortcut-cheatsheet.tsx b/frontend/src/components/fleet/shortcut-cheatsheet.tsx new file mode 100644 index 0000000..ef7d01e --- /dev/null +++ b/frontend/src/components/fleet/shortcut-cheatsheet.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { useLocale } from "@/components/i18n/locale-provider"; +import type { MessageKey } from "@/lib/i18n"; + +const ROWS: { key: string; label: MessageKey }[] = [ + { key: "1", label: "shortcuts.1" }, + { key: "2", label: "shortcuts.2" }, + { key: "3", label: "shortcuts.3" }, + { key: "4", label: "shortcuts.4" }, + { key: "u", label: "shortcuts.u" }, + { key: "g", label: "shortcuts.g" }, + { key: "?", label: "shortcuts.help" }, +]; + +export function ShortcutCheatsheet({ + open, + onClose, +}: { + open: boolean; + onClose: () => void; +}) { + const { t } = useLocale(); + const closeRef = useRef(null); + + useEffect(() => { + if (!open) return; + closeRef.current?.focus(); + }, [open]); + + if (!open) return null; + + return ( +
+
event.stopPropagation()} + > +
+

+ {t("shortcuts.title")} +

+ +
+
    + {ROWS.map((row) => ( +
  • + {t(row.label)} + + {row.key} + +
  • + ))} +
+
+
+ ); +} diff --git a/frontend/src/components/shell/app-shell.tsx b/frontend/src/components/shell/app-shell.tsx index e083dd7..d350c78 100644 --- a/frontend/src/components/shell/app-shell.tsx +++ b/frontend/src/components/shell/app-shell.tsx @@ -135,11 +135,14 @@ export function AppShell({ return (
@@ -180,7 +183,7 @@ export function AppShell({ {right ?
{right}
: null}
{children} -