Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.es.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
43 changes: 36 additions & 7 deletions frontend/src/app/fleet/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 & {
Expand Down Expand Up @@ -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 (
<AppShell>
<FleetHero
Expand All @@ -355,42 +368,58 @@ export default function FleetPage() {
background202={background}
onBackground202Change={setBackground}
userId={identity?.user_id}
onOpenShortcuts={() => setCheatsheetOpen(true)}
/>

<main className="mx-auto w-full max-w-[1120px] px-4 py-6 md:px-6 md:py-8">
{error && (
<p className="mb-4 rounded-lg border border-[#f0c7c3] bg-[#fbefee] px-4 py-3 text-sm text-[#9b2c2c]">
<p className="vf-no-print mb-4 rounded-lg border border-[#f0c7c3] bg-[#fbefee] px-4 py-3 text-sm text-[#9b2c2c]">
{error.kind === "key"
? t(error.key, error.vars)
: formatApiError(error.err, locale) || t(error.fallback)}
</p>
)}
{role === "auditor" && (
<p className="mb-4 rounded-lg border border-[#f3d5b0] bg-[#fbf3e8] px-4 py-3 text-sm text-[#9a4d09]">
<p className="vf-no-print mb-4 rounded-lg border border-[#f3d5b0] bg-[#fbf3e8] px-4 py-3 text-sm text-[#9a4d09]">
{t("auditor.banner", { tools: (identity?.denied_tools || []).join(", ") || "invoice.sign" })}
</p>
)}

<div className="flex flex-col gap-6">
<ResultCard run={run} background202={background} />
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_380px]">
<div className="flex flex-col gap-6">
<div className="vf-fleet-grid grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_380px]">
<div className="vf-no-print flex flex-col gap-6">
<FixtureGrid busy={busy} activeJob={activeJob} onDispatch={loadFixture} />
<IngestPanel busy={busy} activeJob={activeJob} onUpload={ingestPdf} onSweep={runSweep} />
</div>
<aside className="flex flex-col gap-4">
<RecentRuns runs={history} live={run} />
<div className="vf-no-print flex flex-col gap-4">
<RecentRuns runs={history} live={run} />
<AgentRoster agents={registry?.agents || null} compact />
<ReplayHint />
</div>
<FleetChecklist
track={checklist?.track}
items={checklist?.items || []}
aeatRemitting={checklist?.aeat_remitting}
/>
<AgentRoster agents={registry?.agents || null} compact />
<ReplayHint />
<div className="vf-print-only">
<h2 className="mb-2 text-[15px] font-medium tracking-tight text-[#111]">
{t("print.identity")}
</h2>
<AgentIdentity
tenant={tenant}
role={role}
userId={identity?.user_id}
allowed={identity?.allowed_tools || []}
denied={identity?.denied_tools || []}
/>
</div>
</aside>
</div>
</div>
</main>
<ShortcutCheatsheet open={cheatsheetOpen} onClose={() => setCheatsheetOpen(false)} />
</AppShell>
);
}
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
16 changes: 15 additions & 1 deletion frontend/src/components/fleet/control-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export function ControlBar({
background202,
onBackground202Change,
userId,
onOpenShortcuts,
}: {
tenant: string;
onTenantChange: (tenant: string) => void;
Expand All @@ -42,11 +43,12 @@ export function ControlBar({
background202: boolean;
onBackground202Change: (value: boolean) => void;
userId?: string;
onOpenShortcuts?: () => void;
}) {
const { t } = useLocale();

return (
<section aria-label={t("control.aria")} className="border-b border-[#e8e6e3]">
<section aria-label={t("control.aria")} className="vf-no-print border-b border-[#e8e6e3]">
<div className="mx-auto flex w-full max-w-[1120px] flex-wrap items-center gap-2 px-4 py-3 md:px-6">
<div className="vf-chip min-h-11 min-w-0 md:min-h-8">
<span>{t("control.tenant")}</span>
Expand Down Expand Up @@ -107,6 +109,18 @@ export function ControlBar({
</span>
</span>
</label>

{onOpenShortcuts ? (
<button
type="button"
onClick={onOpenShortcuts}
aria-label={t("shortcuts.aria")}
className="vf-chip min-h-11 cursor-pointer md:min-h-8"
>
<kbd className="font-mono text-[12px] text-[#111]">?</kbd>
<span className="text-[#111]">{t("shortcuts.open")}</span>
</button>
) : null}
</div>
</section>
);
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/components/fleet/fixture-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function FixtureGrid({
<h2 className="text-[15px] font-medium tracking-tight text-[#111]">{t("fixtures.heading")}</h2>

<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{FIXTURES.map((fixture) => {
{FIXTURES.map((fixture, index) => {
const Icon = fixture.icon;
const active = busy && activeJob === fixture.path;
return (
Expand All @@ -77,7 +77,12 @@ export function FixtureGrid({
<h3 className="mt-1 text-[15px] leading-tight font-medium text-[#111]">
{t(fixture.labelKey)}
</h3>
<p className="mt-1 text-[12px] text-[#6f6e69]">{fixture.expect}</p>
<p className="mt-1 text-[12px] text-[#6f6e69]">
{fixture.expect}
<kbd className="ml-2 rounded border border-[#e8e6e3] bg-[#fbfbf9] px-1.5 font-mono text-[11px] text-[#6f6e69]">
{index + 1}
</kbd>
</p>
</div>
{active ? (
<Loader2 className="size-4 shrink-0 animate-spin text-[#6f6e69]" />
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/fleet/ingest-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -32,6 +33,7 @@ export function IngestPanel({
return (
<section aria-label={t("ingest.aria")} className="grid grid-cols-1 gap-3 sm:grid-cols-[1fr_minmax(0,200px)]">
<div
id={FLEET_DROPZONE_ID}
role="button"
tabIndex={0}
aria-label={t("ingest.uploadAria")}
Expand Down
22 changes: 20 additions & 2 deletions frontend/src/components/fleet/recent-runs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Loader2 } from "lucide-react";
import { asVerdict } from "./verdict";
import { shortHash, VerdictPill } from "./verdict-pill";
import { useLocale } from "@/components/i18n/locale-provider";
import { downloadTextFile, runsToCsv } from "@/lib/fleet-csv";
import { Button } from "@/components/ui/button";

export type FleetRunView = {
run_id: string;
Expand Down Expand Up @@ -32,11 +34,27 @@ export function RecentRuns({
const rows = live && !runs.some((run) => run.run_id === live.run_id) ? [live, ...runs] : runs;
const visible = rows.slice(0, 12);

function exportCsv() {
downloadTextFile("verifleet-runs.csv", runsToCsv(rows));
}

return (
<section aria-label={t("recent.aria")} className="vf-card overflow-hidden rounded-lg">
<header className="flex items-center justify-between border-b border-[#e8e6e3] px-4 py-3">
<header className="flex flex-wrap items-center justify-between gap-2 border-b border-[#e8e6e3] px-4 py-3">
<h2 className="text-[15px] font-medium tracking-tight text-[#111]">{t("recent.title")}</h2>
<span className="text-[12px] text-[#6f6e69]">{t("recent.inSession", { n: runs.length })}</span>
<div className="flex min-w-0 items-center gap-2">
<span className="text-[12px] text-[#6f6e69]">{t("recent.inSession", { n: runs.length })}</span>
<Button
type="button"
variant="outline"
size="sm"
disabled={rows.length === 0}
onClick={exportCsv}
className="h-8 rounded-md border-[#e8e6e3] bg-white px-2.5 text-[12px] text-[#111] hover:bg-[#f4f3f0]"
>
{t("recent.exportCsv")}
</Button>
</div>
</header>

{visible.length === 0 ? (
Expand Down
72 changes: 72 additions & 0 deletions frontend/src/components/fleet/shortcut-cheatsheet.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement>(null);

useEffect(() => {
if (!open) return;
closeRef.current?.focus();
}, [open]);

if (!open) return null;

return (
<div
className="vf-no-print fixed inset-0 z-50 flex items-end justify-center bg-[#111111]/40 p-4 sm:items-center"
onClick={onClose}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="fleet-shortcuts-title"
className="w-full max-w-[390px] rounded-lg border border-[#e8e6e3] bg-white p-4 shadow-[0_8px_24px_rgba(17,17,17,0.08)]"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-start justify-between gap-3">
<h2 id="fleet-shortcuts-title" className="text-[15px] font-medium tracking-tight text-[#111]">
{t("shortcuts.title")}
</h2>
<button
ref={closeRef}
type="button"
onClick={onClose}
className="min-h-11 min-w-11 rounded-md text-[13px] text-[#6f6e69] hover:text-[#111] sm:min-h-8 sm:min-w-8"
>
{t("shortcuts.close")}
</button>
</div>
<ul className="mt-3 divide-y divide-[#e8e6e3]">
{ROWS.map((row) => (
<li key={row.key} className="flex items-center justify-between gap-3 py-2.5">
<span className="text-[13px] text-[#111]">{t(row.label)}</span>
<kbd className="rounded border border-[#e8e6e3] bg-[#fbfbf9] px-1.5 py-0.5 font-mono text-[12px] text-[#6f6e69]">
{row.key}
</kbd>
</li>
))}
</ul>
</div>
</div>
);
}
9 changes: 6 additions & 3 deletions frontend/src/components/shell/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,14 @@ export function AppShell({

return (
<div
className="vf-app min-h-screen bg-[#fbfbf9] text-[#111]"
className={cn(
"vf-app min-h-screen bg-[#fbfbf9] text-[#111]",
path.startsWith("/fleet") && "vf-fleet-page"
)}
style={{ backgroundColor: "#fbfbf9", color: "#111111" }}
>
<header
className="sticky top-0 z-30 border-b border-[#e8e6e3] bg-[#fbfbf9]"
className="vf-app-nav sticky top-0 z-30 border-b border-[#e8e6e3] bg-[#fbfbf9]"
style={{ backgroundColor: "#fbfbf9" }}
>
<div className="mx-auto flex w-full max-w-[1120px] items-center gap-2 px-4 py-3 md:gap-4 md:px-6">
Expand Down Expand Up @@ -180,7 +183,7 @@ export function AppShell({
{right ? <div className="mx-auto hidden max-w-[1120px] px-4 pb-3 md:block md:px-6">{right}</div> : null}
</header>
{children}
<footer className="border-t border-[#e8e6e3] px-4 py-6 md:px-6">
<footer className="vf-app-footer border-t border-[#e8e6e3] px-4 py-6 md:px-6">
<div className="mx-auto flex w-full max-w-[1120px] flex-col gap-4">
<nav aria-label={t("nav.aria")} className="flex flex-wrap gap-x-4 gap-y-2">
{footerLinks.map((item) => (
Expand Down
Loading