Reconcile artifact renderers and fixtures with the backend shapes

Workers A/B are merged, so match what blueprints/ynab/tools.py and
conversation/agents.py actually emit:

- ynab.spending_report rows and ynab.summary.spent are positive for
  spending; bars use the positive amount and a net refund (negative
  row) sorts last with no bar.
- ynab.transactions start_date may be null (open-ended search); the
  header reads "Through Oct 12, 2026". Amounts stay signed with
  outflows as −$, and total is the net.
- Recipe image_url is never sent, search.documents has no source_ref,
  memo/age_of_money/goal_target are optional; fixtures now match
  (backend titles, ordering, float quantities, no empty fields).
- Mock server: drop the placeholder recipe image, positive month
  spending, and a "philz" open-ended transactions scenario.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 18:34:59 -04:00
parent 0ae87ee610
commit d065b70667
14 changed files with 78 additions and 95 deletions
@@ -35,7 +35,8 @@ export const MealieShoppingLists = ({ data }: RendererProps<MealieShoppingListsD
<span className={cn("flex-1", item.checked ? "text-warm-gray line-through" : "text-charcoal")}>
{item.name}
</span>
{item.quantity != null && item.quantity !== "" && item.quantity !== 0 && (
{/* Like the tool's text, quantities of 0/1 aren't worth showing. */}
{item.quantity != null && item.quantity !== "" && Number(item.quantity) !== 0 && Number(item.quantity) !== 1 && (
<span className="text-xs tabular-nums text-warm-gray">{item.quantity}</span>
)}
</li>
@@ -14,7 +14,7 @@ const MonthBars = ({ rows }: { rows: Row[] }) => {
const ordered = [...rows].sort((a, b) => a.key.localeCompare(b.key));
const withYear = new Set(ordered.map((r) => r.key.slice(0, 4))).size > 1;
const label = (key: string) => monthLabel(key, withYear);
const max = Math.max(...ordered.map((r) => Math.abs(r.amount)), 1);
const max = Math.max(...ordered.map((r) => r.amount), 1);
return (
<div className="rounded-xl border border-sand-light bg-warm-white px-3 pt-4 pb-2">
<div className="flex h-44 items-end gap-1.5">
@@ -25,11 +25,11 @@ const MonthBars = ({ rows }: { rows: Row[] }) => {
title={`${monthLabel(r.key, true)}: ${formatMoney(r.amount)}`}
>
<span className="text-[10px] font-semibold tabular-nums text-warm-gray opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
{formatMoney(Math.abs(r.amount)).replace(/\.\d\d$/, "")}
{formatMoney(r.amount).replace(/\.\d\d$/, "")}
</span>
<div
className="w-full max-w-10 rounded-t-md bg-amber-glow/80 group-hover:bg-amber-dark transition-colors"
style={{ height: `${Math.max(2, (Math.abs(r.amount) / max) * 100)}%` }}
style={{ height: `${Math.max(2, (Math.max(0, r.amount) / max) * 100)}%` }}
/>
</div>
))}
@@ -46,15 +46,16 @@ const MonthBars = ({ rows }: { rows: Row[] }) => {
};
const HorizontalBars = ({ rows }: { rows: Row[] }) => {
const ordered = [...rows].sort((a, b) => Math.abs(b.amount) - Math.abs(a.amount));
const max = Math.max(...ordered.map((r) => Math.abs(r.amount)), 1);
// Amounts are positive spending; a net refund (negative) sorts last with no bar.
const ordered = [...rows].sort((a, b) => b.amount - a.amount);
const max = Math.max(...ordered.map((r) => r.amount), 1);
return (
<ul className="flex flex-col gap-2.5">
{ordered.map((r) => (
<li key={r.key}>
<div className="flex items-baseline justify-between gap-3 text-sm">
<span className="truncate text-charcoal">{r.key}</span>
<span className="shrink-0 font-semibold tabular-nums text-charcoal">
<span className={`shrink-0 font-semibold tabular-nums ${r.amount < 0 ? "text-leaf-dark" : "text-charcoal"}`}>
{formatMoney(r.amount)}
</span>
</div>
@@ -62,7 +63,7 @@ const HorizontalBars = ({ rows }: { rows: Row[] }) => {
<div className="h-2 flex-1 rounded-full bg-sand-light/70 overflow-hidden">
<div
className="h-full rounded-full bg-leaf"
style={{ width: `${(Math.abs(r.amount) / max) * 100}%` }}
style={{ width: `${(Math.max(0, r.amount) / max) * 100}%` }}
/>
</div>
{r.count != null && (
@@ -15,6 +15,7 @@ export const YnabSummary = ({ data }: RendererProps<YnabSummaryData>) => {
/>
<StatTile label="Income" value={formatMoney(data.income)} />
<StatTile label="Assigned" value={formatMoney(data.assigned)} />
{/* Positive for spending, like the spending report. */}
<StatTile label="Spent" value={formatMoney(data.spent)} />
<StatTile
label="Net worth"
@@ -44,7 +44,8 @@ export function formatMonth(value: string | null | undefined): string {
return formatDate(value, { month: "long", year: "numeric" });
}
export function formatRange(start?: string, end?: string): string {
export function formatRange(start?: string | null, end?: string | null): string {
if (start && end) return `${formatDate(start)} – ${formatDate(end)}`;
return formatDate(start ?? end);
if (end) return `Through ${formatDate(end)}`;
return start ? `From ${formatDate(start)}` : "";
}
@@ -11,7 +11,7 @@ export type SourceRef = {
export type MealieRecipeData = {
slug: string;
name: string;
description: string;
description: string | null;
servings: string | number | null;
prep_time: string | null;
cook_time: string | null;
@@ -19,7 +19,7 @@ export type MealieRecipeData = {
ingredients: string[];
instructions: { title?: string; text: string }[];
tags: string[];
image_url?: string;
image_url?: string; // in the contract, not sent by the backend today
};
export type MealieMealPlanData = {
@@ -32,7 +32,7 @@ export type MealieMealPlanData = {
};
export type MealieShoppingListsData = {
lists: { name: string; items: { name: string; quantity: string | number | null; checked: boolean }[] }[];
lists: { name: string; items: { name: string; quantity: number | string | null; checked: boolean }[] }[];
};
export type ObsidianNoteData = {
@@ -61,6 +61,7 @@ export type YnabSummaryData = {
ready_to_assign: number;
income: number;
assigned: number;
/** Positive for spending (-SUM of outflows). */
spent: number;
age_of_money?: number | null;
net_worth: number;
@@ -68,9 +69,11 @@ export type YnabSummaryData = {
};
export type YnabTransactionsData = {
start_date: string;
/** Null for open-ended searches ("all time through end_date"). */
start_date: string | null;
end_date: string;
count: number;
/** Net of outflows and inflows. */
total: number;
transactions: {
date: string;
@@ -78,6 +81,7 @@ export type YnabTransactionsData = {
category: string;
account: string;
memo?: string | null;
/** Signed: negative = outflow. */
amount: number;
}[];
};
@@ -85,7 +89,8 @@ export type YnabTransactionsData = {
export type YnabSpendingReportData = {
start_date: string;
end_date: string;
group_by: string;
group_by: "category" | "category_group" | "payee" | "account" | "month" | string;
/** amount is positive for spending (-SUM); a net refund comes out negative. */
rows: { key: string; amount: number; count?: number }[];
total: number;
};
@@ -97,6 +102,7 @@ export type YnabCategoryBudgetData = {
categories: {
name: string;
assigned: number;
/** Negative for spending. */
activity: number;
available: number;
goal_target?: number | null;
@@ -1,9 +1,6 @@
{
"kind": "mealie.meal_plan",
"title": "Meal plan, Oct 12–18",
"source_ref": {
"system": "mealie"
},
"title": "Meal plan · Oct 12–18",
"data": {
"start_date": "2026-10-12",
"end_date": "2026-10-18",
@@ -48,10 +45,6 @@
}
]
},
{
"date": "2026-10-15",
"meals": []
},
{
"date": "2026-10-16",
"meals": [
@@ -51,7 +51,6 @@
"Indian",
"Weeknight",
"Freezer friendly"
],
"image_url": "/mock-assets/tikka.svg"
]
}
}
@@ -1,9 +1,6 @@
{
"kind": "mealie.shopping_lists",
"title": "Shopping lists",
"source_ref": {
"system": "mealie"
},
"data": {
"lists": [
{
@@ -11,33 +8,33 @@
"items": [
{
"name": "Chicken thighs",
"quantity": "1.5 lb",
"quantity": 1.5,
"checked": false
},
{
"name": "Whole-milk yogurt",
"quantity": 1,
"checked": true
},
{
"name": "Crushed tomatoes (28 oz)",
"quantity": 1,
"quantity": 1.0,
"checked": false
},
{
"name": "Heavy cream",
"quantity": 1,
"quantity": 1.0,
"checked": false
},
{
"name": "Cilantro",
"quantity": null,
"checked": true
},
{
"name": "Tortillas",
"quantity": "2 packs",
"quantity": 2.0,
"checked": false
},
{
"name": "Whole-milk yogurt",
"quantity": 1.0,
"checked": true
},
{
"name": "Cilantro",
"quantity": 1.0,
"checked": true
}
]
},
@@ -46,18 +43,18 @@
"items": [
{
"name": "Cat litter",
"quantity": 2,
"quantity": 2.0,
"checked": false
},
{
"name": "Olive oil",
"quantity": 1.0,
"checked": false
},
{
"name": "Paper towels",
"quantity": 1,
"quantity": 1.0,
"checked": true
},
{
"name": "Olive oil",
"quantity": 1,
"checked": false
}
]
},
@@ -1,6 +1,6 @@
{
"kind": "obsidian.note",
"title": "Simba vet visit 2026-09",
"title": "2026-09 annual checkup",
"source_ref": {
"system": "obsidian",
"path": "Simba/Vet/2026-09 annual checkup.md",
@@ -8,7 +8,7 @@
},
"data": {
"path": "Simba/Vet/2026-09 annual checkup.md",
"title": "Simba vet visit 2026-09",
"title": "2026-09 annual checkup",
"tags": [
"simba",
"vet",
@@ -1,32 +1,29 @@
{
"kind": "search.documents",
"title": "Search: Simba vaccination records",
"source_ref": {
"system": "paperless"
},
"data": {
"query": "Simba vaccination records",
"source": "paperless",
"results": [
{
"title": "Mission Pet Hospital invoice — Sep 2026",
"title": "2026-09-18_mission-pet-hospital",
"snippet": "Patient: Simba (feline, DSH, M/N). Services: annual wellness exam, FVRCP vaccine (3-year), fecal test. Next due: rabies 03/2027. Total $110.00.",
"filename": "2026-09-18_mission-pet-hospital.pdf",
"document_type": "Invoice",
"date": "2026-09-18"
},
{
"title": "Rabies vaccination certificate",
"title": "rabies-certificate-2024",
"snippet": "This certifies that Simba has been vaccinated against rabies. Vaccine: Purevax Feline Rabies, lot 3K91A. Date given 03/14/2024. Expires 03/14/2027.",
"filename": "rabies-certificate-2024.pdf",
"document_type": "Certificate",
"date": "2024-03-14"
},
{
"title": "Adoption packet — SF SPCA",
"title": "2022-05-09_sf-spca-adoption",
"snippet": "Simba, approx. 10 weeks. Vaccines given: FVRCP #1 (2022-05-02), deworming. Microchip 985112004567213 registered to adopter.",
"path": "Simba/Adoption/SPCA packet.md",
"document_type": "Note",
"filename": "2022-05-09_sf-spca-adoption.pdf",
"document_type": "Pet records",
"date": "2022-05-09"
}
]
@@ -1,9 +1,6 @@
{
"kind": "ynab.category_budget",
"title": "October 2026 category budget",
"source_ref": {
"system": "ynab"
},
"title": "Category budget · October 2026",
"data": {
"month": "2026-10",
"groups": [
@@ -1,53 +1,55 @@
{
"kind": "ynab.spending_report",
"title": "Spending by category, Sep 2026",
"source_ref": {
"system": "ynab"
},
"title": "Spending by category · Sep 1–30",
"data": {
"start_date": "2026-09-01",
"end_date": "2026-09-30",
"group_by": "category",
"total": -3684.21,
"total": 3642.21,
"rows": [
{
"key": "Rent",
"amount": -2400.0,
"amount": 2400.0,
"count": 1
},
{
"key": "Groceries",
"amount": -588.14,
"amount": 588.14,
"count": 14
},
{
"key": "Dining Out",
"amount": -243.7,
"amount": 243.7,
"count": 9
},
{
"key": "Electric",
"amount": -97.55,
"count": 1
},
{
"key": "Pet Care",
"amount": -148.0,
"amount": 148.0,
"count": 2
},
{
"key": "Electric",
"amount": 97.55,
"count": 1
},
{
"key": "Transportation",
"amount": -74.32,
"amount": 74.32,
"count": 5
},
{
"key": "Internet",
"amount": -70.0,
"amount": 70.0,
"count": 1
},
{
"key": "Pet Food",
"amount": -62.5,
"amount": 62.5,
"count": 2
},
{
"key": "Clothing",
"amount": -42.0,
"count": 2
}
]
@@ -1,15 +1,12 @@
{
"kind": "ynab.summary",
"title": "October 2026 budget summary",
"source_ref": {
"system": "ynab"
},
"title": "Budget · October 2026",
"data": {
"month": "2026-10",
"ready_to_assign": 412.37,
"income": 7850.0,
"assigned": 7437.63,
"spent": -3921.48,
"spent": 3921.48,
"age_of_money": 41,
"net_worth": 84213.92,
"overspent": [
@@ -1,9 +1,6 @@
{
"kind": "ynab.transactions",
"title": "Dining out, last 30 days",
"source_ref": {
"system": "ynab"
},
"title": "Transactions · Sep 12–Oct 12",
"data": {
"start_date": "2026-09-12",
"end_date": "2026-10-12",
@@ -15,7 +12,6 @@
"payee": "Tartine Bakery",
"category": "Dining Out",
"account": "Chase Sapphire",
"memo": null,
"amount": -18.75
},
{
@@ -31,7 +27,6 @@
"payee": "Philz Coffee",
"category": "Dining Out",
"account": "Checking",
"memo": null,
"amount": -6.25
},
{
@@ -39,7 +34,6 @@
"payee": "Souvla",
"category": "Dining Out",
"account": "Chase Sapphire",
"memo": null,
"amount": -34.1
},
{
@@ -63,7 +57,6 @@
"payee": "Philz Coffee",
"category": "Dining Out",
"account": "Checking",
"memo": null,
"amount": -6.25
},
{
@@ -71,7 +64,6 @@
"payee": "Golden Boy Pizza",
"category": "Dining Out",
"account": "Chase Sapphire",
"memo": null,
"amount": -29.6
},
{
@@ -79,7 +71,6 @@
"payee": "Tartine Bakery",
"category": "Dining Out",
"account": "Chase Sapphire",
"memo": null,
"amount": -12.75
}
]