Skip to content

Commit 7b8f1de

Browse files
committed
Merge branch 'retrofit/dylanmerigaud-config'
# Conflicts: # app/page.tsx
2 parents 3f245dd + 7a2ba88 commit 7b8f1de

150 files changed

Lines changed: 3350 additions & 4524 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.husky/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pnpm lint-staged

app/api/pdf/[id]/route.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const runtime = "nodejs";
2323

2424
export const GET = async (
2525
_request: Request,
26-
{ params }: { params: Promise<{ id: string }> },
26+
{ params }: { params: Promise<{ id: string }> }
2727
): Promise<Response> => {
2828
const { id } = await params;
2929

@@ -38,10 +38,10 @@ export const GET = async (
3838
}
3939

4040
const bytes = await renderInvoicePdf(invoice);
41-
// Copy into a fresh ArrayBuffer so the body is a plain BodyInit (not a typed
42-
// array view over a possibly-larger buffer).
43-
const body = bytes.slice().buffer;
44-
return new Response(body, {
41+
// Copy into a fresh, exactly-sized ArrayBuffer so the body is a plain BodyInit
42+
// (not a typed-array view over a possibly-larger backing buffer).
43+
const copy = new Uint8Array(bytes);
44+
return new Response(copy.buffer, {
4545
status: 200,
4646
headers: {
4747
"content-type": "application/pdf",

app/api/reset/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ export const GET = async (request: Request): Promise<Response> => {
3434
const counts = await resetAndReseed(getDb());
3535
log.info("nightly reset complete", counts);
3636
return Response.json({ ok: true, ...counts });
37-
} catch (err) {
37+
} catch (error) {
3838
log.error("nightly reset failed", {
39-
error: err instanceof Error ? err.message : String(err),
39+
error: error instanceof Error ? error.message : String(error),
4040
});
4141
return new Response("Reset failed.", { status: 500 });
4242
}

app/dev/graph-cases/page.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33
import { useEffect, useState } from "react";
44

55
import { WorkflowGraph } from "@/components/workflow-graph";
6-
import { resolvePath, type InvoiceContext } from "@/lib/approval-workflow";
7-
import {
8-
DEFAULT_APPROVAL_POLICY,
9-
workflowFromPolicy,
10-
} from "@/lib/client-profile";
6+
import { type InvoiceContext, resolvePath } from "@/lib/approval-workflow";
7+
import { DEFAULT_APPROVAL_POLICY, workflowFromPolicy } from "@/lib/client-profile";
118

129
/**
1310
* A deterministic render harness for the graph LAYOUT, no model calls, so the e2e
@@ -41,9 +38,8 @@ const LinearLit = () => {
4138
const [statuses, setStatuses] = useState<Record<string, string>>({});
4239
useEffect(() => {
4340
const t = setTimeout(
44-
() =>
45-
setStatuses({ "manager-review": "approved", "post-netsuite": "done" }),
46-
1200,
41+
() => setStatuses({ "manager-review": "approved", "post-netsuite": "done" }),
42+
1200
4743
);
4844
return () => clearTimeout(t);
4945
}, []);

app/layout.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import type { Metadata } from "next";
2+
13
import { GeistMono } from "geist/font/mono";
24
import { GeistSans } from "geist/font/sans";
3-
import type { Metadata } from "next";
45
import { Toaster } from "sonner";
5-
import "@/app/globals.css";
66

7+
import "@/app/globals.css";
78
import { QueryProvider } from "@/components/query-provider";
89

910
const SITE_URL = "https://ledgerloop-eta.vercel.app";
@@ -37,11 +38,7 @@ export const metadata: Metadata = {
3738
},
3839
};
3940

40-
export default function RootLayout({
41-
children,
42-
}: {
43-
children: React.ReactNode;
44-
}) {
41+
export default function RootLayout({ children }: { children: React.ReactNode }) {
4542
return (
4643
<html lang="en" className={`${GeistSans.variable} ${GeistMono.variable}`}>
4744
<body className="min-h-full font-sans">

app/opengraph-image.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,22 +90,17 @@ export default function OpengraphImage() {
9090
>
9191
An onboarding agent that derives the approval workflow
9292
</div>
93-
<div
94-
style={{ fontSize: 27, color: MUTED, lineHeight: 1.4, maxWidth: 940 }}
95-
>
96-
It reads a client&apos;s HRIS, resolves who signs off on what to real
97-
people, and runs procure-to-pay against the workflow it builds. Live
98-
execution trace, with a human in the loop before anything posts.
93+
<div style={{ fontSize: 27, color: MUTED, lineHeight: 1.4, maxWidth: 940 }}>
94+
It reads a client&apos;s HRIS, resolves who signs off on what to real people, and runs
95+
procure-to-pay against the workflow it builds. Live execution trace, with a human in the
96+
loop before anything posts.
9997
</div>
10098
</div>
10199

102100
{/* Stage flow */}
103101
<div style={{ display: "flex", alignItems: "center", gap: 14 }}>
104102
{stages.map((label, i) => (
105-
<div
106-
key={label}
107-
style={{ display: "flex", alignItems: "center", gap: 14 }}
108-
>
103+
<div key={label} style={{ display: "flex", alignItems: "center", gap: 14 }}>
109104
<div
110105
style={{
111106
display: "flex",
@@ -137,9 +132,7 @@ export default function OpengraphImage() {
137132
</div>
138133
{label}
139134
</div>
140-
{i < stages.length - 1 && (
141-
<div style={{ fontSize: 26, color: LINE }}></div>
142-
)}
135+
{i < stages.length - 1 && <div style={{ fontSize: 26, color: LINE }}></div>}
143136
</div>
144137
))}
145138
</div>
@@ -160,6 +153,6 @@ export default function OpengraphImage() {
160153
<div style={{ color: INK }}>Dylan Mérigaud</div>
161154
</div>
162155
</div>,
163-
{ ...size },
156+
{ ...size }
164157
);
165158
}

app/page.tsx

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ export default async function Page() {
2121
let dbError: string | null = null;
2222
try {
2323
queue = await listInvoiceQueue();
24-
} catch (err) {
24+
} catch (error) {
2525
dbError =
26-
err instanceof Error && err.message.includes("DATABASE_URL")
26+
error instanceof Error && error.message.includes("DATABASE_URL")
2727
? "DATABASE_URL is not set."
2828
: "Could not reach the database.";
2929
}
@@ -65,10 +65,8 @@ const Header = () => {
6565
</div>
6666
<p className="mt-2 max-w-xl text-[13.5px] leading-snug text-muted">
6767
Reads a client&apos;s HRIS,{" "}
68-
<span className="font-medium text-ink">
69-
derives their approval workflow
70-
</span>
71-
, and runs procure-to-pay against it.
68+
<span className="font-medium text-ink">derives their approval workflow</span>, and runs
69+
procure-to-pay against it.
7270
</p>
7371
</div>
7472
<div className="flex shrink-0 items-center gap-2">
@@ -99,45 +97,34 @@ const HowItWorks = () => {
9997
<details className="group relative">
10098
<summary className="flex cursor-pointer list-none items-center gap-1.5 rounded-full bg-subtle px-3 py-1.5 text-[12px] font-medium text-muted ring-1 ring-inset ring-line-strong transition-colors hover:text-ink">
10199
How it works
102-
<span
103-
aria-hidden
104-
className="text-faint transition-transform group-open:rotate-180"
105-
>
100+
<span aria-hidden className="text-faint transition-transform group-open:rotate-180">
106101
107102
</span>
108103
</summary>
109104
<div className="absolute right-0 z-20 mt-2 w-[300px] rounded-xl bg-surface p-3 text-[12px] leading-snug text-muted shadow-lift ring-1 ring-inset ring-line">
110105
<dl className="space-y-1.5">
111106
<Term name="Gate">
112-
an approval step that fires on a condition (amount, variance, …) and
113-
pauses for a human.
107+
an approval step that fires on a condition (amount, variance, …) and pauses for a human.
114108
</Term>
115109
<Term name="2-way / 3-way match">
116110
invoice ↔ PO, or invoice ↔ PO ↔ goods receipt (did we receive it?).
117111
</Term>
118112
<Term name="Verdict">
119113
<span className="text-ink">clean</span> (reconciles),{" "}
120-
<span className="text-ink">exception</span> (a variance needs a
121-
decision), <span className="text-ink">duplicate</span> (blocked,
122-
never paid twice).
114+
<span className="text-ink">exception</span> (a variance needs a decision),{" "}
115+
<span className="text-ink">duplicate</span> (blocked, never paid twice).
123116
</Term>
124117
<Term name="Investigator">
125-
an AI agent that reads messy records on an exception and recommends.
126-
A human still decides.
118+
an AI agent that reads messy records on an exception and recommends. A human still
119+
decides.
127120
</Term>
128121
</dl>
129122
</div>
130123
</details>
131124
);
132125
};
133126

134-
const Term = ({
135-
name,
136-
children,
137-
}: {
138-
name: string;
139-
children: React.ReactNode;
140-
}) => {
127+
const Term = ({ name, children }: { name: string; children: React.ReactNode }) => {
141128
return (
142129
<div>
143130
<dt className="inline font-medium text-ink">{name}: </dt>
@@ -150,10 +137,7 @@ const Term = ({
150137
glyph (matches app/icon.svg). The second stroke is the accent. */
151138
const LogoMark = () => {
152139
return (
153-
<div
154-
aria-hidden
155-
className="grid size-9 shrink-0 place-items-center rounded-[10px] bg-ink"
156-
>
140+
<div aria-hidden className="grid size-9 shrink-0 place-items-center rounded-[10px] bg-ink">
157141
<svg viewBox="0 0 32 32" className="size-[19px]" fill="none" aria-hidden>
158142
<path
159143
d="M12 7v12a4 4 0 0 0 4 4"
@@ -177,9 +161,7 @@ const LogoMark = () => {
177161
const SetupNotice = ({ detail }: { detail: string }) => {
178162
return (
179163
<div className="rounded-2xl bg-warn-soft/60 px-6 py-5 text-[13px] text-ink ring-1 ring-inset ring-warn-line/60">
180-
<p className="font-semibold">
181-
Almost there. The demo needs its database.
182-
</p>
164+
<p className="font-semibold">Almost there. The demo needs its database.</p>
183165
<p className="mt-1 text-muted">{detail}</p>
184166
<p className="mt-2.5 text-muted">
185167
Set{" "}
@@ -204,10 +186,9 @@ const Footer = () => {
204186
return (
205187
<footer className="mt-7 flex flex-wrap items-center justify-between gap-3 border-t border-line pt-4 text-[12px] text-faint">
206188
<p>
207-
Matching and reconciliation run in code. A{" "}
208-
<span className="text-muted">Mastra</span> agent on{" "}
209-
<span className="font-mono text-muted">{PIPELINE_MODEL}</span>{" "}
210-
investigates only the exceptions, all on the small model.
189+
Matching and reconciliation run in code. A <span className="text-muted">Mastra</span> agent
190+
on <span className="font-mono text-muted">{PIPELINE_MODEL}</span> investigates only the
191+
exceptions, all on the small model.
211192
</p>
212193
<div className="flex items-center gap-2">
213194
<span className="font-medium text-muted">Dylan Mérigaud</span>

components/app-view.tsx

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import { useSearchParams } from "next/navigation";
44
import { useState } from "react";
55

6-
import { Dashboard } from "@/components/dashboard";
7-
import { Onboarding } from "@/components/onboarding";
86
import type { QueueItem } from "@/db/client";
97
import type { ApprovalWorkflow } from "@/lib/approval-workflow";
108

9+
import { Dashboard } from "@/components/dashboard";
10+
import { Onboarding } from "@/components/onboarding";
11+
1112
/**
1213
* The top-level view switch. Two halves of the product, in the order a
1314
* forward-deployed engineer works them:
@@ -38,9 +39,7 @@ export const AppView = ({ queue }: { queue: QueueItem[] }) => {
3839
// own URL effect then replays it). Read once for the initial tab; the user can still
3940
// switch tabs afterwards.
4041
const searchParams = useSearchParams();
41-
const [view, setView] = useState<View>(
42-
searchParams.get("run") ? "pipeline" : "onboarding",
43-
);
42+
const [view, setView] = useState<View>(searchParams.get("run") ? "pipeline" : "onboarding");
4443
// The single approval workflow shared across both tabs. null until the user
4544
// runs discovery; the pipeline falls back to its default DAG meanwhile.
4645
const [workflow, setWorkflow] = useState<ApprovalWorkflow | null>(null);
@@ -49,8 +48,10 @@ export const AppView = ({ queue }: { queue: QueueItem[] }) => {
4948
// values a vendor/currency gate can route on. Derived from the already-loaded
5049
// queue (no extra query); passed to onboarding so the editor offers + validates
5150
// them, and the popover documents them.
52-
const vendors = [...new Set(queue.map((q) => q.vendor))].sort();
53-
const currencies = [...new Set(queue.map((q) => q.currency))].sort();
51+
const vendors = [...new Set(queue.map((q) => q.vendor))].toSorted((a, b) => a.localeCompare(b));
52+
const currencies = [...new Set(queue.map((q) => q.currency))].toSorted((a, b) =>
53+
a.localeCompare(b)
54+
);
5455

5556
return (
5657
<div className="flex h-full min-h-0 flex-col gap-4">
@@ -60,11 +61,7 @@ export const AppView = ({ queue }: { queue: QueueItem[] }) => {
6061
switch. `hidden` collapses the inactive pane to zero box, so the active
6162
one keeps the full height (the panes use lg:h-full). */}
6263
<div className={view === "onboarding" ? "h-full" : "hidden"}>
63-
<Onboarding
64-
onWorkflowChange={setWorkflow}
65-
vendors={vendors}
66-
currencies={currencies}
67-
/>
64+
<Onboarding onWorkflowChange={setWorkflow} vendors={vendors} currencies={currencies} />
6865
</div>
6966
<div className={view === "pipeline" ? "h-full" : "hidden"}>
7067
<Dashboard
@@ -78,13 +75,7 @@ export const AppView = ({ queue }: { queue: QueueItem[] }) => {
7875
);
7976
};
8077

81-
const Tabs = ({
82-
view,
83-
onChange,
84-
}: {
85-
view: View;
86-
onChange: (v: View) => void;
87-
}) => {
78+
const Tabs = ({ view, onChange }: { view: View; onChange: (v: View) => void }) => {
8879
return (
8980
// Two steps of ONE flow, not two unrelated tabs: build the workflow, then run
9081
// invoices through it. The arrow + the "→ that workflow" subtitles make the link
@@ -132,9 +123,7 @@ const TabButton = ({
132123
<button
133124
onClick={onClick}
134125
className={`group relative -mb-px flex items-center gap-2.5 border-b-2 pb-2 pt-1 text-left transition-colors ${
135-
active
136-
? "border-ink text-ink"
137-
: "border-transparent text-muted hover:text-ink"
126+
active ? "border-ink text-ink" : "border-transparent text-muted hover:text-ink"
138127
}`}
139128
>
140129
<span
@@ -147,9 +136,7 @@ const TabButton = ({
147136
{step}
148137
</span>
149138
<span className="min-w-0">
150-
<span className="block text-[13px] font-medium leading-tight">
151-
{label}
152-
</span>
139+
<span className="block text-[13px] font-medium leading-tight">{label}</span>
153140
{/* The subtitle is the point, keep it visible on wider screens; drop it on
154141
narrow ones where it would wrap and crowd the row. */}
155142
<span className="hidden text-[11px] font-normal leading-tight text-faint sm:block">

0 commit comments

Comments
 (0)