diff --git a/README.md b/README.md index b5d0edd..b2e4649 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ Zaati OS is an open-source, private-by-default personal operating system. Schedu You choose the LLM. You own the data. You control the deployment. Zaati OS has no hosted account, required telemetry, central database, or platform fee. -Explore the [three populated dashboard examples](examples/README.md), with local screenshots, forkable JSON compositions, and a documented [design system](docs/design-system.md). Run `npm run examples` after installing dependencies. +Explore the [seven populated dashboard examples](examples/README.md), with local screenshots, forkable JSON compositions, and a documented [design system](docs/design-system.md). Run `npm run examples` after installing dependencies.

-Animated preview of the Zaati OS synthetic dashboard +Zaati OS Money dashboard with synthetic balance trends, budget, and allocation

> The LLM is the ingestion and reasoning layer. Your private snapshot store is the durable memory. The dashboard is the interface. diff --git a/config/instance.example.json b/config/instance.example.json index 760f1f5..80d1125 100644 --- a/config/instance.example.json +++ b/config/instance.example.json @@ -1,7 +1,7 @@ { "$schema": "../schemas/instance.schema.json", "brand_name": "Zaati OS", - "brand_mark": "ذ", + "brand_mark": "\u0630", "tagline": "Your day, with context.", "timezone": "UTC", "locale": "en-US", @@ -10,7 +10,7 @@ "enabled_sources": ["overview:daily", "agenda:primary", "inbox:attention", "work:focus", "money:pulse", "news:briefing", "review:weekly"], "experience": { "mode": "demo", - "show_tour": true + "show_tour": false }, "theme": { "preset": "sage", diff --git a/config/sources.json b/config/sources.json index e4272a7..2d6a08e 100644 --- a/config/sources.json +++ b/config/sources.json @@ -14,7 +14,9 @@ "cadence": "daily", "freshness_sla_hours": 30, "dashboard_role": "primary", - "presentation": { "preferred_blocks": ["metric-group", "calendar", "list", "timeline", "notice"] }, + "presentation": { + "preferred_blocks": ["metric-group", "calendar", "list", "timeline", "notice"] + }, "depends_on": [], "target_path": "data/snapshots/agenda/primary/{YYYY}/{MM}/{YYYY-MM-DD}.json", "privacy": { @@ -28,7 +30,7 @@ "id": "inbox:attention", "domain": "inbox", "source": "attention", - "label": "Inbox attention", + "label": "Inbox", "description": "Only messages that need a response, decision, deadline, or follow-up.", "worker_id": "inbox-attention-daily", "prompt": "prompts/inbox-attention.md", @@ -36,7 +38,9 @@ "cadence": "daily", "freshness_sla_hours": 30, "dashboard_role": "primary", - "presentation": { "preferred_blocks": ["metric-group", "list", "timeline", "table", "notice"] }, + "presentation": { + "preferred_blocks": ["metric-group", "list", "timeline", "table", "notice"] + }, "depends_on": [], "target_path": "data/snapshots/inbox/attention/{YYYY}/{MM}/{YYYY-MM-DD}.json", "privacy": { @@ -50,7 +54,7 @@ "id": "work:focus", "domain": "work", "source": "focus", - "label": "Work focus", + "label": "Work", "description": "Owned work, blockers, decisions, and the smallest useful next actions.", "worker_id": "work-focus-daily", "prompt": "prompts/work-focus.md", @@ -58,7 +62,9 @@ "cadence": "weekdays", "freshness_sla_hours": 36, "dashboard_role": "primary", - "presentation": { "preferred_blocks": ["metric-group", "table", "list", "progress", "timeline", "notice"] }, + "presentation": { + "preferred_blocks": ["metric-group", "table", "list", "progress", "timeline", "notice"] + }, "depends_on": [], "target_path": "data/snapshots/work/focus/{YYYY}/{MM}/{YYYY-MM-DD}.json", "privacy": { @@ -72,7 +78,7 @@ "id": "money:pulse", "domain": "money", "source": "pulse", - "label": "Money pulse", + "label": "Money", "description": "User-approved totals, changes, goals, and evidence without raw statements.", "worker_id": "money-pulse-daily", "prompt": "prompts/money-pulse.md", @@ -80,7 +86,9 @@ "cadence": "daily", "freshness_sla_hours": 36, "dashboard_role": "primary", - "presentation": { "preferred_blocks": ["metric-group", "line-chart", "bar-chart", "donut-chart", "table", "progress", "notice"] }, + "presentation": { + "preferred_blocks": ["metric-group", "line-chart", "bar-chart", "donut-chart", "table", "progress", "notice"] + }, "depends_on": [], "target_path": "data/snapshots/money/pulse/{YYYY}/{MM}/{YYYY-MM-DD}.json", "privacy": { @@ -94,7 +102,7 @@ "id": "news:briefing", "domain": "news", "source": "briefing", - "label": "News briefing", + "label": "Briefing", "description": "A small evidence-linked briefing based on configured interests.", "worker_id": "news-briefing-daily", "prompt": "prompts/news-briefing.md", @@ -102,7 +110,9 @@ "cadence": "daily", "freshness_sla_hours": 24, "dashboard_role": "primary", - "presentation": { "preferred_blocks": ["list", "timeline", "bar-chart", "table", "text", "notice"] }, + "presentation": { + "preferred_blocks": ["list", "timeline", "bar-chart", "table", "text", "notice"] + }, "depends_on": [], "target_path": "data/snapshots/news/briefing/{YYYY}/{MM}/{YYYY-MM-DD}.json", "privacy": { @@ -124,7 +134,9 @@ "cadence": "daily", "freshness_sla_hours": 30, "dashboard_role": "primary", - "presentation": { "preferred_blocks": ["metric-group", "line-chart", "calendar", "list", "progress", "timeline", "table", "notice"] }, + "presentation": { + "preferred_blocks": ["metric-group", "line-chart", "calendar", "list", "progress", "timeline", "table", "notice"] + }, "depends_on": ["agenda:primary", "inbox:attention", "work:focus", "money:pulse", "news:briefing"], "target_path": "data/snapshots/overview/daily/{YYYY}/{MM}/{YYYY-MM-DD}.json", "privacy": { @@ -146,7 +158,9 @@ "cadence": "weekly", "freshness_sla_hours": 192, "dashboard_role": "primary", - "presentation": { "preferred_blocks": ["metric-group", "line-chart", "list", "progress", "timeline", "table", "notice"] }, + "presentation": { + "preferred_blocks": ["metric-group", "line-chart", "list", "progress", "timeline", "table", "notice"] + }, "depends_on": ["overview:daily", "agenda:primary", "inbox:attention", "work:focus", "money:pulse", "news:briefing"], "target_path": "data/snapshots/review/weekly/{YYYY}/{MM}/{YYYY-MM-DD}.json", "privacy": { diff --git a/data/component-examples.json b/data/component-examples.json new file mode 100644 index 0000000..2835171 --- /dev/null +++ b/data/component-examples.json @@ -0,0 +1,9 @@ +[ + { + "id": "reading-note", + "kind": "text", + "title": "Reading a snapshot", + "body": "A snapshot records what was known at a point in time. Check the source dates before using it to make a decision.", + "span": "full" + } +] diff --git a/data/examples/agenda/primary/2026-08-24.json b/data/examples/agenda/primary/2026-08-24.json index 863a5d2..2f4e8dd 100644 --- a/data/examples/agenda/primary/2026-08-24.json +++ b/data/examples/agenda/primary/2026-08-24.json @@ -6,36 +6,60 @@ "domain": "agenda", "source": "primary", "generated_at": "2026-08-24T07:20:00Z", - "effective_period": { "start": "2026-08-24T00:00:00Z", "end": "2026-08-24T23:59:59Z", "timezone": "UTC" }, + "effective_period": { + "start": "2026-08-24T00:00:00Z", + "end": "2026-08-24T23:59:59Z", + "timezone": "UTC" + }, "status": "success", - "producer": { "worker_id": "agenda-daily", "workflow": "example", "version": "0.1.1" }, - "sources": [{ "label": "Synthetic calendar", "status": "ok", "as_of": "2026-08-24T07:20:00Z", "reference": "synthetic://calendar" }], - "freshness": { "expires_at": "2026-08-25T13:20:00Z" }, - "quality": { "confidence": "high", "warnings": [] }, - "privacy": { "classification": "public", "contains_personal_data": false, "synthetic": true }, + "producer": { + "worker_id": "agenda-daily", + "workflow": "example", + "version": "0.1.1" + }, + "sources": [ + { + "label": "Synthetic calendar", + "status": "ok", + "as_of": "2026-08-24T07:20:00Z", + "reference": "synthetic://calendar" + } + ], + "freshness": { + "expires_at": "2026-08-25T13:20:00Z" + }, + "quality": { + "confidence": "high", + "warnings": [] + }, + "privacy": { + "classification": "public", + "contains_personal_data": false, + "synthetic": true + }, "data": { - "title": "A focused Monday", - "summary": "Two deep-work windows, one short sync, and enough recovery space remain.", + "title": "Agenda", + "summary": "See today\u2019s commitments and prepare for the next one. Two focus windows total 3.5 hours; the only meeting takes 30 minutes.", "attention": "low", "facts": { "events": [ { "id": "a1", - "title": "Build the first usable slice", + "title": "Finish the homepage", "start": "2026-08-24T08:30:00Z", "end": "2026-08-24T10:30:00Z", "status": "scheduled" }, { "id": "a2", - "title": "Project sync", + "title": "Project check-in", "start": "2026-08-24T11:00:00Z", "end": "2026-08-24T11:30:00Z", "status": "scheduled" }, { "id": "a3", - "title": "Review and ship", + "title": "Review the homepage", "start": "2026-08-24T13:00:00Z", "end": "2026-08-24T14:30:00Z", "status": "scheduled" @@ -49,78 +73,111 @@ } ], "actions": [ - { "id": "p1", "title": "Open the acceptance criteria", "due_at": "2026-08-24T08:30:00Z", "status": "open" }, - { "id": "p2", "title": "Write the sync decision", "due_at": "2026-08-24T11:00:00Z", "status": "open" }, - { "id": "p3", "title": "Prepare the release note", "due_at": "2026-08-24T13:00:00Z", "status": "open" } + { + "id": "p1", + "title": "Open the homepage feedback", + "due_at": "2026-08-24T08:30:00Z", + "status": "open" + }, + { + "id": "p2", + "title": "List the decision needed at check-in", + "due_at": "2026-08-24T11:00:00Z", + "status": "open" + }, + { + "id": "p3", + "title": "Prepare the review link", + "due_at": "2026-08-24T13:00:00Z", + "status": "open" + } ] }, "presentation": { - "layout": "focus", + "layout": "dashboard", "blocks": [ + { + "id": "day-capacity", + "kind": "metric-group", + "title": "Today\u2019s commitments", + "span": "full", + "metrics": [ + { + "label": "Focus time", + "value": 3.5, + "unit": "hours" + }, + { + "label": "Meeting time", + "value": 30, + "unit": "minutes" + }, + { + "label": "Scheduled events", + "value": 4 + } + ] + }, { "id": "schedule", "kind": "calendar", - "title": "Schedule", + "title": "Monday\u2019s schedule", "date": "2026-08-24", "span": "two", "events": [ { "id": "a1", - "title": "Build the first usable slice", + "title": "Finish the homepage", "start": "2026-08-24T08:30:00Z", "end": "2026-08-24T10:30:00Z", "tone": "positive" }, - { "id": "a2", "title": "Project sync", "start": "2026-08-24T11:00:00Z", "end": "2026-08-24T11:30:00Z", "tone": "info" }, - { "id": "a3", "title": "Review and ship", "start": "2026-08-24T13:00:00Z", "end": "2026-08-24T14:30:00Z", "tone": "positive" }, - { "id": "a4", "title": "Walk and reset", "start": "2026-08-24T15:00:00Z", "end": "2026-08-24T15:30:00Z", "tone": "neutral" } - ] + { + "id": "a2", + "title": "Project check-in", + "start": "2026-08-24T11:00:00Z", + "end": "2026-08-24T11:30:00Z", + "tone": "info" + }, + { + "id": "a3", + "title": "Review the homepage", + "start": "2026-08-24T13:00:00Z", + "end": "2026-08-24T14:30:00Z", + "tone": "positive" + }, + { + "id": "a4", + "title": "Walk and reset", + "start": "2026-08-24T15:00:00Z", + "end": "2026-08-24T15:30:00Z", + "tone": "neutral" + } + ], + "description": "August 24 \u00b7 all times in UTC" }, { "id": "prep", "kind": "list", - "title": "Tiny preparation", - "span": "one", - "items": [ - { "id": "p1", "title": "Open the acceptance criteria", "meta": "Before 08:30", "tone": "neutral" }, - { "id": "p2", "title": "Write the sync decision in one sentence", "meta": "Before 11:00", "tone": "info" }, - { "id": "p3", "title": "Prepare the release note", "meta": "Before 13:00", "tone": "neutral" } - ] - }, - { - "id": "day-capacity", - "kind": "metric-group", - "title": "Day capacity", - "span": "full", - "metrics": [ - { "label": "Protected focus", "value": 3.5, "unit": "hours", "tone": "positive" }, - { "label": "Meetings", "value": 0.5, "unit": "hours", "tone": "neutral" }, - { "label": "Open buffer", "value": 2, "unit": "hours", "tone": "info" }, - { "label": "Prep actions", "value": 3, "tone": "warning" } - ] - }, - { - "id": "day-sequence", - "kind": "timeline", - "title": "Energy-aware sequence", + "title": "Before each commitment", "span": "one", "items": [ { - "label": "08:30", - "title": "Build before coordinating", - "description": "Use the highest-energy window for the complete slice.", - "tone": "positive" + "id": "p1", + "title": "Open the homepage feedback", + "meta": "Before 08:30", + "tone": "neutral" }, { - "label": "11:00", - "title": "Make one decision visible", - "description": "End the sync with an owner and next checkpoint.", + "id": "p2", + "title": "List the decision needed at check-in", + "meta": "Before 11:00", "tone": "info" }, { - "label": "15:00", - "title": "Stop before the final push", - "description": "The reset protects the remaining buffer.", + "id": "p3", + "title": "Prepare the review link", + "meta": "Before 13:00", "tone": "neutral" } ] @@ -131,7 +188,7 @@ "title": "Keep 15:30 onward unclaimed", "body": "That window absorbs a late review, travel, or an unfinished release note without displacing recovery time.", "tone": "info", - "span": "one" + "span": "full" } ] } diff --git a/data/examples/inbox/attention/2026-08-24.json b/data/examples/inbox/attention/2026-08-24.json index d5de576..9f213e3 100644 --- a/data/examples/inbox/attention/2026-08-24.json +++ b/data/examples/inbox/attention/2026-08-24.json @@ -6,16 +6,40 @@ "domain": "inbox", "source": "attention", "generated_at": "2026-08-24T07:15:00Z", - "effective_period": { "start": "2026-08-23T07:15:00Z", "end": "2026-08-24T07:15:00Z", "timezone": "UTC" }, + "effective_period": { + "start": "2026-08-23T07:15:00Z", + "end": "2026-08-24T07:15:00Z", + "timezone": "UTC" + }, "status": "success", - "producer": { "worker_id": "inbox-attention-daily", "workflow": "example", "version": "0.1.1" }, - "sources": [{ "label": "Synthetic inbox", "status": "ok", "as_of": "2026-08-24T07:15:00Z", "reference": "synthetic://inbox" }], - "freshness": { "expires_at": "2026-08-25T13:15:00Z" }, - "quality": { "confidence": "high", "warnings": [] }, - "privacy": { "classification": "public", "contains_personal_data": false, "synthetic": true }, + "producer": { + "worker_id": "inbox-attention-daily", + "workflow": "example", + "version": "0.1.1" + }, + "sources": [ + { + "label": "Synthetic inbox", + "status": "ok", + "as_of": "2026-08-24T07:15:00Z", + "reference": "synthetic://inbox" + } + ], + "freshness": { + "expires_at": "2026-08-25T13:15:00Z" + }, + "quality": { + "confidence": "high", + "warnings": [] + }, + "privacy": { + "classification": "public", + "contains_personal_data": false, + "synthetic": true + }, "data": { - "title": "Four messages fit into two reply windows", - "summary": "One deadline and one scheduling decision come first. A review and follow-up can share the afternoon window; 18 low-value messages remain excluded.", + "title": "Inbox", + "summary": "Three messages need your response, including two due today. One conversation is waiting on someone else.", "attention": "medium", "facts": { "items": [ @@ -52,31 +76,47 @@ ] }, "presentation": { - "layout": "focus", + "layout": "dashboard", "blocks": [ { "id": "counts", "kind": "metric-group", - "title": "Attention filter", + "title": "Message status", "span": "full", "metrics": [ - { "label": "Needs reply", "value": 4, "tone": "warning" }, - { "label": "Due today", "value": 2, "tone": "danger" }, - { "label": "Waiting on others", "value": 1, "tone": "neutral" }, - { "label": "Ignored noise", "value": 18, "tone": "positive" } + { + "label": "Needs your response", + "value": 3 + }, + { + "label": "Due today", + "value": 2 + }, + { + "label": "Waiting on others", + "value": 1 + } ] }, { "id": "messages", "kind": "list", - "title": "Reply queue", + "title": "Needs your response", "span": "two", "items": [ + { + "id": "m3", + "title": "Confirm the renewal choice", + "description": "A yes-or-no choice is due before the afternoon processing window.", + "meta": "Operations contact \u00b7 Due today, 15:00 UTC", + "status": "Deadline", + "tone": "danger" + }, { "id": "m1", "title": "Choose a planning window", "description": "A collaborator proposed two times for tomorrow.", - "meta": "Due today", + "meta": "Project collaborator \u00b7 Due today, 17:00 UTC", "status": "Decision", "tone": "warning" }, @@ -84,72 +124,27 @@ "id": "m2", "title": "Approve the revised outline", "description": "The requested changes are ready for a quick review.", - "meta": "15 minutes", + "meta": "Review partner \u00b7 No stated deadline", "status": "Review", "tone": "info" - }, - { - "id": "m3", - "title": "Confirm the renewal choice", - "description": "A yes-or-no choice is due before the afternoon processing window.", - "meta": "Due 15:00", - "status": "Deadline", - "tone": "danger" - }, - { - "id": "m4", - "title": "Close the open research question", - "description": "Send the missing constraint; no full thread review is required.", - "meta": "No explicit deadline", - "status": "Follow-up", - "tone": "neutral" } ] }, { - "id": "noise", - "kind": "notice", - "title": "Noise stayed quiet", - "body": "Newsletters, receipts, automated updates, and promotional messages were excluded from the attention queue.", - "tone": "positive", - "span": "one" - }, - { - "id": "reply-plan", - "kind": "timeline", - "title": "Reply plan", + "id": "waiting", + "kind": "list", + "title": "Waiting on a response", "span": "one", + "description": "No reply needed from you right now.", "items": [ { - "label": "09:50", - "title": "Resolve the scheduling choice", - "description": "Reply before the first focus block closes.", - "tone": "warning" - }, - { - "label": "12:15", - "title": "Confirm the renewal", - "description": "A one-line answer clears the only hard deadline.", - "tone": "danger" - }, - { "label": "16:00", "title": "Batch review and follow-up", "description": "Cap the window at 25 minutes.", "tone": "info" } - ] - }, - { - "id": "attention-register", - "kind": "table", - "title": "Attention register", - "span": "one", - "columns": [ - { "key": "item", "label": "Item" }, - { "key": "reason", "label": "Reason" }, - { "key": "window", "label": "Window" } - ], - "rows": [ - { "item": "Planning window", "reason": "Decision", "window": "09:50" }, - { "item": "Renewal choice", "reason": "Deadline", "window": "12:15" }, - { "item": "Revised outline", "reason": "Review", "window": "16:00" }, - { "item": "Research question", "reason": "Follow-up", "window": "16:00" } + "id": "m4", + "title": "Research question", + "description": "The collaborator has the question. Follow up if it is still unresolved at your next review.", + "meta": "Research collaborator \u00b7 No deadline", + "status": "Waiting", + "tone": "neutral" + } ] } ] diff --git a/data/examples/money/pulse/2026-08-24.json b/data/examples/money/pulse/2026-08-24.json index 7aff829..c8669c9 100644 --- a/data/examples/money/pulse/2026-08-24.json +++ b/data/examples/money/pulse/2026-08-24.json @@ -6,20 +6,52 @@ "domain": "money", "source": "pulse", "generated_at": "2026-08-24T07:00:00Z", - "effective_period": { "start": "2026-08-23T07:00:00Z", "end": "2026-08-24T07:00:00Z", "timezone": "UTC" }, + "effective_period": { + "start": "2026-08-23T07:00:00Z", + "end": "2026-08-24T07:00:00Z", + "timezone": "UTC" + }, "status": "partial", - "producer": { "worker_id": "money-pulse-daily", "workflow": "example", "version": "0.1.1" }, + "producer": { + "worker_id": "money-pulse-daily", + "workflow": "example", + "version": "0.1.1" + }, "sources": [ - { "label": "Demo banking connection", "status": "ok", "as_of": "2026-08-24T06:40:00Z", "reference": "synthetic://banking" }, - { "label": "Demo brokerage summary", "status": "ok", "as_of": "2026-08-24T06:35:00Z", "reference": "synthetic://brokerage" }, - { "label": "Demo crypto summary", "status": "stale", "as_of": "2026-08-23T18:00:00Z", "reference": "synthetic://crypto" } + { + "label": "Demo banking connection", + "status": "ok", + "as_of": "2026-08-24T06:40:00Z", + "reference": "synthetic://banking" + }, + { + "label": "Demo brokerage summary", + "status": "ok", + "as_of": "2026-08-24T06:35:00Z", + "reference": "synthetic://brokerage" + }, + { + "label": "Demo crypto summary", + "status": "stale", + "as_of": "2026-08-23T18:00:00Z", + "reference": "synthetic://crypto" + } ], - "freshness": { "expires_at": "2026-08-25T19:00:00Z" }, - "quality": { "confidence": "medium", "warnings": ["One synthetic source has not refreshed today."] }, - "privacy": { "classification": "public", "contains_personal_data": false, "synthetic": true }, + "freshness": { + "expires_at": "2026-08-25T19:00:00Z" + }, + "quality": { + "confidence": "medium", + "warnings": ["One synthetic source has not refreshed today."] + }, + "privacy": { + "classification": "public", + "contains_personal_data": false, + "synthetic": true + }, "data": { - "title": "The plan is funded, with one price still provisional", - "summary": "Cash covers 11 months, contributions are ahead of plan, and the baseline five-year scenario reaches 178,000 synthetic dollars. Crypto pricing is stale, so allocation and daily movement remain provisional.", + "title": "Money", + "summary": "Track your balances and where your monthly budget goes. Cash covers 7.3 months of living costs; crypto prices are awaiting a refresh.", "attention": "low", "facts": { "measures": [ @@ -39,8 +71,21 @@ "currency": "USD", "as_of": "2026-08-24T06:35:00Z" }, - { "id": "savings-rate", "label": "Savings rate", "kind": "percent", "value": 31, "as_of": "2026-08-24T06:40:00Z" }, - { "id": "runway", "label": "Cash runway", "kind": "duration", "value": 11, "unit": "months", "as_of": "2026-08-24T06:40:00Z" }, + { + "id": "savings-rate", + "label": "Savings rate", + "kind": "percent", + "value": 38.5, + "as_of": "2026-08-24T06:40:00Z" + }, + { + "id": "runway", + "label": "Cash runway", + "kind": "duration", + "value": 7.3, + "unit": "months", + "as_of": "2026-08-24T06:40:00Z" + }, { "id": "contribution-rate", "label": "Monthly contribution", @@ -51,8 +96,20 @@ } ], "goals": [ - { "id": "reserve", "label": "Emergency reserve", "current": 11, "target": 12, "unit": "months" }, - { "id": "investing", "label": "Annual investing target", "current": 14400, "target": 21600, "unit": "USD" } + { + "id": "reserve", + "label": "Emergency reserve", + "current": 5.1, + "target": 6, + "unit": "months" + }, + { + "id": "investing", + "label": "Annual investing target", + "current": 14400, + "target": 21600, + "unit": "USD" + } ], "accounts": [ { @@ -159,22 +216,79 @@ "as_of": "2026-08-23T18:00:00Z" } ], - "projections": [ - { "year": 2026, "scenario": "conservative", "value": 104800, "currency": "USD" }, - { "year": 2026, "scenario": "baseline", "value": 104800, "currency": "USD" }, - { "year": 2026, "scenario": "stretch", "value": 104800, "currency": "USD" }, - { "year": 2027, "scenario": "conservative", "value": 124000, "currency": "USD" }, - { "year": 2027, "scenario": "baseline", "value": 128000, "currency": "USD" }, - { "year": 2027, "scenario": "stretch", "value": 132000, "currency": "USD" }, - { "year": 2028, "scenario": "conservative", "value": 142000, "currency": "USD" }, - { "year": 2028, "scenario": "baseline", "value": 150000, "currency": "USD" }, - { "year": 2028, "scenario": "stretch", "value": 160000, "currency": "USD" }, - { "year": 2029, "scenario": "conservative", "value": 158000, "currency": "USD" }, - { "year": 2029, "scenario": "baseline", "value": 166000, "currency": "USD" }, - { "year": 2029, "scenario": "stretch", "value": 181000, "currency": "USD" }, - { "year": 2030, "scenario": "conservative", "value": 171000, "currency": "USD" }, - { "year": 2030, "scenario": "baseline", "value": 178000, "currency": "USD" }, - { "year": 2030, "scenario": "stretch", "value": 205000, "currency": "USD" } + "balance_history": [ + { + "date": "2026-06-08", + "cash": 18700, + "invested": 75500, + "currency": "USD" + }, + { + "date": "2026-06-15", + "cash": 19200, + "invested": 76100, + "currency": "USD" + }, + { + "date": "2026-06-22", + "cash": 18900, + "invested": 75300, + "currency": "USD" + }, + { + "date": "2026-06-29", + "cash": 19600, + "invested": 76800, + "currency": "USD" + }, + { + "date": "2026-07-06", + "cash": 20200, + "invested": 77200, + "currency": "USD" + }, + { + "date": "2026-07-13", + "cash": 19800, + "invested": 77900, + "currency": "USD" + }, + { + "date": "2026-07-20", + "cash": 20400, + "invested": 77300, + "currency": "USD" + }, + { + "date": "2026-07-27", + "cash": 21100, + "invested": 78900, + "currency": "USD" + }, + { + "date": "2026-08-03", + "cash": 20900, + "invested": 79600, + "currency": "USD" + }, + { + "date": "2026-08-10", + "cash": 21600, + "invested": 79100, + "currency": "USD" + }, + { + "date": "2026-08-17", + "cash": 22100, + "invested": 80900, + "currency": "USD" + }, + { + "date": "2026-08-24", + "cash": 22600, + "invested": 82200, + "currency": "USD" + } ] }, "presentation": { @@ -183,159 +297,443 @@ { "id": "money-metrics", "kind": "metric-group", - "title": "Current setup", - "description": "Illustrative values from synthetic connected sources.", - "span": "full", + "title": "Balance overview", + "span": "one", "metrics": [ { "label": "Tracked value", "value": 104800, "format": "currency", - "change": 2100, - "change_label": "this month", - "tone": "positive" + "tone": "positive", + "trend": { + "label": "Jun 8\u2013Aug 24 \u00b7 weekly", + "points": [ + { + "label": "2026-06-08", + "value": 94200 + }, + { + "label": "2026-06-15", + "value": 95300 + }, + { + "label": "2026-06-22", + "value": 94200 + }, + { + "label": "2026-06-29", + "value": 96400 + }, + { + "label": "2026-07-06", + "value": 97400 + }, + { + "label": "2026-07-13", + "value": 97700 + }, + { + "label": "2026-07-20", + "value": 97700 + }, + { + "label": "2026-07-27", + "value": 100000 + }, + { + "label": "2026-08-03", + "value": 100500 + }, + { + "label": "2026-08-10", + "value": 100700 + }, + { + "label": "2026-08-17", + "value": 103000 + }, + { + "label": "2026-08-24", + "value": 104800 + } + ] + }, + "change": 10600, + "change_label": "since Jun 8" + }, + { + "label": "Invested", + "value": 82200, + "format": "currency", + "tone": "info", + "trend": { + "label": "Jun 8\u2013Aug 24 \u00b7 weekly", + "points": [ + { + "label": "2026-06-08", + "value": 75500 + }, + { + "label": "2026-06-15", + "value": 76100 + }, + { + "label": "2026-06-22", + "value": 75300 + }, + { + "label": "2026-06-29", + "value": 76800 + }, + { + "label": "2026-07-06", + "value": 77200 + }, + { + "label": "2026-07-13", + "value": 77900 + }, + { + "label": "2026-07-20", + "value": 77300 + }, + { + "label": "2026-07-27", + "value": 78900 + }, + { + "label": "2026-08-03", + "value": 79600 + }, + { + "label": "2026-08-10", + "value": 79100 + }, + { + "label": "2026-08-17", + "value": 80900 + }, + { + "label": "2026-08-24", + "value": 82200 + } + ] + }, + "change": 6700, + "change_label": "since Jun 8" }, - { "label": "Invested", "value": 82200, "format": "currency", "tone": "info" }, - { "label": "Cash", "value": 22600, "format": "currency", "tone": "neutral" }, - { "label": "Savings rate", "value": 31, "format": "percent", "tone": "positive" }, - { "label": "Runway", "value": 11, "unit": "months", "tone": "positive" }, - { "label": "Monthly contribution", "value": 1800, "format": "currency", "tone": "info" } + { + "label": "Cash", + "value": 22600, + "format": "currency", + "tone": "neutral", + "trend": { + "label": "Jun 8\u2013Aug 24 \u00b7 weekly", + "points": [ + { + "label": "2026-06-08", + "value": 18700 + }, + { + "label": "2026-06-15", + "value": 19200 + }, + { + "label": "2026-06-22", + "value": 18900 + }, + { + "label": "2026-06-29", + "value": 19600 + }, + { + "label": "2026-07-06", + "value": 20200 + }, + { + "label": "2026-07-13", + "value": 19800 + }, + { + "label": "2026-07-20", + "value": 20400 + }, + { + "label": "2026-07-27", + "value": 21100 + }, + { + "label": "2026-08-03", + "value": 20900 + }, + { + "label": "2026-08-10", + "value": 21600 + }, + { + "label": "2026-08-17", + "value": 22100 + }, + { + "label": "2026-08-24", + "value": 22600 + } + ] + }, + "change": 3900, + "change_label": "since Jun 8" + }, + { + "label": "Cash runway", + "value": 7.3, + "unit": "months", + "tone": "positive" + } ] }, { - "id": "five-year-projection", + "id": "balance-history", "kind": "line-chart", - "title": "Five-year projection", - "description": "Three illustrative scenarios based on a fixed monthly contribution; not a forecast or financial advice.", + "title": "Balance history", + "description": "12 weekly snapshots \u00b7 Jun 8\u2013Aug 24. Changes include contributions, withdrawals, and market movements; they are not investment returns.", "span": "two", + "x_label": "Weekly balance date", "y_format": "currency", "series": [ - { "key": "conservative", "label": "Conservative" }, - { "key": "baseline", "label": "Baseline" }, - { "key": "stretch", "label": "Stretch" } + { + "key": "total", + "label": "Tracked balance" + } ], "points": [ - { "x": "2026", "values": { "conservative": 104800, "baseline": 104800, "stretch": 104800 } }, - { "x": "2027", "values": { "conservative": 124000, "baseline": 128000, "stretch": 132000 } }, - { "x": "2028", "values": { "conservative": 142000, "baseline": 150000, "stretch": 160000 } }, - { "x": "2029", "values": { "conservative": 158000, "baseline": 166000, "stretch": 181000 } }, - { "x": "2030", "values": { "conservative": 171000, "baseline": 178000, "stretch": 205000 } } + { + "x": "Jun 8", + "values": { + "total": 94200 + } + }, + { + "x": "Jun 15", + "values": { + "total": 95300 + } + }, + { + "x": "Jun 22", + "values": { + "total": 94200 + } + }, + { + "x": "Jun 29", + "values": { + "total": 96400 + } + }, + { + "x": "Jul 6", + "values": { + "total": 97400 + } + }, + { + "x": "Jul 13", + "values": { + "total": 97700 + } + }, + { + "x": "Jul 20", + "values": { + "total": 97700 + } + }, + { + "x": "Jul 27", + "values": { + "total": 100000 + } + }, + { + "x": "Aug 3", + "values": { + "total": 100500 + } + }, + { + "x": "Aug 10", + "values": { + "total": 100700 + } + }, + { + "x": "Aug 17", + "values": { + "total": 103000 + } + }, + { + "x": "Aug 24", + "values": { + "total": 104800 + } + } + ] + }, + { + "id": "monthly-flow", + "kind": "bar-chart", + "title": "Monthly budget", + "description": "6,500 total allocated: living, investing, reserve, and flexible spending.", + "span": "two", + "value_format": "currency", + "bars": [ + { + "label": "Living", + "value": 3100, + "tone": "neutral" + }, + { + "label": "Investing", + "value": 1800, + "tone": "positive" + }, + { + "label": "Reserve", + "value": 700, + "tone": "info" + }, + { + "label": "Flexible", + "value": 900, + "tone": "neutral" + } ] }, { "id": "allocation", "kind": "donut-chart", - "title": "Portfolio allocation", - "description": "Cash is included so the chart reconciles to the tracked total.", + "title": "Where the money is", + "description": "Accounted balances, including cash. Crypto valuation is provisional.", "span": "one", "value_format": "currency", "center_label": "Tracked total", "segments": [ - { "label": "Global equity fund", "value": 49770 }, - { "label": "Quality stocks", "value": 19400 }, - { "label": "Bonds", "value": 7930 }, - { "label": "Crypto", "value": 5100 }, - { "label": "Cash", "value": 22600 } + { + "label": "Global equity fund", + "value": 49770 + }, + { + "label": "Quality stocks", + "value": 19400 + }, + { + "label": "Bonds", + "value": 7930 + }, + { + "label": "Crypto", + "value": 5100 + }, + { + "label": "Cash", + "value": 22600 + } ] }, { "id": "holdings", "kind": "table", "title": "Invested holdings", - "description": "Exact synthetic positions and the price time used for each value.", - "span": "full", + "description": "Rounded market values. Weights use the 104,800 tracked total, including cash.", + "span": "two", "columns": [ - { "key": "asset", "label": "Asset" }, - { "key": "type", "label": "Type" }, - { "key": "units", "label": "Units", "format": "number" }, - { "key": "price", "label": "Price", "format": "currency" }, - { "key": "value", "label": "Value", "format": "currency" }, - { "key": "weight", "label": "Weight", "format": "percent" }, - { "key": "day", "label": "Day", "format": "percent" } + { + "key": "asset", + "label": "Asset" + }, + { + "key": "value", + "label": "Value", + "format": "currency" + }, + { + "key": "weight", + "label": "Weight", + "format": "percent" + }, + { + "key": "status", + "label": "Prices", + "filterable": true, + "tones": { + "Current": "neutral", + "Older": "warning" + } + } ], "rows": [ { - "asset": "WORLD · Global equity fund", - "type": "Fund", - "units": 420, - "price": 118.5, + "asset": "WORLD \u00b7 Global equity fund", "value": 49770, "weight": 47.5, - "day": 0.6 + "status": "Current" }, { - "asset": "QUAL · Quality companies", - "type": "Stock basket", - "units": 160, - "price": 121.25, + "asset": "QUAL \u00b7 Quality companies", "value": 19400, "weight": 18.5, - "day": -0.2 + "status": "Current" }, - { "asset": "BOND · Short bond fund", "type": "Bond", "units": 155, "price": 51.16, "value": 7930, "weight": 7.6, "day": 0.1 }, - { "asset": "BTC · Bitcoin", "type": "Crypto", "units": 0.062, "price": 65000, "value": 4030, "weight": 3.8, "day": 1.4 }, - { "asset": "ETH · Ethereum", "type": "Crypto", "units": 0.42, "price": 2548, "value": 1070, "weight": 1, "day": -0.8 } - ] - }, - { - "id": "account-connections", - "kind": "list", - "title": "Connections", - "description": "Only normalized totals are retained—never account numbers or statements.", - "span": "one", - "items": [ { - "id": "bank", - "title": "Banking summary", - "description": "Daily cash and emergency reserve refreshed at 06:40 UTC.", - "status": "Connected", - "tone": "positive" + "asset": "BOND \u00b7 Short bond fund", + "value": 7930, + "weight": 7.6, + "status": "Current" }, { - "id": "brokerage", - "title": "Brokerage summary", - "description": "Three long-term positions refreshed at 06:35 UTC.", - "status": "Connected", - "tone": "positive" + "asset": "BTC \u00b7 Bitcoin", + "value": 4030, + "weight": 3.8, + "status": "Older" }, { - "id": "crypto", - "title": "Crypto summary", - "description": "Two positions last priced yesterday at 18:00 UTC.", - "status": "Stale", - "tone": "warning" + "asset": "ETH \u00b7 Ethereum", + "value": 1070, + "weight": 1, + "status": "Older" } - ] - }, - { - "id": "monthly-flow", - "kind": "bar-chart", - "title": "Monthly plan", - "description": "Where the illustrative take-home amount is assigned this month.", - "span": "one", - "value_format": "currency", - "bars": [ - { "label": "Living", "value": 3100, "tone": "neutral" }, - { "label": "Investing", "value": 1800, "tone": "positive" }, - { "label": "Reserve", "value": 700, "tone": "info" }, - { "label": "Flexible", "value": 900, "tone": "neutral" } - ] + ], + "searchable": true }, { "id": "money-goals", "kind": "progress", - "title": "Explicit goals", + "title": "Progress toward your goals", "span": "one", "items": [ - { "label": "Emergency reserve", "value": 11, "max": 12, "value_label": "11 of 12 months", "tone": "positive" }, - { "label": "Annual investing", "value": 14400, "max": 21600, "value_label": "$14.4k of $21.6k", "tone": "info" } - ] + { + "label": "Emergency reserve", + "value": 5.1, + "max": 6, + "value_label": "5.1 of 6 months", + "tone": "positive" + }, + { + "label": "Annual investing", + "value": 14400, + "max": 21600, + "value_label": "$14.4k of $21.6k", + "tone": "info" + } + ], + "description": "The 15,800 emergency reserve covers 5.1 months of living costs; daily cash is excluded." }, { "id": "money-warning", "kind": "notice", - "title": "Projection, not promise", - "body": "These scenarios assume a fixed monthly contribution and illustrative returns. Crypto prices are stale, taxes and fees are excluded, and the page is context—not financial advice or an automated decision.", + "title": "Refresh prices before comparing returns", + "body": "The crypto balance uses yesterday\u2019s prices. This view is suitable for an approximate balance overview, but not for a daily return comparison.", "tone": "warning", "span": "full" } diff --git a/data/examples/news/briefing/2026-08-24.json b/data/examples/news/briefing/2026-08-24.json index ca6c8ef..24b6563 100644 --- a/data/examples/news/briefing/2026-08-24.json +++ b/data/examples/news/briefing/2026-08-24.json @@ -6,16 +6,40 @@ "domain": "news", "source": "briefing", "generated_at": "2026-08-24T06:55:00Z", - "effective_period": { "start": "2026-08-23T06:55:00Z", "end": "2026-08-24T06:55:00Z", "timezone": "UTC" }, + "effective_period": { + "start": "2026-08-23T06:55:00Z", + "end": "2026-08-24T06:55:00Z", + "timezone": "UTC" + }, "status": "success", - "producer": { "worker_id": "news-briefing-daily", "workflow": "example", "version": "0.1.1" }, - "sources": [{ "label": "Synthetic public reporting", "status": "ok", "as_of": "2026-08-24T06:45:00Z", "reference": "synthetic://news" }], - "freshness": { "expires_at": "2026-08-25T06:55:00Z" }, - "quality": { "confidence": "high", "warnings": ["Stories and links in demo mode are illustrative, not current news."] }, - "privacy": { "classification": "public", "contains_personal_data": false, "synthetic": true }, + "producer": { + "worker_id": "news-briefing-daily", + "workflow": "example", + "version": "0.1.1" + }, + "sources": [ + { + "label": "Synthetic public reporting", + "status": "ok", + "as_of": "2026-08-24T06:45:00Z", + "reference": "synthetic://news" + } + ], + "freshness": { + "expires_at": "2026-08-25T06:55:00Z" + }, + "quality": { + "confidence": "high", + "warnings": ["Stories and links in demo mode are illustrative, not current news."] + }, + "privacy": { + "classification": "public", + "contains_personal_data": false, + "synthetic": true + }, "data": { - "title": "Five developments, three things to watch", - "summary": "The synthetic briefing connects each retained development to a concrete watchpoint and shows which topics received enough independent coverage.", + "title": "Briefing", + "summary": "Five developments selected for your interests, with context for what to investigate next. These example stories and links are fictional.", "attention": "low", "facts": { "stories": [ @@ -62,127 +86,76 @@ ] }, "presentation": { - "layout": "timeline", + "layout": "dashboard", "blocks": [ { "id": "briefing", "kind": "list", - "title": "Briefing", + "title": "Selected reading", "span": "two", "items": [ { "id": "n1", "title": "Open standards make agent workflows easier to move", - "description": "Why it matters: teams can change models without rebuilding every downstream view.", + "description": "Teams can change models without rebuilding every downstream view.", "meta": "AI systems", - "tone": "info", + "tone": "neutral", "href": "https://example.com/ai-standards" }, { "id": "n2", - "title": "Smaller software releases continue to outperform big-bang delivery", - "description": "Why it matters: reliability improves when feedback arrives before architecture hardens.", + "title": "Smaller software releases outperform big-bang delivery", + "description": "Reliability improves when feedback arrives before architecture hardens.", "meta": "Engineering", - "tone": "positive", + "tone": "neutral", "href": "https://example.com/software-delivery" }, { "id": "n3", "title": "Privacy controls move closer to the data layer", - "description": "Why it matters: access boundaries become portable across hosting providers.", + "description": "Access boundaries become portable across hosting providers.", "meta": "Privacy", - "tone": "warning", + "tone": "neutral", "href": "https://example.com/privacy-controls" }, { "id": "n4", "title": "Local-first tools add portable encrypted exports", - "description": "Why it matters: durable records can move without creating a permanent hosted account dependency.", - "meta": "Privacy · Product design", - "tone": "positive", + "description": "Users can move durable records without giving the application permanent custody of their history.", + "meta": "Privacy \u00b7 Product design", + "tone": "neutral", "href": "https://example.com/local-first-exports" }, { "id": "n5", "title": "Schema registries become a coordination layer for AI workflows", - "description": "Why it matters: facts remain stable even when models and renderers change.", - "meta": "AI systems · Engineering", - "tone": "info", + "description": "Teams are separating durable facts from model-specific presentation decisions.", + "meta": "AI systems \u00b7 Engineering", + "tone": "neutral", "href": "https://example.com/schema-registries" } - ] + ], + "description": "Short summaries with source links; open a source before acting on a claim." }, { "id": "watch", "kind": "timeline", - "title": "What to watch", + "title": "Questions worth checking", "span": "one", "items": [ { - "label": "This week", - "title": "Provider portability", - "description": "Look for concrete export formats, not broad interoperability claims.", - "tone": "info" - }, - { - "label": "Next", - "title": "Access by default", - "description": "Watch whether private deployments ship authentication as a first-class step.", - "tone": "warning" + "label": "Portability", + "title": "Can you export your data?", + "description": "Look for a documented export format before adopting a new tool.", + "tone": "neutral" }, { - "label": "This quarter", - "title": "Portable evidence", - "description": "Watch whether exports preserve provenance and schema versions, not only rendered summaries.", - "tone": "positive" + "label": "Privacy", + "title": "Is access private by default?", + "description": "Check the actual access settings, not only the product description.", + "tone": "neutral" } ] - }, - { - "id": "editor-note", - "kind": "text", - "title": "Reading note", - "body": "This synthetic briefing favors developments with a concrete decision consequence. Topics without new evidence stay out of the way.", - "span": "full" - }, - { - "id": "topic-coverage", - "kind": "bar-chart", - "title": "Retained topic coverage", - "description": "Story-topic matches after duplicates and low-impact items were removed.", - "span": "one", - "bars": [ - { "label": "AI systems", "value": 2, "tone": "info" }, - { "label": "Engineering", "value": 2, "tone": "positive" }, - { "label": "Privacy", "value": 2, "tone": "warning" }, - { "label": "Product", "value": 1, "tone": "neutral" } - ] - }, - { - "id": "evidence-register", - "kind": "table", - "title": "Evidence register", - "span": "one", - "columns": [ - { "key": "story", "label": "Development" }, - { "key": "published", "label": "Published" }, - { "key": "watch", "label": "Watch next" } - ], - "rows": [ - { "story": "Agent standards", "published": "06:30 UTC", "watch": "Export formats" }, - { "story": "Small releases", "published": "06:20 UTC", "watch": "Measured reliability" }, - { "story": "Data-layer privacy", "published": "06:10 UTC", "watch": "Default access" }, - { "story": "Encrypted exports", "published": "05:50 UTC", "watch": "Portable provenance" }, - { "story": "Schema registries", "published": "05:40 UTC", "watch": "Stable fact models" } - ] - }, - { - "id": "demo-warning", - "kind": "notice", - "title": "Illustrative reporting only", - "body": "Every story and link on this page is synthetic. A real worker must retain direct HTTPS evidence, compare publication time with event time, and clearly separate reporting from inference.", - "tone": "warning", - "span": "full" } ] } diff --git a/data/examples/overview/daily/2026-08-24.json b/data/examples/overview/daily/2026-08-24.json index 158b354..5d9c891 100644 --- a/data/examples/overview/daily/2026-08-24.json +++ b/data/examples/overview/daily/2026-08-24.json @@ -6,25 +6,65 @@ "domain": "overview", "source": "daily", "generated_at": "2026-08-24T07:35:00Z", - "effective_period": { "start": "2026-08-24T00:00:00Z", "end": "2026-08-24T23:59:59Z", "timezone": "UTC" }, + "effective_period": { + "start": "2026-08-24T00:00:00Z", + "end": "2026-08-24T23:59:59Z", + "timezone": "UTC" + }, "status": "partial", - "producer": { "worker_id": "overview-daily", "workflow": "example", "version": "0.1.1" }, + "producer": { + "worker_id": "overview-daily", + "workflow": "example", + "version": "0.1.1" + }, "sources": [ - { "label": "Synthetic agenda", "status": "ok", "as_of": "2026-08-24T07:20:00Z", "reference": "agenda:primary:2026-08-24" }, - { "label": "Synthetic inbox", "status": "ok", "as_of": "2026-08-24T07:15:00Z", "reference": "inbox:attention:2026-08-24" }, - { "label": "Synthetic work", "status": "ok", "as_of": "2026-08-24T07:10:00Z", "reference": "work:focus:2026-08-24" }, - { "label": "Synthetic money", "status": "stale", "as_of": "2026-08-23T18:00:00Z", "reference": "money:pulse:2026-08-24" }, - { "label": "Synthetic news", "status": "ok", "as_of": "2026-08-24T06:55:00Z", "reference": "news:briefing:2026-08-24" } + { + "label": "Synthetic agenda", + "status": "ok", + "as_of": "2026-08-24T07:20:00Z", + "reference": "agenda:primary:2026-08-24" + }, + { + "label": "Synthetic inbox", + "status": "ok", + "as_of": "2026-08-24T07:15:00Z", + "reference": "inbox:attention:2026-08-24" + }, + { + "label": "Synthetic work", + "status": "ok", + "as_of": "2026-08-24T07:10:00Z", + "reference": "work:focus:2026-08-24" + }, + { + "label": "Synthetic money", + "status": "stale", + "as_of": "2026-08-23T18:00:00Z", + "reference": "money:pulse:2026-08-24" + }, + { + "label": "Synthetic news", + "status": "ok", + "as_of": "2026-08-24T06:55:00Z", + "reference": "news:briefing:2026-08-24" + } ], - "freshness": { "expires_at": "2026-08-25T13:35:00Z", "next_expected_at": "2026-08-25T07:35:00Z" }, + "freshness": { + "expires_at": "2026-08-25T13:35:00Z", + "next_expected_at": "2026-08-25T07:35:00Z" + }, "quality": { "confidence": "medium", "warnings": ["The synthetic money source is intentionally marked stale to demonstrate a real failure state."] }, - "privacy": { "classification": "public", "contains_personal_data": false, "synthetic": true }, + "privacy": { + "classification": "public", + "contains_personal_data": false, + "synthetic": true + }, "data": { - "title": "Good morning. Your day has shape.", - "summary": "Protect the first focus block, answer two time-sensitive messages, and leave the afternoon buffer intact.", + "title": "Today", + "summary": "Your schedule and priorities in one place. Protect 3.5 hours for the website refresh, handle the two message deadlines, and leave the late-afternoon buffer open.", "attention": "medium", "facts": { "decisions": [ @@ -37,7 +77,7 @@ { "id": "batch-replies", "title": "Use two bounded reply windows", - "reason": "The inbox has four actionable messages but only two carry hard timing constraints.", + "reason": "Three messages need a response; two have explicit deadlines today.", "source_ids": ["inbox:attention:2026-08-24", "agenda:primary:2026-08-24"] }, { @@ -56,8 +96,8 @@ }, { "id": "ship-slice", - "title": "Finish the complete renderer slice", - "reason": "It is startable, high priority, and supported by the protected morning block.", + "title": "Apply the homepage feedback", + "reason": "The current work snapshot marks this as in progress.", "source_ids": ["work:focus:2026-08-24", "agenda:primary:2026-08-24"] }, { @@ -74,157 +114,132 @@ { "id": "day-at-glance", "kind": "metric-group", - "title": "Day at a glance", + "title": "Today at a glance", "span": "full", "metrics": [ - { "label": "Focus time", "value": 3.5, "unit": "hours", "tone": "positive" }, - { "label": "Needs a reply", "value": 4, "format": "number", "tone": "warning" }, - { "label": "Open blockers", "value": 1, "format": "number", "tone": "neutral" }, - { "label": "Goal pace", "value": 78, "format": "percent", "change": 6, "change_label": "this week", "tone": "positive" } - ] - }, - { - "id": "attention-shape", - "kind": "line-chart", - "title": "Attention shape", - "description": "A synthetic seven-day view of completed focus blocks and interruptions.", - "span": "two", - "y_format": "number", - "series": [ - { "key": "focus", "label": "Focus blocks" }, - { "key": "interruptions", "label": "Interruptions" } - ], - "points": [ - { "x": "Mon", "values": { "focus": 2, "interruptions": 5 } }, - { "x": "Tue", "values": { "focus": 3, "interruptions": 4 } }, - { "x": "Wed", "values": { "focus": 3, "interruptions": 3 } }, - { "x": "Thu", "values": { "focus": 4, "interruptions": 2 } }, - { "x": "Fri", "values": { "focus": 3, "interruptions": 3 } }, - { "x": "Sat", "values": { "focus": 1, "interruptions": 1 } }, - { "x": "Sun", "values": { "focus": 2, "interruptions": 1 } } + { + "label": "Focus scheduled", + "value": 3.5, + "unit": "hours" + }, + { + "label": "Messages due today", + "value": 2 + }, + { + "label": "Work in progress", + "value": 1 + } ] }, { "id": "today", "kind": "calendar", - "title": "Today", - "description": "Four commitments with one protected buffer.", - "span": "one", + "title": "Coming up today", + "description": "The same schedule shown in Agenda, with time left after 15:30.", + "span": "two", "date": "2026-08-24", "events": [ { - "id": "focus-1", - "title": "Build the first usable slice", + "id": "a1", + "title": "Finish the homepage", "start": "2026-08-24T08:30:00Z", "end": "2026-08-24T10:30:00Z", "tone": "positive" }, - { "id": "sync", "title": "Project sync", "start": "2026-08-24T11:00:00Z", "end": "2026-08-24T11:30:00Z", "tone": "info" }, { - "id": "focus-2", - "title": "Review and ship", + "id": "a2", + "title": "Project check-in", + "start": "2026-08-24T11:00:00Z", + "end": "2026-08-24T11:30:00Z", + "tone": "info" + }, + { + "id": "a3", + "title": "Review the homepage", "start": "2026-08-24T13:00:00Z", "end": "2026-08-24T14:30:00Z", "tone": "positive" }, - { "id": "buffer", "title": "Open buffer", "start": "2026-08-24T15:00:00Z", "end": "2026-08-24T16:00:00Z", "tone": "neutral" } + { + "id": "a4", + "title": "Walk and reset", + "start": "2026-08-24T15:00:00Z", + "end": "2026-08-24T15:30:00Z", + "tone": "neutral" + } ] }, { "id": "next-actions", "kind": "list", - "title": "Decide or do", + "title": "Needs attention", "description": "Only actions with a concrete next step appear here.", "span": "one", "items": [ { - "id": "reply", - "title": "Reply to the scheduling question", - "description": "Confirm one of the two proposed windows.", - "meta": "Before 10:00", - "status": "Needs reply", + "id": "renewal", + "title": "Confirm the renewal choice", + "description": "Send a yes-or-no answer to the operations contact.", + "meta": "Due today, 15:00 UTC", + "status": "Deadline", "tone": "warning" }, { - "id": "review", - "title": "Review the small release", - "description": "Check the acceptance criteria and approve or request changes.", - "meta": "20 minutes", - "status": "Ready", - "tone": "positive" - }, - { - "id": "plan", - "title": "Choose tomorrow's first task", - "description": "Write a single startable action before ending the day.", - "meta": "End of day", - "status": "Later", + "id": "planning", + "title": "Choose a planning window", + "description": "Pick one of the two proposed times for tomorrow.", + "meta": "Due today, 17:00 UTC", + "status": "Decision", "tone": "neutral" } ] }, { - "id": "goal-progress", - "kind": "progress", - "title": "Goals in motion", - "span": "one", - "items": [ - { "label": "Weekly deep work", "value": 7.5, "max": 10, "value_label": "7.5 of 10 hours", "tone": "positive" }, - { "label": "Movement", "value": 4, "max": 5, "value_label": "4 of 5 sessions", "tone": "info" }, - { "label": "Admin cleared", "value": 6, "max": 10, "value_label": "6 of 10 items", "tone": "warning" } - ] - }, - { - "id": "protect-buffer", - "kind": "notice", - "title": "Protect the buffer", - "body": "The afternoon has enough space to absorb surprises. Filling it now would turn a calm plan into calendar Tetris.", - "tone": "info", - "span": "full" - }, - { - "id": "decision-sequence", - "kind": "timeline", - "title": "Decision sequence", - "span": "two", - "items": [ + "id": "source-register", + "kind": "table", + "title": "What the sources support", + "span": "full", + "columns": [ { - "label": "Before 08:30", - "title": "Open the acceptance criteria", - "description": "Start the focus block with the finish line visible.", - "tone": "positive" + "key": "source", + "label": "Source" }, { - "label": "09:50", - "title": "Resolve the scheduling choice", - "description": "One bounded reply prevents inbox drift.", - "tone": "warning" + "key": "state", + "label": "State" }, { - "label": "13:00", - "title": "Review and ship", - "description": "Use the second focus block for evidence and release.", - "tone": "positive" - }, - { "label": "15:30", "title": "Stop scheduling", "description": "Keep the buffer available for reality.", "tone": "info" } - ] - }, - { - "id": "source-register", - "kind": "table", - "title": "Source state", - "span": "one", - "columns": [ - { "key": "source", "label": "Source" }, - { "key": "state", "label": "State" }, - { "key": "consequence", "label": "Decision consequence" } + "key": "consequence", + "label": "Decision consequence" + } ], "rows": [ - { "source": "Agenda", "state": "Fresh", "consequence": "Schedule is usable" }, - { "source": "Inbox", "state": "Fresh", "consequence": "Four actions retained" }, - { "source": "Work", "state": "Fresh", "consequence": "One startable priority" }, - { "source": "Money", "state": "Partial", "consequence": "No daily movement claim" }, - { "source": "News", "state": "Fresh", "consequence": "No urgent action" } + { + "source": "Agenda", + "state": "Fresh", + "consequence": "Schedule is usable" + }, + { + "source": "Inbox", + "state": "Fresh", + "consequence": "Three responses needed; one conversation waiting" + }, + { + "source": "Work", + "state": "Fresh", + "consequence": "One item in progress; two ready" + }, + { + "source": "Money", + "state": "Partial", + "consequence": "No daily movement claim" + }, + { + "source": "News", + "state": "Fresh", + "consequence": "No urgent action" + } ] } ] diff --git a/data/examples/review/weekly/2026-08-24.json b/data/examples/review/weekly/2026-08-24.json index 40fbe9c..aed0035 100644 --- a/data/examples/review/weekly/2026-08-24.json +++ b/data/examples/review/weekly/2026-08-24.json @@ -6,57 +6,93 @@ "domain": "review", "source": "weekly", "generated_at": "2026-08-24T07:40:00Z", - "effective_period": { "start": "2026-08-18T00:00:00Z", "end": "2026-08-24T23:59:59Z", "timezone": "UTC" }, + "effective_period": { + "start": "2026-08-17T00:00:00Z", + "end": "2026-08-24T07:40:00Z", + "timezone": "UTC" + }, "status": "partial", - "producer": { "worker_id": "review-weekly", "workflow": "example", "version": "0.1.1" }, + "producer": { + "worker_id": "review-weekly", + "workflow": "example", + "version": "0.1.1" + }, "sources": [ - { "label": "Synthetic daily overviews", "status": "ok", "as_of": "2026-08-24T07:35:00Z", "reference": "overview:daily:2026-08-24" }, - { "label": "Synthetic agenda", "status": "ok", "as_of": "2026-08-24T07:20:00Z", "reference": "agenda:primary:2026-08-24" }, - { "label": "Synthetic inbox", "status": "ok", "as_of": "2026-08-24T07:15:00Z", "reference": "inbox:attention:2026-08-24" }, - { "label": "Synthetic work", "status": "ok", "as_of": "2026-08-24T07:10:00Z", "reference": "work:focus:2026-08-24" }, - { "label": "Synthetic money pulse", "status": "stale", "as_of": "2026-08-23T18:00:00Z", "reference": "money:pulse:2026-08-24" }, - { "label": "Synthetic news", "status": "ok", "as_of": "2026-08-24T06:55:00Z", "reference": "news:briefing:2026-08-24" } + { + "label": "Synthetic daily overviews", + "status": "ok", + "as_of": "2026-08-24T07:35:00Z", + "reference": "overview:daily:2026-08-24" + }, + { + "label": "Synthetic agenda", + "status": "ok", + "as_of": "2026-08-24T07:20:00Z", + "reference": "agenda:primary:2026-08-24" + }, + { + "label": "Synthetic inbox", + "status": "ok", + "as_of": "2026-08-24T07:15:00Z", + "reference": "inbox:attention:2026-08-24" + }, + { + "label": "Synthetic work", + "status": "ok", + "as_of": "2026-08-24T07:10:00Z", + "reference": "work:focus:2026-08-24" + }, + { + "label": "Synthetic money pulse", + "status": "stale", + "as_of": "2026-08-23T18:00:00Z", + "reference": "money:pulse:2026-08-24" + }, + { + "label": "Synthetic news", + "status": "ok", + "as_of": "2026-08-24T06:55:00Z", + "reference": "news:briefing:2026-08-24" + } ], - "freshness": { "expires_at": "2026-09-01T07:40:00Z", "next_expected_at": "2026-08-31T07:40:00Z" }, - "quality": { "confidence": "medium", "warnings": ["One input was stale, so money-related conclusions are intentionally limited."] }, - "privacy": { "classification": "public", "contains_personal_data": false, "synthetic": true }, + "freshness": { + "expires_at": "2026-09-01T07:40:00Z", + "next_expected_at": "2026-08-31T07:40:00Z" + }, + "quality": { + "confidence": "medium", + "warnings": ["One input was stale, so money-related conclusions are intentionally limited."] + }, + "privacy": { + "classification": "public", + "contains_personal_data": false, + "synthetic": true + }, "data": { - "title": "A calmer week produced better output", - "summary": "Protected focus blocks rose while interruptions fell. The useful next move is to preserve the same morning boundary.", + "title": "Weekly review", + "summary": "Review August 17\u201323 before planning the next week. You recorded 18 focus blocks and 19 interruptions. Thursday had the most focus blocks; this alone does not establish what caused the difference.", "attention": "low", "facts": { "outcomes": [ { - "id": "complete-slice", - "label": "One complete slice shipped", - "status": "positive", - "evidence_snapshot_ids": ["work:focus:2026-08-24", "overview:daily:2026-08-24"] - }, - { - "id": "reply-backlog", - "label": "The actionable inbox returned to a bounded queue", - "status": "positive", - "evidence_snapshot_ids": ["inbox:attention:2026-08-24", "overview:daily:2026-08-24"] + "id": "focus-count", + "label": "18 focus blocks and 19 interruptions recorded across seven days", + "status": "neutral", + "evidence_snapshot_ids": ["overview:daily:2026-08-24"] }, { "id": "money-limited", - "label": "Money movement remained unconfirmed because one source was stale", + "label": "Daily financial returns cannot be confirmed from stale prices", "status": "unknown", "evidence_snapshot_ids": ["money:pulse:2026-08-24"] } ], "patterns": [ { - "id": "protected-mornings", - "label": "Protected mornings coincided with completed work", - "status": "positive", - "evidence_snapshot_ids": ["agenda:primary:2026-08-24", "overview:daily:2026-08-24"] - }, - { - "id": "smaller-queue", - "label": "A smaller active queue made completion and review more predictable", - "status": "positive", - "evidence_snapshot_ids": ["work:focus:2026-08-24", "inbox:attention:2026-08-24"] + "id": "variation", + "label": "Daily focus varied from one to four blocks; no causal conclusion is supported", + "status": "neutral", + "evidence_snapshot_ids": ["overview:daily:2026-08-24"] } ], "next_actions": [ @@ -68,135 +104,159 @@ }, { "id": "two-reply-windows", - "label": "Keep inbox handling to two bounded windows", + "label": "Try reply windows at 08:00 and 16:00", "status": "neutral", "evidence_snapshot_ids": ["inbox:attention:2026-08-24", "overview:daily:2026-08-24"] } ] }, "presentation": { - "layout": "timeline", + "layout": "dashboard", "blocks": [ { "id": "week-metrics", "kind": "metric-group", - "title": "Week in numbers", + "title": "Week totals", "span": "full", "metrics": [ - { "label": "Focus blocks", "value": 18, "change": 4, "change_label": "vs last week", "tone": "positive" }, - { "label": "Completed decisions", "value": 9, "tone": "positive" }, - { "label": "Carried forward", "value": 3, "tone": "warning" }, - { "label": "Inbox actions", "value": 11, "change": -5, "change_label": "vs last week", "tone": "positive" }, - { "label": "Evidence gaps", "value": 1, "tone": "warning" } - ] - }, - { - "id": "week-goals", - "kind": "progress", - "title": "Commitments", - "span": "one", - "items": [ - { "label": "Deep work", "value": 9.2, "max": 10, "value_label": "92%", "tone": "positive" }, - { "label": "Movement", "value": 4, "max": 5, "value_label": "4 of 5", "tone": "info" }, - { "label": "Weekly cleanup", "value": 1, "max": 1, "value_label": "Done", "tone": "positive" } - ] - }, - { - "id": "week-story", - "kind": "timeline", - "title": "What changed", - "span": "two", - "items": [ { - "label": "Monday", - "title": "Reduced the active queue", - "description": "Three low-value items were deliberately removed.", - "tone": "positive" + "label": "Focus blocks", + "value": 18 }, { - "label": "Wednesday", - "title": "Protected the first focus block", - "description": "A meeting was moved instead of splitting the morning.", - "tone": "info" + "label": "Interruptions", + "value": 19 }, { - "label": "Friday", - "title": "Shipped a complete slice", - "description": "The smaller scope made review faster and clearer.", - "tone": "positive" + "label": "Days recorded", + "value": 7 } ] }, - { - "id": "week-decision", - "kind": "notice", - "title": "Decision for next week", - "body": "Keep the first two hours meeting-free on at least three weekdays. The evidence is behavioral, not motivational: those mornings produced the week's completed work.", - "tone": "positive", - "span": "full" - }, { "id": "focus-trend", "kind": "line-chart", - "title": "Focus and interruption trend", - "description": "Synthetic daily counts retained from the week's normalized snapshots.", + "title": "Focus through the week", + "description": "Daily recorded counts, August 17\u201323. Focus blocks and interruptions are different measures, not hours.", "span": "two", "series": [ - { "key": "focus", "label": "Focus blocks" }, - { "key": "interruptions", "label": "Interruptions" } + { + "key": "focus", + "label": "Focus blocks" + }, + { + "key": "interruptions", + "label": "Interruptions" + } ], "points": [ - { "x": "Mon", "values": { "focus": 2, "interruptions": 5 } }, - { "x": "Tue", "values": { "focus": 3, "interruptions": 4 } }, - { "x": "Wed", "values": { "focus": 3, "interruptions": 3 } }, - { "x": "Thu", "values": { "focus": 4, "interruptions": 2 } }, - { "x": "Fri", "values": { "focus": 3, "interruptions": 3 } }, - { "x": "Sat", "values": { "focus": 1, "interruptions": 1 } }, - { "x": "Sun", "values": { "focus": 2, "interruptions": 1 } } - ] - }, - { - "id": "carry-forward", - "kind": "table", - "title": "Carry-forward register", - "span": "one", - "columns": [ - { "key": "decision", "label": "Decision" }, - { "key": "evidence", "label": "Evidence" }, - { "key": "first_step", "label": "First step" } - ], - "rows": [ - { "decision": "Protect three mornings", "evidence": "Completed work", "first_step": "Block Monday 08:30" }, - { "decision": "Use two reply windows", "evidence": "Smaller queue", "first_step": "Set 09:50 and 16:00" }, - { "decision": "Recheck money source", "evidence": "Stale pricing", "first_step": "Wait for valid refresh" } + { + "x": "Mon", + "values": { + "focus": 2, + "interruptions": 5 + } + }, + { + "x": "Tue", + "values": { + "focus": 3, + "interruptions": 4 + } + }, + { + "x": "Wed", + "values": { + "focus": 3, + "interruptions": 3 + } + }, + { + "x": "Thu", + "values": { + "focus": 4, + "interruptions": 2 + } + }, + { + "x": "Fri", + "values": { + "focus": 3, + "interruptions": 3 + } + }, + { + "x": "Sat", + "values": { + "focus": 1, + "interruptions": 1 + } + }, + { + "x": "Sun", + "values": { + "focus": 2, + "interruptions": 1 + } + } ] }, { "id": "next-week-actions", "kind": "list", - "title": "First actions", - "span": "full", + "title": "Try next week", + "span": "one", "items": [ { "id": "n1", "title": "Reserve Monday, Wednesday, and Friday mornings", - "description": "Keep the first two hours meeting-free.", + "description": "Try keeping two hours free on three mornings, then compare next week\u2019s recorded focus.", "status": "Calendar", "tone": "positive" }, { "id": "n2", "title": "Set two inbox windows", - "description": "Use 09:50 for decisions and 16:00 for reviews and follow-ups.", + "description": "Reply at 08:00 and 16:00 so messages do not interrupt the 08:30 focus block.", "status": "Boundary", "tone": "info" + } + ] + }, + { + "id": "review-evidence", + "kind": "table", + "title": "What this review can establish", + "span": "full", + "columns": [ + { + "key": "measure", + "label": "Evidence" + }, + { + "key": "finding", + "label": "Finding" + }, + { + "key": "limit", + "label": "Limit" + } + ], + "rows": [ + { + "measure": "Seven daily observations", + "finding": "18 focus blocks; 19 interruptions", + "limit": "Counts do not measure output quality or explain causes" + }, + { + "measure": "Latest money snapshot", + "finding": "One price source is stale", + "limit": "No verified weekly return" }, { - "id": "n3", - "title": "Wait for reconciled money evidence", - "description": "Do not convert a stale source into a weekly conclusion.", - "status": "Evidence", - "tone": "warning" + "measure": "Current work queue", + "finding": "One item in progress", + "limit": "No complete shipment history for the week" } ] } diff --git a/data/examples/work/focus/2026-08-24.json b/data/examples/work/focus/2026-08-24.json index d23223d..5e93fc1 100644 --- a/data/examples/work/focus/2026-08-24.json +++ b/data/examples/work/focus/2026-08-24.json @@ -6,54 +6,78 @@ "domain": "work", "source": "focus", "generated_at": "2026-08-24T07:10:00Z", - "effective_period": { "start": "2026-08-24T00:00:00Z", "end": "2026-08-24T23:59:59Z", "timezone": "UTC" }, + "effective_period": { + "start": "2026-08-24T00:00:00Z", + "end": "2026-08-24T23:59:59Z", + "timezone": "UTC" + }, "status": "success", - "producer": { "worker_id": "work-focus-daily", "workflow": "example", "version": "0.1.1" }, - "sources": [{ "label": "Synthetic task tracker", "status": "ok", "as_of": "2026-08-24T07:10:00Z", "reference": "synthetic://tasks" }], - "freshness": { "expires_at": "2026-08-25T19:10:00Z" }, - "quality": { "confidence": "high", "warnings": [] }, - "privacy": { "classification": "public", "contains_personal_data": false, "synthetic": true }, + "producer": { + "worker_id": "work-focus-daily", + "workflow": "example", + "version": "0.1.1" + }, + "sources": [ + { + "label": "Synthetic task tracker", + "status": "ok", + "as_of": "2026-08-24T07:10:00Z", + "reference": "synthetic://tasks" + } + ], + "freshness": { + "expires_at": "2026-08-25T19:10:00Z" + }, + "quality": { + "confidence": "high", + "warnings": [] + }, + "privacy": { + "classification": "public", + "contains_personal_data": false, + "synthetic": true + }, "data": { - "title": "Ship the smallest complete slice", - "summary": "One item is ready to build, one awaits a decision, and one is blocked outside your control.", + "title": "Work", + "summary": "One item is in progress and two are ready. Finish the homepage before starting another item; the usage report remains blocked on access.", "attention": "medium", "facts": { "items": [ { "id": "renderer", - "title": "Adaptive block renderer", + "title": "Website refresh", "status": "in-progress", "priority": "high", - "next_action": "Complete the table component" + "next_action": "Apply the final homepage feedback" }, { "id": "deployment", - "title": "Private deployment recipe", + "title": "Launch checklist", "status": "ready", "priority": "medium", - "next_action": "Verify the Access checklist" + "next_action": "Confirm the release owner" }, { "id": "connector", - "title": "Connector example", + "title": "Usage report", "status": "blocked", "priority": "medium", - "next_action": "Wait for source permission" + "next_action": "Request read-only reporting access" }, { "id": "fixtures", - "title": "Renderer fixture coverage", + "title": "Mobile review", "status": "ready", "priority": "high", - "next_action": "Add one valid example per audited block", + "next_action": "Check the homepage on a phone", "due_at": "2026-08-25T16:00:00Z" }, { "id": "docs", - "title": "Contract extension guide", + "title": "Support guide", "status": "waiting", "priority": "medium", - "next_action": "Review after fixtures land" + "next_action": "Review after the homepage is approved" } ] }, @@ -63,95 +87,105 @@ { "id": "work-counts", "kind": "metric-group", - "title": "Current work", + "title": "Work status", "span": "full", "metrics": [ - { "label": "In progress", "value": 1, "tone": "positive" }, - { "label": "Ready", "value": 2, "tone": "info" }, - { "label": "Blocked", "value": 1, "tone": "warning" }, - { "label": "Waiting", "value": 1, "tone": "neutral" }, - { "label": "Due this week", "value": 2, "tone": "warning" } + { + "label": "In progress", + "value": 1 + }, + { + "label": "Ready to start", + "value": 2 + }, + { + "label": "Blocked", + "value": 1 + }, + { + "label": "Waiting", + "value": 1 + } ] }, { "id": "owned-work", "kind": "table", - "title": "Owned work", - "span": "two", + "title": "Active work", + "description": "Filter by status or priority, sort a column, and find the next step. This is a read-only snapshot of your task tracker.", + "span": "full", "columns": [ - { "key": "item", "label": "Item" }, - { "key": "state", "label": "State" }, - { "key": "next", "label": "Next action" } + { + "key": "item", + "label": "Task" + }, + { + "key": "state", + "label": "Status", + "filterable": true, + "tones": { + "In progress": "info", + "Ready": "positive", + "Blocked": "warning", + "Waiting": "neutral" + } + }, + { + "key": "priority", + "label": "Priority", + "filterable": true, + "tones": { + "High": "warning", + "Medium": "neutral" + } + }, + { + "key": "due", + "label": "Due (UTC)" + }, + { + "key": "next", + "label": "Next step" + } ], "rows": [ - { "item": "Adaptive block renderer", "state": "In progress", "next": "Complete the table component" }, - { "item": "Private deployment recipe", "state": "Ready", "next": "Verify Access checklist" }, - { "item": "Connector example", "state": "Blocked", "next": "Wait for source permission" }, - { "item": "Renderer fixture coverage", "state": "Ready", "next": "Add one example per audited block" }, - { "item": "Contract extension guide", "state": "Waiting", "next": "Review after fixtures land" } - ] - }, - { - "id": "work-next", - "kind": "list", - "title": "Best next moves", - "span": "one", - "items": [ { - "id": "w1", - "title": "Finish one end-to-end path", - "description": "Prefer a complete thin slice over three half-started components.", - "meta": "90 minutes", - "tone": "positive" + "item": "Website refresh", + "state": "In progress", + "next": "Apply the final homepage feedback", + "priority": "High", + "due": "No deadline" }, { - "id": "w2", - "title": "Write the open decision", - "description": "Make the blocker answerable in one message.", - "meta": "10 minutes", - "tone": "info" - } - ] - }, - { - "id": "delivery-progress", - "kind": "progress", - "title": "Delivery evidence", - "span": "one", - "items": [ - { "label": "Complete slice", "value": 7, "max": 10, "value_label": "7 of 10 checks", "tone": "positive" }, - { "label": "Fixture coverage", "value": 8, "max": 11, "value_label": "8 of 11 blocks", "tone": "info" }, - { "label": "Release evidence", "value": 3, "max": 5, "value_label": "3 of 5 gates", "tone": "warning" } - ] - }, - { - "id": "delivery-sequence", - "kind": "timeline", - "title": "Path to done", - "span": "one", - "items": [ + "item": "Launch checklist", + "state": "Ready", + "next": "Confirm the release owner", + "priority": "Medium", + "due": "No deadline" + }, + { + "item": "Usage report", + "state": "Blocked", + "next": "Request read-only reporting access", + "priority": "Medium", + "due": "No deadline" + }, { - "label": "Now", - "title": "Finish the renderer path", - "description": "Implementation, fixture, and test move together.", - "tone": "positive" + "item": "Mobile review", + "state": "Ready", + "next": "Check the homepage on a phone", + "priority": "High", + "due": "Aug 25, 16:00" }, - { "label": "Next", "title": "Run the full contract gate", "description": "Fix only evidence-backed failures.", "tone": "info" }, { - "label": "Then", - "title": "Publish the extension guide", - "description": "Document the exact safe boundary.", - "tone": "neutral" + "item": "Support guide", + "state": "Waiting", + "next": "Review after the homepage is approved", + "priority": "Medium", + "due": "No deadline" } - ] - }, - { - "id": "external-blocker", - "kind": "notice", - "title": "The connector is not startable", - "body": "Source permission is outside the current work boundary. Keep it visible, but do not let it interrupt the complete renderer slice.", - "tone": "warning", - "span": "full" + ], + "searchable": true } ] } diff --git a/docs/assets/dashboard-dark.png b/docs/assets/dashboard-dark.png index 0df619e..c4d7d21 100644 Binary files a/docs/assets/dashboard-dark.png and b/docs/assets/dashboard-dark.png differ diff --git a/docs/assets/dashboard-light.png b/docs/assets/dashboard-light.png index 10ab911..a88ddf0 100644 Binary files a/docs/assets/dashboard-light.png and b/docs/assets/dashboard-light.png differ diff --git a/docs/assets/dashboard-mobile.png b/docs/assets/dashboard-mobile.png index 3a896a6..f95456e 100644 Binary files a/docs/assets/dashboard-mobile.png and b/docs/assets/dashboard-mobile.png differ diff --git a/docs/assets/onboarding-dark.png b/docs/assets/onboarding-dark.png index a06296f..502a3a2 100644 Binary files a/docs/assets/onboarding-dark.png and b/docs/assets/onboarding-dark.png differ diff --git a/docs/assets/onboarding-light.png b/docs/assets/onboarding-light.png index 9c206dd..3902c25 100644 Binary files a/docs/assets/onboarding-light.png and b/docs/assets/onboarding-light.png differ diff --git a/docs/assets/onboarding-mobile.png b/docs/assets/onboarding-mobile.png index 95af4f7..5878543 100644 Binary files a/docs/assets/onboarding-mobile.png and b/docs/assets/onboarding-mobile.png differ diff --git a/docs/design-system.md b/docs/design-system.md index dbf1ef1..cc18728 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -1,84 +1,85 @@ # Design system -Zaati OS is a decision interface, not a marketing dashboard. +Zaati OS helps someone understand their day and decide what deserves attention. Every page needs a clear purpose, evidence for its claims, and a manageable next step. -## Visual language - -- shadcn New York composition -- Tailwind v4 semantic variables -- flat surfaces, quiet borders, no gradients -- compact typography with strong hierarchy -- one dominant visualization with supporting rows, lists, and tables -- restrained status color used for meaning -- brief transitions that respect reduced motion - -## Page anatomy +## Shared frame -1. Source label and effective period -2. One plain-language answer -3. Short explanation -4. Visible freshness or evidence warning -5. Adaptive block grid -6. Provenance footer +All pages use the same available content width and 16/24/32px responsive gutters. The sidebar is 256px wide on desktop. `dashboard`, `focus`, and `timeline` change the internal column arrangement, never the outer page width. Three-column dashboards begin at 1280px; smaller screens stack blocks to protect readable card widths. Long paragraphs can have a readable line length inside that frame. -Repeated facts belong in a table or row. Empty cards, decorative KPIs, arbitrary bold text, and chart-shaped wallpaper do not belong. +The instance's `enabled_sources` order controls navigation. The daily overview comes first in the example instance. Setup and Component lab sit below the everyday pages. The demo opens on Today and offers the tour from Start here instead of interrupting the first visit. Demo freshness is evaluated at the clearly labeled example date; personal instances continue using the real clock. -Long lists and tables disclose an initial decision-sized set, with the remaining validated content available on demand. Metric values wrap without truncation. Line charts use a data-relative domain so meaningful change stays visible; categorical bar charts retain a zero baseline. - -Complexity should come from hierarchy, not ornament. Use `dashboard` for a dominant view plus supporting evidence, `focus` for one leading decision, and `timeline` for a narrow sequence. A block may span one, two, or all dashboard columns. Do not simulate complexity with nested cards, arbitrary component trees, or repeated metrics. +## Visual language -## Components +- Neutral white/charcoal surfaces, quiet gray borders, and flat cards. +- Palette colors belong to charts, actions, and meaningful status indicators. Do not tint the entire workspace. +- Page titles: 24–30px, medium weight. Section titles: 16px, medium. Body: 14px with comfortable line height. Metrics: 24–32px tabular numerals. +- A connected metric strip uses separators, not a stack of colored cards. Values get emphasis through size, not a different background per metric. +- Keep the page title and summary concise. Use ordinary labels such as Agenda, Inbox, and Money. Avoid slogans and motivational claims. +- Static cards do not lift or cast hover shadows. Controls retain obvious focus and hover states. -Owned shadcn-compatible primitives live under `src/components/ui/`. Domain rendering goes through `BlockRenderer`. Add a primitive only when an existing one cannot express the interaction cleanly. +Owned shadcn-compatible primitives live in `src/components/ui/`. Use their shared Card, Button, Input, Dialog, Tabs, Badge, and Progress composition. Preserve semantic tokens and the component generator contract. -The synthetic **Component lab** is the executable catalog: validated JSON appears on the left and the production renderer appears on the right. A new block is not complete until it appears there through a public synthetic fixture, with schema, type, renderer, tests, privacy review, and documentation. +## Page composition -Charts are explorable rather than decorative. Line points and bars expose the same exact-value callout on pointer hover and keyboard focus. Donut legends act as controls that highlight the matching segment and update its center value. Supporting blocks use quiet hover feedback and brief entrance motion; interaction must never be required to recover a fact that is absent from the accessible table, list, or text equivalent. +Start with a purpose statement and a short finding. Follow it with a small metric strip only when the metrics answer useful questions. Put the primary evidence next, then supporting context. Normally use 3–6 blocks; this is guidance, not a quota. A reading page may need only two. -Use semantic tokens such as `background`, `card`, `muted`, `primary`, `warning`, and chart tokens. Do not hardcode provider or source colors inside components. +| Page | Reader's question | Primary evidence | +| ------------- | ------------------------------------------ | ------------------------------------------------------------------ | +| Today | What deserves attention today? | Commitments and explicit message deadlines | +| Agenda | What is happening, and how do I prepare? | Timed events and preparation tasks | +| Inbox | What do I need to answer? | Deadline-ordered response queue, separate waiting list | +| Work | What should move next? | Owned items, current states, concrete next steps | +| Money | What do I have, and is the plan covered? | Balance history, budget allocation, holdings, reserve calculation | +| Briefing | What is worth reading, and why? | Selected summaries, source links, follow-up questions | +| Weekly review | What happened, and what should I try next? | Dated observations, explicit limits, a modest next-week experiment | -## Dashboard composition recipes +Do not repeat a queue as a list, timeline, and table. Do not add topic-count or status-allocation charts when they add no decision value. Do not invent comparison periods, financial scenarios, shipment claims, or causal explanations to make a page look complete. Missing evidence is a valid finding. -The [populated examples](../examples/README.md) are executable composition references. They use the production renderer and existing snapshot schema. Personal data never belongs in examples or screenshots. +## Data display -| Element | Rule | Reason | -| ------------------ | --------------------------------------------------------------------------------------- | ---------------------------------------------------- | -| Primary metric | First metric uses `primary` / `primary-foreground`; values use 24–32px tabular numerals | Establish one visual anchor without a marketing hero | -| Supporting metrics | Semantic accent edge, 9% tinted surface, full-contrast labels | Separate measures while preserving legibility | -| Cartesian charts | Two-column span preferred; 480px minimum canvas with contained horizontal scroll | Keep tick labels readable at 320px | -| Donut | Stack inside narrow cards; switch to side-by-side at a 480px container | Respond to the card rather than viewport width | -| Exact values | Native `View data` disclosure, formatted table; full donut legend values | Make precision available to touch and keyboard users | -| Evidence | Keep dates, stale status, and assumptions adjacent to the decision | Avoid implying live or complete data | -| Tables | Prefer two columns or full width for prose-heavy rows | Avoid unreadable evidence columns | +| Element | Rule | +| --------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| Metrics | Usually 3–4; label the unit and period. A change requires a real comparison. Its sign alone does not establish whether it is good or bad. | +| Tables | Use two columns or full width for prose-heavy rows. Preserve the exact next step beside its item. | +| Lines | Use for ordered observations; keep comparison units compatible. Explain what the data does and does not establish. | +| Bars | Use for meaningful category comparisons with a zero baseline. | +| Donuts | Use only for parts of a meaningful total. The money allocation must reconcile to tracked balances. | +| Calendars | Retain start/end times, all-day state, and the instance time zone. Do not infer available hours without a workday boundary. | +| Evidence | Keep freshness visible; disclose source timestamps and caveats without surrounding every section with warning cards. | -Categorical chart colors distinguish series, not good and bad. Line styles also differ so color is not the only encoding. A numeric increase is not automatically a favorable outcome; producers should supply a change only when its meaning is clear. Metric emphasis indicates priority, not health. +Cartesian charts measure their container so the complete trend remains visible on phones with readable axis labels. Exact data lives in a native `View data` table; only dense data tables scroll horizontally. Donuts adapt to their container width, expose full values in the legend, and support persistent click/tap/keyboard selection. The compact center is a summary, not the only value representation. -### Motion and interaction +Component lab demonstrates all eleven safe block kinds. Small catalog-only examples live in `data/component-examples.json` and are schema-validated. A consumer page should never carry filler just to demonstrate the renderer's capabilities. -Entrance motion runs once, with no perpetual pulsing or autoplay. Blocks settle over 320ms; line and progress reveals use a 520ms clip wipe so the data's geometry never stretches sideways; bars grow over 480ms. Chart interaction changes emphasis without moving the measured endpoint. Static cards remain in place on hover. Reduced-motion preference reduces animations and transitions to 0.01ms. +## Evidence-rich overview and task tables -Donut legend buttons toggle persistent selection with click, tap, Enter, or Space. Hover and focus provide temporary inspection; `aria-pressed` describes selection only. Long category names wrap. The center uses compact notation; full values remain in the legend. Cartesian data is also available through a native disclosure, with no pointer precision required. +Money uses a compact two-by-two metric panel beside the dominant balance-history chart. This puts the current position, comparison, and trajectory in the first section. The next row explains the monthly allocation and asset mix; holdings and reserve goals follow. This is a reusable composition of existing block spans, not a finance-specific React page. -### References and boundaries +Optional `metric.trend` accepts a period `label` and 2–24 labeled numeric `points`. Sparklines use a restrained area fill and an expandable exact-value list. Supply recorded observations, never generated decorative curves. In the synthetic Money example, `facts.balance_history` is the source for all three sparklines and the main chart; the final observation reconciles to accounts. Balance change includes cash flows and is explicitly not described as return. Omit trends when history is unavailable. All points use the metric's unit and format; different sparklines have independent scales and should not be compared by slope. -Composition research: [shadcn dashboard-01](https://ui.shadcn.com/blocks), [chart gallery](https://ui.shadcn.com/charts/area), and [chart documentation](https://ui.shadcn.com/docs/components/chart), reviewed September 5, 2026. Adopted the strong metric row, dominant visualization, contextual table, and progressive disclosure patterns. Zaati retains its owned shadcn-compatible primitives and lightweight SVG renderer; it does not add Recharts or copy reference code. +Optional `table.searchable` enables text search, sortable headers, a matching-item count, and ten-row pagination. Set a column's `filterable: true` to add an exact-value filter. Optional column `tones` maps up to twelve literal cell values to semantic badge tones. Filters combine with search and Clear resets them. Empty results state what happened. Work uses a full-width table for task, status, priority, deadline, and next step. These controls explore the current snapshot; they never imply that a task or account was edited. -Palette changes belong in semantic tokens or validated instance themes. Snapshots select content, spans, and audited block kinds, never CSS, JavaScript, SVG, or arbitrary component trees. The showcase build is explicit and ignores local private configuration. Normal personal builds retain the existing behavior. +These are additive optional fields on existing safe blocks. Existing snapshots remain valid. Forkers must update their schema and renderer together before producing snapshots with these fields. No arbitrary component names, markup, styling, or event handlers are accepted from data. -## Accessibility verification +## Motion and accessibility -All controls need accessible names, keyboard focus, usable touch targets, and sufficient contrast. Color cannot be the only status signal. Charts need an accessible label and should have table or narrative evidence in the snapshot when exact values matter. +Use brief entrance motion, never continuous activity. Charts reveal without changing their final measured coordinates. Existing reduced-motion preferences disable perceptible animation. Controls need usable touch targets, readable contrast, accessible names, and visible keyboard focus. Preserve data equivalents for charts and scroll containment for wide tables. -Review every interface change at desktop and mobile widths in light and dark modes. +Review every source page in desktop/mobile and light/dark. `npm run accessibility:check` covers the responsive matrix, contrast, overflow, chart interactions, reduced motion, and built-in palettes. Automated checks do not replace a human screen-reader or physical-device review. -`npm run accessibility:check` runs axe against the production build in desktop light, desktop dark, and mobile layouts. It is a floor, not a substitute for keyboard, screen-reader, zoom, reduced-motion, and real-device review. +## Research and design decisions -## Performance +Reviewed September 6, 2026. These are composition references and published guidance, not claims that a template alone proves usability. -- keep the dashboard payload separate from cached application JavaScript -- lazy-load charts and onboarding code -- use local system font stacks with no third-party requests -- keep navigation and empty states useful before charts load -- enforce compressed JavaScript, CSS, and data budgets with `npm run performance:check` +| Source | Observation or guidance | Application to Zaati | +| ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| [Studio Admin analytics](https://next-shadcn-admin-dashboard.vercel.app/dashboard/analytics) and the supplied screenshot | Connected metric row; clear comparison periods; detailed data follows summaries | Neutral metric strip, consistent header and gutters, evidence before decoration | +| [Studio Admin tasks](https://next-shadcn-admin-dashboard.vercel.app/dashboard/tasks) | Search, status and priority filters, sorting and pagination | Full-width, filterable Work table with meaningful task content | +| [Studio Admin finance](https://next-shadcn-admin-dashboard.vercel.app/dashboard/finance) | Balance comparisons, spending history, allocation, and account detail | Dense overview with measured history and restrained color | +| [Shadcn UI Kit finance](https://shadcnuikit.com/dashboard/finance) | Balances, expenses, goals, and transactions are distinct sections | Balance and plan views remain distinct; do not copy commercial actions into a read-only snapshot app | +| [Shadcn UI Kit project management](https://shadcnuikit.com/dashboard/project-management) | Project-level information and a detailed project table | Pair each item with its state and next step; skip unrelated revenue and lead metrics | +| [Carbon dashboards](https://carbondesignsystem.com/data-visualization/dashboards/) | Limit nonessential metrics; use hierarchy and consistent spacing/color assignments | Reduce redundant blocks and keep one stable page frame | +| [NN/g complex applications](https://www.nngroup.com/articles/complex-application-design/) | Reduce clutter while preserving access to secondary information | Keep source details and chart values available through disclosure | +| [PatternFly dashboard guidelines](https://www.patternfly.org/patterns/dashboard/design-guidelines/) | Start from requirements and user tasks | Define the question each page answers before choosing its components | -Responsiveness starts at 320 CSS pixels. Touch controls should be at least 40 pixels in the application shell and preserve a visible focus state. +The specific widths, typography, and page recipes above are Zaati design decisions informed by those references. No paid template code, third-party fonts, telemetry, new chart library, or snapshot-controlled markup is introduced. diff --git a/docs/ui-audit-cohesion.md b/docs/ui-audit-cohesion.md new file mode 100644 index 0000000..5d39b77 --- /dev/null +++ b/docs/ui-audit-cohesion.md @@ -0,0 +1,47 @@ +# Page-by-page cohesion audit + +September 6, 2026. This review supersedes the visual direction of the earlier showcase pass. The earlier work improved controls but did not resolve uneven widths, duplicated content, or an overly decorative visual hierarchy. + +## What changed + +| Page | Problem found | Revised value and composition | +| ------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Today | A slogan, historical chart, repeated actions, and an overly narrow source table competed with the current day | Four blocks: daily measures, timetable, two deadlines, source evidence. No motivational headline. | +| Agenda | Focus layout narrowed the page; the calendar was repeated as an energy timeline; open hours lacked a defined workday | Four blocks: derived schedule measures, calendar, prep, one buffer note. | +| Inbox | Four replies were claimed although one item was waiting; the same messages appeared in three views | Three blocks: status counts, deadline-ordered response queue, separate waiting list. | +| Work | Status donut repeated counts; invented delivery-check progress competed with the actual work | Two blocks: state summary and full-width searchable task table, with status/priority filters, sorting, deadlines, and next steps. | +| Money | Decorative projection curves lacked a reproducible model; detail was spread across too many sections | Seven blocks: a compact metric panel with three sparklines beside balance history; budget, allocation, filterable holdings, goals, and price caveat. All history reconciles to facts. | +| Briefing | The narrow narrative layout and topic-count chart did little to help someone choose what to read | Two blocks: selected reading and questions to investigate. Fictional story/link status is explicit. | +| Weekly review | Narrow stacked sections claimed better output and causation that the evidence did not establish | Four blocks: recorded counts, dated trend, next-week experiment, evidence limits. No invented shipment history. | +| Start here | A large promotional heading and technical introduction preceded usable value | Modest welcome, plain purpose, optional tour. The demo initially opens Today. | +| Component lab | Developer content sat among daily tasks; consumer pages carried blocks merely to fill the catalog | Moved below everyday navigation. A schema-validated text fixture preserves full block coverage without filler on source pages. | + +All pages now share the same outer frame, neutral surfaces, section typography, and metric strip. Layout modes remain available for forkers but only alter internal columns. Source ordering follows instance configuration. The default demo and `/examples` use identical source snapshots so visitors do not see an older, less coherent set of pages. + +## Research + +The [design system](design-system.md#research-and-design-decisions) records the web research, direct demo links, and how each reference affected the implementation. The supplied Studio Admin screenshot is the main visual reference. The task and finance demos, plus the supplied financial sparklines and store overview, also informed the denser metric/chart composition and interactive data tables. + +## Review protocol + +Review the production app locally and inspect every page at 1440px and 390px in both themes. Capture each source page under `examples/screenshots`. Run the existing five-width accessibility matrix, check contained horizontal scrolling, inspect metric and body wrapping, exercise chart selection and data disclosure, and verify reduced motion. Run `npm run check` for contracts, privacy, tests, build, tutorial, and performance. + +A calm page may have fewer cards than another page. Consistency means stable navigation, typography, spacing, and interaction; it does not mean adding unsupported metrics to make every page equally full. This is an implementation and expert visual review, not a claim of user testing or accessibility certification. + +## Content integrity checks + +- Today, Agenda, Inbox, and Work agree on the homepage work and message deadlines. Waiting messages are excluded from reply counts. +- Money: cash 22,600 + invested 82,200 = tracked 104,800. The final historical point agrees with those balances. Monthly allocations total 6,500. Emergency reserve uses 15,800 / 3,100; total cash runway uses 22,600 / 3,100. Price freshness remains visible. +- Weekly review: daily focus entries total 18 blocks and interruptions total 19. Observations are not presented as proof of causality. +- Demo page dates use the snapshot generation date, while source/effective-period detail remains available. The sample clock is stable without altering real-instance freshness. + +## Completed validation + +- `npm run check` passed: formatting, lint, repository checks, contracts/privacy, build, 56 tests with coverage gates, tutorial, performance, and accessibility. +- Chromium completed 107 axe audits, including every source page, Start here, and Component lab at 320, 390, 768, 1024, and 1440px in light/dark themes, plus palettes and dialogs. No document overflow was found. +- Search, no-results recovery, status filtering, Clear, ascending/descending sorting, chart data disclosure, donut selection, keyboard activation, and reduced motion passed interaction checks. +- Visual review corrected compact currency wrapping, incomplete mobile chart histories, empty cells in odd-sized metric strips, and obsolete example wording. Cartesian charts now retain the complete history on narrow screens. +- Twenty-eight full-page source screenshots, a Money preview, and six refreshed onboarding/dashboard documentation captures were produced locally from synthetic data. +- Measured build budgets: JavaScript 112 KB gzip, CSS 10 KB gzip, dashboard data 89 KB gzip. No runtime dependency was added. + +The automated accessibility checks and visual inspection do not establish usability with real users or replace assistive-technology testing. diff --git a/docs/ui-audit-showcase.md b/docs/ui-audit-showcase.md index 89a1cba..64dc101 100644 --- a/docs/ui-audit-showcase.md +++ b/docs/ui-audit-showcase.md @@ -1,5 +1,7 @@ # UI audit: template dashboard showcase +Historical audit. See the newer [cohesion review](ui-audit-cohesion.md) for the current visual direction. + Reviewed September 5–6, 2026. Scope: `zaati-os` only. No private dashboard, source snapshot store, scheduled task, or deployment configuration was changed. ## Intent diff --git a/examples/README.md b/examples/README.md index 91a6bcd..4f5f290 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ # Dashboard examples -Three complete, synthetic dashboards rendered by the production shadcn-compatible components. These are starter compositions for forkers, not personal records or live feeds. +Seven complete, synthetic dashboards rendered by the production shadcn-compatible components. These are starter compositions for forkers, not personal records or live feeds. ```sh npm ci @@ -9,17 +9,23 @@ npm run examples Open the local Vite URL with one of these query strings: -| Dashboard | URL query | Reading order | -| -------------- | ------------------------------------------------ | --------------------------------------------------------------------- | -| Daily cockpit | `?view=overview%3Adaily&at=2026-08-24T07:35:00Z` | Available focus, attention pattern, today's schedule, next actions | -| Money overview | `?view=money%3Apulse&at=2026-08-24T07:35:00Z` | Tracked value, scenarios and allocation, monthly plan, exact holdings | -| Work focus | `?view=work%3Afocus&at=2026-08-24T07:35:00Z` | Work in progress, state distribution, next moves, delivery evidence | +| Dashboard | URL query | Reading order | +| ------------- | ------------------------- | ---------------------------------------------------------------- | +| Today | `?view=overview%3Adaily` | Focus time, commitments, reply deadlines, source evidence | +| Agenda | `?view=agenda%3Aprimary` | Schedule, preparation, buffer | +| Inbox | `?view=inbox%3Aattention` | Reply counts, deadline-ordered queue, waiting | +| Work | `?view=work%3Afocus` | Status summary, searchable task table, priorities and next steps | +| Money | `?view=money%3Apulse` | Balance trends, monthly plan, allocation, holdings and goals | +| Briefing | `?view=news%3Abriefing` | Selected reading, source links, follow-up questions | +| Weekly review | `?view=review%3Aweekly` | Weekly measures, daily evidence, next-week experiment | -`at` fixes the review clock to the fixture date. Without it, historical examples correctly appear stale. Every screen retains source dates and uncertainty. Scenarios are illustrative, not predictions. +The example clock defaults to the latest fixture timestamp. An optional `at=2026-08-24T07:35:00Z` query overrides it for reproducible freshness tests. Personal instances use the real clock. Every screen retains source dates and uncertainty. All values, histories, projects, and stories here are synthetic. + +Navigation follows the example instance order. These snapshots match the default demo. ## Adapt an example -Complete envelopes live in `snapshots/{domain}/{source}/2026-08-24.json`. Keep the envelope and use the existing eleven block kinds. Change content, block order, spans, and instance theme before considering renderer changes. Keep a two-column chart next to a one-column supporting block; use full-width tables when text needs room. Put the most important metric first because it receives the high-contrast treatment. +Complete envelopes live in `snapshots/{domain}/{source}/2026-08-24.json`. Keep the envelope and use the existing eleven block kinds. Change content, block order, spans, and instance theme before considering renderer changes. Keep a two-column chart next to a one-column supporting block; use full-width tables when text needs room. Keep the metric strip short and give each measure a clear label and unit. Examples are validated by `npm run data:validate`, including public/synthetic privacy requirements. `npm run examples` explicitly ignores private snapshots and local instance configuration. It builds the shared ignored `public/data/dashboard-data.json`, so stop other local dev servers first. Run `npm run dev` or `npm run build` afterward to restore normal instance selection. Never set `ZAATI_EXAMPLES=true` in a personal production deployment. @@ -40,4 +46,6 @@ Install Chrome or set `CHROME_PATH`. Capture rejects non-synthetic datasets, wai | Money | [Light](screenshots/money-1440-light.png) | [Dark](screenshots/money-1440-dark.png) | [390px](screenshots/money-390-light.png) | | Work | [Light](screenshots/work-1440-light.png) | [Dark](screenshots/work-1440-dark.png) | [390px](screenshots/work-390-light.png) | -See the [design system](../docs/design-system.md) and [UI audit](../docs/ui-audit-showcase.md). +See the [design system](../docs/design-system.md) and [page-by-page audit](../docs/ui-audit-cohesion.md). + +Additional page captures: [Agenda](screenshots/agenda-1440-light.png), [Inbox](screenshots/inbox-1440-light.png), [Briefing](screenshots/news-1440-light.png), and [Weekly review](screenshots/review-1440-light.png). Each also has desktop dark and 390px light/dark variants. diff --git a/examples/screenshots/agenda-1440-dark.png b/examples/screenshots/agenda-1440-dark.png new file mode 100644 index 0000000..02e43c2 Binary files /dev/null and b/examples/screenshots/agenda-1440-dark.png differ diff --git a/examples/screenshots/agenda-1440-light.png b/examples/screenshots/agenda-1440-light.png new file mode 100644 index 0000000..5483b1f Binary files /dev/null and b/examples/screenshots/agenda-1440-light.png differ diff --git a/examples/screenshots/agenda-390-dark.png b/examples/screenshots/agenda-390-dark.png new file mode 100644 index 0000000..402b53c Binary files /dev/null and b/examples/screenshots/agenda-390-dark.png differ diff --git a/examples/screenshots/agenda-390-light.png b/examples/screenshots/agenda-390-light.png new file mode 100644 index 0000000..0812c2d Binary files /dev/null and b/examples/screenshots/agenda-390-light.png differ diff --git a/examples/screenshots/inbox-1440-dark.png b/examples/screenshots/inbox-1440-dark.png new file mode 100644 index 0000000..499f4e4 Binary files /dev/null and b/examples/screenshots/inbox-1440-dark.png differ diff --git a/examples/screenshots/inbox-1440-light.png b/examples/screenshots/inbox-1440-light.png new file mode 100644 index 0000000..3a39743 Binary files /dev/null and b/examples/screenshots/inbox-1440-light.png differ diff --git a/examples/screenshots/inbox-390-dark.png b/examples/screenshots/inbox-390-dark.png new file mode 100644 index 0000000..700f35c Binary files /dev/null and b/examples/screenshots/inbox-390-dark.png differ diff --git a/examples/screenshots/inbox-390-light.png b/examples/screenshots/inbox-390-light.png new file mode 100644 index 0000000..940667f Binary files /dev/null and b/examples/screenshots/inbox-390-light.png differ diff --git a/examples/screenshots/money-1440-dark.png b/examples/screenshots/money-1440-dark.png index 8e95ed3..4bfb8a8 100644 Binary files a/examples/screenshots/money-1440-dark.png and b/examples/screenshots/money-1440-dark.png differ diff --git a/examples/screenshots/money-1440-light.png b/examples/screenshots/money-1440-light.png index fdfe3b1..b592dab 100644 Binary files a/examples/screenshots/money-1440-light.png and b/examples/screenshots/money-1440-light.png differ diff --git a/examples/screenshots/money-390-dark.png b/examples/screenshots/money-390-dark.png index 1528193..9c3dafb 100644 Binary files a/examples/screenshots/money-390-dark.png and b/examples/screenshots/money-390-dark.png differ diff --git a/examples/screenshots/money-390-light.png b/examples/screenshots/money-390-light.png index cbfb1b2..48af2b6 100644 Binary files a/examples/screenshots/money-390-light.png and b/examples/screenshots/money-390-light.png differ diff --git a/examples/screenshots/money-preview.png b/examples/screenshots/money-preview.png new file mode 100644 index 0000000..0dd5a71 Binary files /dev/null and b/examples/screenshots/money-preview.png differ diff --git a/examples/screenshots/news-1440-dark.png b/examples/screenshots/news-1440-dark.png new file mode 100644 index 0000000..40d78b4 Binary files /dev/null and b/examples/screenshots/news-1440-dark.png differ diff --git a/examples/screenshots/news-1440-light.png b/examples/screenshots/news-1440-light.png new file mode 100644 index 0000000..c4602f3 Binary files /dev/null and b/examples/screenshots/news-1440-light.png differ diff --git a/examples/screenshots/news-390-dark.png b/examples/screenshots/news-390-dark.png new file mode 100644 index 0000000..dd97efb Binary files /dev/null and b/examples/screenshots/news-390-dark.png differ diff --git a/examples/screenshots/news-390-light.png b/examples/screenshots/news-390-light.png new file mode 100644 index 0000000..7b2a025 Binary files /dev/null and b/examples/screenshots/news-390-light.png differ diff --git a/examples/screenshots/overview-1440-dark.png b/examples/screenshots/overview-1440-dark.png index 5a43884..c4d7d21 100644 Binary files a/examples/screenshots/overview-1440-dark.png and b/examples/screenshots/overview-1440-dark.png differ diff --git a/examples/screenshots/overview-1440-light.png b/examples/screenshots/overview-1440-light.png index d6d3a9b..a88ddf0 100644 Binary files a/examples/screenshots/overview-1440-light.png and b/examples/screenshots/overview-1440-light.png differ diff --git a/examples/screenshots/overview-390-dark.png b/examples/screenshots/overview-390-dark.png index cd81a20..f95456e 100644 Binary files a/examples/screenshots/overview-390-dark.png and b/examples/screenshots/overview-390-dark.png differ diff --git a/examples/screenshots/overview-390-light.png b/examples/screenshots/overview-390-light.png index 519af3c..1313dea 100644 Binary files a/examples/screenshots/overview-390-light.png and b/examples/screenshots/overview-390-light.png differ diff --git a/examples/screenshots/review-1440-dark.png b/examples/screenshots/review-1440-dark.png new file mode 100644 index 0000000..675ec00 Binary files /dev/null and b/examples/screenshots/review-1440-dark.png differ diff --git a/examples/screenshots/review-1440-light.png b/examples/screenshots/review-1440-light.png new file mode 100644 index 0000000..9d38eb6 Binary files /dev/null and b/examples/screenshots/review-1440-light.png differ diff --git a/examples/screenshots/review-390-dark.png b/examples/screenshots/review-390-dark.png new file mode 100644 index 0000000..60068b2 Binary files /dev/null and b/examples/screenshots/review-390-dark.png differ diff --git a/examples/screenshots/review-390-light.png b/examples/screenshots/review-390-light.png new file mode 100644 index 0000000..9505484 Binary files /dev/null and b/examples/screenshots/review-390-light.png differ diff --git a/examples/screenshots/work-1440-dark.png b/examples/screenshots/work-1440-dark.png index 2a18804..121e4c0 100644 Binary files a/examples/screenshots/work-1440-dark.png and b/examples/screenshots/work-1440-dark.png differ diff --git a/examples/screenshots/work-1440-light.png b/examples/screenshots/work-1440-light.png index 3a8363f..f756ad1 100644 Binary files a/examples/screenshots/work-1440-light.png and b/examples/screenshots/work-1440-light.png differ diff --git a/examples/screenshots/work-390-dark.png b/examples/screenshots/work-390-dark.png index aaaf946..dbefee2 100644 Binary files a/examples/screenshots/work-390-dark.png and b/examples/screenshots/work-390-dark.png differ diff --git a/examples/screenshots/work-390-light.png b/examples/screenshots/work-390-light.png index fffdcbb..8fc8d16 100644 Binary files a/examples/screenshots/work-390-light.png and b/examples/screenshots/work-390-light.png differ diff --git a/examples/snapshots/agenda/primary/2026-08-24.json b/examples/snapshots/agenda/primary/2026-08-24.json new file mode 100644 index 0000000..2f4e8dd --- /dev/null +++ b/examples/snapshots/agenda/primary/2026-08-24.json @@ -0,0 +1,196 @@ +{ + "schema_version": "0.1.1", + "schema_ref": "schemas/domains/agenda.schema.json", + "snapshot_id": "agenda:primary:2026-08-24", + "source_id": "agenda:primary", + "domain": "agenda", + "source": "primary", + "generated_at": "2026-08-24T07:20:00Z", + "effective_period": { + "start": "2026-08-24T00:00:00Z", + "end": "2026-08-24T23:59:59Z", + "timezone": "UTC" + }, + "status": "success", + "producer": { + "worker_id": "agenda-daily", + "workflow": "example", + "version": "0.1.1" + }, + "sources": [ + { + "label": "Synthetic calendar", + "status": "ok", + "as_of": "2026-08-24T07:20:00Z", + "reference": "synthetic://calendar" + } + ], + "freshness": { + "expires_at": "2026-08-25T13:20:00Z" + }, + "quality": { + "confidence": "high", + "warnings": [] + }, + "privacy": { + "classification": "public", + "contains_personal_data": false, + "synthetic": true + }, + "data": { + "title": "Agenda", + "summary": "See today\u2019s commitments and prepare for the next one. Two focus windows total 3.5 hours; the only meeting takes 30 minutes.", + "attention": "low", + "facts": { + "events": [ + { + "id": "a1", + "title": "Finish the homepage", + "start": "2026-08-24T08:30:00Z", + "end": "2026-08-24T10:30:00Z", + "status": "scheduled" + }, + { + "id": "a2", + "title": "Project check-in", + "start": "2026-08-24T11:00:00Z", + "end": "2026-08-24T11:30:00Z", + "status": "scheduled" + }, + { + "id": "a3", + "title": "Review the homepage", + "start": "2026-08-24T13:00:00Z", + "end": "2026-08-24T14:30:00Z", + "status": "scheduled" + }, + { + "id": "a4", + "title": "Walk and reset", + "start": "2026-08-24T15:00:00Z", + "end": "2026-08-24T15:30:00Z", + "status": "scheduled" + } + ], + "actions": [ + { + "id": "p1", + "title": "Open the homepage feedback", + "due_at": "2026-08-24T08:30:00Z", + "status": "open" + }, + { + "id": "p2", + "title": "List the decision needed at check-in", + "due_at": "2026-08-24T11:00:00Z", + "status": "open" + }, + { + "id": "p3", + "title": "Prepare the review link", + "due_at": "2026-08-24T13:00:00Z", + "status": "open" + } + ] + }, + "presentation": { + "layout": "dashboard", + "blocks": [ + { + "id": "day-capacity", + "kind": "metric-group", + "title": "Today\u2019s commitments", + "span": "full", + "metrics": [ + { + "label": "Focus time", + "value": 3.5, + "unit": "hours" + }, + { + "label": "Meeting time", + "value": 30, + "unit": "minutes" + }, + { + "label": "Scheduled events", + "value": 4 + } + ] + }, + { + "id": "schedule", + "kind": "calendar", + "title": "Monday\u2019s schedule", + "date": "2026-08-24", + "span": "two", + "events": [ + { + "id": "a1", + "title": "Finish the homepage", + "start": "2026-08-24T08:30:00Z", + "end": "2026-08-24T10:30:00Z", + "tone": "positive" + }, + { + "id": "a2", + "title": "Project check-in", + "start": "2026-08-24T11:00:00Z", + "end": "2026-08-24T11:30:00Z", + "tone": "info" + }, + { + "id": "a3", + "title": "Review the homepage", + "start": "2026-08-24T13:00:00Z", + "end": "2026-08-24T14:30:00Z", + "tone": "positive" + }, + { + "id": "a4", + "title": "Walk and reset", + "start": "2026-08-24T15:00:00Z", + "end": "2026-08-24T15:30:00Z", + "tone": "neutral" + } + ], + "description": "August 24 \u00b7 all times in UTC" + }, + { + "id": "prep", + "kind": "list", + "title": "Before each commitment", + "span": "one", + "items": [ + { + "id": "p1", + "title": "Open the homepage feedback", + "meta": "Before 08:30", + "tone": "neutral" + }, + { + "id": "p2", + "title": "List the decision needed at check-in", + "meta": "Before 11:00", + "tone": "info" + }, + { + "id": "p3", + "title": "Prepare the review link", + "meta": "Before 13:00", + "tone": "neutral" + } + ] + }, + { + "id": "buffer", + "kind": "notice", + "title": "Keep 15:30 onward unclaimed", + "body": "That window absorbs a late review, travel, or an unfinished release note without displacing recovery time.", + "tone": "info", + "span": "full" + } + ] + } + } +} diff --git a/examples/snapshots/inbox/attention/2026-08-24.json b/examples/snapshots/inbox/attention/2026-08-24.json new file mode 100644 index 0000000..9f213e3 --- /dev/null +++ b/examples/snapshots/inbox/attention/2026-08-24.json @@ -0,0 +1,153 @@ +{ + "schema_version": "0.1.1", + "schema_ref": "schemas/domains/inbox.schema.json", + "snapshot_id": "inbox:attention:2026-08-24", + "source_id": "inbox:attention", + "domain": "inbox", + "source": "attention", + "generated_at": "2026-08-24T07:15:00Z", + "effective_period": { + "start": "2026-08-23T07:15:00Z", + "end": "2026-08-24T07:15:00Z", + "timezone": "UTC" + }, + "status": "success", + "producer": { + "worker_id": "inbox-attention-daily", + "workflow": "example", + "version": "0.1.1" + }, + "sources": [ + { + "label": "Synthetic inbox", + "status": "ok", + "as_of": "2026-08-24T07:15:00Z", + "reference": "synthetic://inbox" + } + ], + "freshness": { + "expires_at": "2026-08-25T13:15:00Z" + }, + "quality": { + "confidence": "high", + "warnings": [] + }, + "privacy": { + "classification": "public", + "contains_personal_data": false, + "synthetic": true + }, + "data": { + "title": "Inbox", + "summary": "Three messages need your response, including two due today. One conversation is waiting on someone else.", + "attention": "medium", + "facts": { + "items": [ + { + "id": "m1", + "sender_label": "Project collaborator", + "subject_summary": "Choose a planning window", + "reason": "decision", + "deadline": "2026-08-24T17:00:00Z", + "status": "open" + }, + { + "id": "m2", + "sender_label": "Review partner", + "subject_summary": "Approve the revised outline", + "reason": "reply", + "status": "open" + }, + { + "id": "m3", + "sender_label": "Operations contact", + "subject_summary": "Confirm the renewal choice", + "reason": "deadline", + "deadline": "2026-08-24T15:00:00Z", + "status": "open" + }, + { + "id": "m4", + "sender_label": "Research collaborator", + "subject_summary": "Close the open question", + "reason": "follow-up", + "status": "waiting" + } + ] + }, + "presentation": { + "layout": "dashboard", + "blocks": [ + { + "id": "counts", + "kind": "metric-group", + "title": "Message status", + "span": "full", + "metrics": [ + { + "label": "Needs your response", + "value": 3 + }, + { + "label": "Due today", + "value": 2 + }, + { + "label": "Waiting on others", + "value": 1 + } + ] + }, + { + "id": "messages", + "kind": "list", + "title": "Needs your response", + "span": "two", + "items": [ + { + "id": "m3", + "title": "Confirm the renewal choice", + "description": "A yes-or-no choice is due before the afternoon processing window.", + "meta": "Operations contact \u00b7 Due today, 15:00 UTC", + "status": "Deadline", + "tone": "danger" + }, + { + "id": "m1", + "title": "Choose a planning window", + "description": "A collaborator proposed two times for tomorrow.", + "meta": "Project collaborator \u00b7 Due today, 17:00 UTC", + "status": "Decision", + "tone": "warning" + }, + { + "id": "m2", + "title": "Approve the revised outline", + "description": "The requested changes are ready for a quick review.", + "meta": "Review partner \u00b7 No stated deadline", + "status": "Review", + "tone": "info" + } + ] + }, + { + "id": "waiting", + "kind": "list", + "title": "Waiting on a response", + "span": "one", + "description": "No reply needed from you right now.", + "items": [ + { + "id": "m4", + "title": "Research question", + "description": "The collaborator has the question. Follow up if it is still unresolved at your next review.", + "meta": "Research collaborator \u00b7 No deadline", + "status": "Waiting", + "tone": "neutral" + } + ] + } + ] + } + } +} diff --git a/examples/snapshots/money/pulse/2026-08-24.json b/examples/snapshots/money/pulse/2026-08-24.json index 2f27ab0..c8669c9 100644 --- a/examples/snapshots/money/pulse/2026-08-24.json +++ b/examples/snapshots/money/pulse/2026-08-24.json @@ -50,8 +50,8 @@ "synthetic": true }, "data": { - "title": "A clear view of what you have.", - "summary": "104,800 synthetic dollars tracked. Cash covers 7.3 months of the 3,100 monthly living budget. Keep contributions steady, but refresh stale crypto prices before judging daily movement.", + "title": "Money", + "summary": "Track your balances and where your monthly budget goes. Cash covers 7.3 months of living costs; crypto prices are awaiting a refresh.", "attention": "low", "facts": { "measures": [ @@ -216,95 +216,77 @@ "as_of": "2026-08-23T18:00:00Z" } ], - "projections": [ + "balance_history": [ { - "year": 2026, - "scenario": "conservative", - "value": 104800, - "currency": "USD" - }, - { - "year": 2026, - "scenario": "baseline", - "value": 104800, - "currency": "USD" - }, - { - "year": 2026, - "scenario": "stretch", - "value": 104800, - "currency": "USD" - }, - { - "year": 2027, - "scenario": "conservative", - "value": 124000, + "date": "2026-06-08", + "cash": 18700, + "invested": 75500, "currency": "USD" }, { - "year": 2027, - "scenario": "baseline", - "value": 128000, + "date": "2026-06-15", + "cash": 19200, + "invested": 76100, "currency": "USD" }, { - "year": 2027, - "scenario": "stretch", - "value": 132000, + "date": "2026-06-22", + "cash": 18900, + "invested": 75300, "currency": "USD" }, { - "year": 2028, - "scenario": "conservative", - "value": 142000, + "date": "2026-06-29", + "cash": 19600, + "invested": 76800, "currency": "USD" }, { - "year": 2028, - "scenario": "baseline", - "value": 150000, + "date": "2026-07-06", + "cash": 20200, + "invested": 77200, "currency": "USD" }, { - "year": 2028, - "scenario": "stretch", - "value": 160000, + "date": "2026-07-13", + "cash": 19800, + "invested": 77900, "currency": "USD" }, { - "year": 2029, - "scenario": "conservative", - "value": 158000, + "date": "2026-07-20", + "cash": 20400, + "invested": 77300, "currency": "USD" }, { - "year": 2029, - "scenario": "baseline", - "value": 166000, + "date": "2026-07-27", + "cash": 21100, + "invested": 78900, "currency": "USD" }, { - "year": 2029, - "scenario": "stretch", - "value": 181000, + "date": "2026-08-03", + "cash": 20900, + "invested": 79600, "currency": "USD" }, { - "year": 2030, - "scenario": "conservative", - "value": 171000, + "date": "2026-08-10", + "cash": 21600, + "invested": 79100, "currency": "USD" }, { - "year": 2030, - "scenario": "baseline", - "value": 178000, + "date": "2026-08-17", + "cash": 22100, + "invested": 80900, "currency": "USD" }, { - "year": 2030, - "scenario": "stretch", - "value": 205000, + "date": "2026-08-24", + "cash": 22600, + "invested": 82200, "currency": "USD" } ] @@ -315,30 +297,194 @@ { "id": "money-metrics", "kind": "metric-group", - "title": "Current setup", - "description": "Cash runway uses all cash divided by the 3,100 monthly living budget.", - "span": "full", + "title": "Balance overview", + "span": "one", "metrics": [ { "label": "Tracked value", "value": 104800, "format": "currency", - "tone": "positive" + "tone": "positive", + "trend": { + "label": "Jun 8\u2013Aug 24 \u00b7 weekly", + "points": [ + { + "label": "2026-06-08", + "value": 94200 + }, + { + "label": "2026-06-15", + "value": 95300 + }, + { + "label": "2026-06-22", + "value": 94200 + }, + { + "label": "2026-06-29", + "value": 96400 + }, + { + "label": "2026-07-06", + "value": 97400 + }, + { + "label": "2026-07-13", + "value": 97700 + }, + { + "label": "2026-07-20", + "value": 97700 + }, + { + "label": "2026-07-27", + "value": 100000 + }, + { + "label": "2026-08-03", + "value": 100500 + }, + { + "label": "2026-08-10", + "value": 100700 + }, + { + "label": "2026-08-17", + "value": 103000 + }, + { + "label": "2026-08-24", + "value": 104800 + } + ] + }, + "change": 10600, + "change_label": "since Jun 8" }, { "label": "Invested", "value": 82200, "format": "currency", - "tone": "info" + "tone": "info", + "trend": { + "label": "Jun 8\u2013Aug 24 \u00b7 weekly", + "points": [ + { + "label": "2026-06-08", + "value": 75500 + }, + { + "label": "2026-06-15", + "value": 76100 + }, + { + "label": "2026-06-22", + "value": 75300 + }, + { + "label": "2026-06-29", + "value": 76800 + }, + { + "label": "2026-07-06", + "value": 77200 + }, + { + "label": "2026-07-13", + "value": 77900 + }, + { + "label": "2026-07-20", + "value": 77300 + }, + { + "label": "2026-07-27", + "value": 78900 + }, + { + "label": "2026-08-03", + "value": 79600 + }, + { + "label": "2026-08-10", + "value": 79100 + }, + { + "label": "2026-08-17", + "value": 80900 + }, + { + "label": "2026-08-24", + "value": 82200 + } + ] + }, + "change": 6700, + "change_label": "since Jun 8" }, { "label": "Cash", "value": 22600, "format": "currency", - "tone": "neutral" + "tone": "neutral", + "trend": { + "label": "Jun 8\u2013Aug 24 \u00b7 weekly", + "points": [ + { + "label": "2026-06-08", + "value": 18700 + }, + { + "label": "2026-06-15", + "value": 19200 + }, + { + "label": "2026-06-22", + "value": 18900 + }, + { + "label": "2026-06-29", + "value": 19600 + }, + { + "label": "2026-07-06", + "value": 20200 + }, + { + "label": "2026-07-13", + "value": 19800 + }, + { + "label": "2026-07-20", + "value": 20400 + }, + { + "label": "2026-07-27", + "value": 21100 + }, + { + "label": "2026-08-03", + "value": 20900 + }, + { + "label": "2026-08-10", + "value": 21600 + }, + { + "label": "2026-08-17", + "value": 22100 + }, + { + "label": "2026-08-24", + "value": 22600 + } + ] + }, + "change": 3900, + "change_label": "since Jun 8" }, { - "label": "Runway", + "label": "Cash runway", "value": 7.3, "unit": "months", "tone": "positive" @@ -346,105 +492,99 @@ ] }, { - "id": "five-year-projection", + "id": "balance-history", "kind": "line-chart", - "title": "Three paths to 2030", - "description": "Illustrative annual balances, 2026\u20132030. These are scenarios, not predicted returns.", + "title": "Balance history", + "description": "12 weekly snapshots \u00b7 Jun 8\u2013Aug 24. Changes include contributions, withdrawals, and market movements; they are not investment returns.", "span": "two", + "x_label": "Weekly balance date", "y_format": "currency", "series": [ { - "key": "conservative", - "label": "Conservative" - }, - { - "key": "baseline", - "label": "Baseline" - }, - { - "key": "stretch", - "label": "Stretch" + "key": "total", + "label": "Tracked balance" } ], "points": [ { - "x": "2026", + "x": "Jun 8", "values": { - "conservative": 104800, - "baseline": 104800, - "stretch": 104800 + "total": 94200 } }, { - "x": "2027", + "x": "Jun 15", "values": { - "conservative": 124000, - "baseline": 128000, - "stretch": 132000 + "total": 95300 } }, { - "x": "2028", + "x": "Jun 22", "values": { - "conservative": 142000, - "baseline": 150000, - "stretch": 160000 + "total": 94200 } }, { - "x": "2029", + "x": "Jun 29", "values": { - "conservative": 158000, - "baseline": 166000, - "stretch": 181000 + "total": 96400 } }, { - "x": "2030", + "x": "Jul 6", "values": { - "conservative": 171000, - "baseline": 178000, - "stretch": 205000 + "total": 97400 } - } - ] - }, - { - "id": "allocation", - "kind": "donut-chart", - "title": "Portfolio allocation", - "description": "Cash is included so the chart reconciles to the tracked total.", - "span": "one", - "value_format": "currency", - "center_label": "Tracked total", - "segments": [ + }, { - "label": "Global equity fund", - "value": 49770 + "x": "Jul 13", + "values": { + "total": 97700 + } }, { - "label": "Quality stocks", - "value": 19400 + "x": "Jul 20", + "values": { + "total": 97700 + } }, { - "label": "Bonds", - "value": 7930 + "x": "Jul 27", + "values": { + "total": 100000 + } }, { - "label": "Crypto", - "value": 5100 + "x": "Aug 3", + "values": { + "total": 100500 + } }, { - "label": "Cash", - "value": 22600 + "x": "Aug 10", + "values": { + "total": 100700 + } + }, + { + "x": "Aug 17", + "values": { + "total": 103000 + } + }, + { + "x": "Aug 24", + "values": { + "total": 104800 + } } ] }, { "id": "monthly-flow", "kind": "bar-chart", - "title": "Monthly plan", - "description": "Where the illustrative take-home amount is assigned this month.", + "title": "Monthly budget", + "description": "6,500 total allocated: living, investing, reserve, and flexible spending.", "span": "two", "value_format": "currency", "bars": [ @@ -471,53 +611,47 @@ ] }, { - "id": "money-goals", - "kind": "progress", - "title": "Explicit goals", + "id": "allocation", + "kind": "donut-chart", + "title": "Where the money is", + "description": "Accounted balances, including cash. Crypto valuation is provisional.", "span": "one", - "items": [ + "value_format": "currency", + "center_label": "Tracked total", + "segments": [ { - "label": "Emergency reserve", - "value": 5.1, - "max": 6, - "value_label": "5.1 of 6 months", - "tone": "positive" + "label": "Global equity fund", + "value": 49770 }, { - "label": "Annual investing", - "value": 14400, - "max": 21600, - "value_label": "$14.4k of $21.6k", - "tone": "info" + "label": "Quality stocks", + "value": 19400 + }, + { + "label": "Bonds", + "value": 7930 + }, + { + "label": "Crypto", + "value": 5100 + }, + { + "label": "Cash", + "value": 22600 } - ], - "description": "The 15,800 emergency reserve covers 5.1 months of living costs; daily cash is excluded." + ] }, { "id": "holdings", "kind": "table", "title": "Invested holdings", - "description": "Rounded synthetic positions. Quote timestamps and stale pricing are listed in Connections below.", - "span": "full", + "description": "Rounded market values. Weights use the 104,800 tracked total, including cash.", + "span": "two", "columns": [ { "key": "asset", "label": "Asset" }, - { - "key": "type", - "label": "Type" - }, - { - "key": "units", - "label": "Units", - "format": "number" - }, - { - "key": "price", - "label": "Price", - "format": "currency" - }, { "key": "value", "label": "Value", @@ -529,94 +663,77 @@ "format": "percent" }, { - "key": "day", - "label": "Day", - "format": "percent" + "key": "status", + "label": "Prices", + "filterable": true, + "tones": { + "Current": "neutral", + "Older": "warning" + } } ], "rows": [ { "asset": "WORLD \u00b7 Global equity fund", - "type": "Fund", - "units": 420, - "price": 118.5, "value": 49770, "weight": 47.5, - "day": 0.6 + "status": "Current" }, { "asset": "QUAL \u00b7 Quality companies", - "type": "Stock basket", - "units": 160, - "price": 121.25, "value": 19400, "weight": 18.5, - "day": -0.2 + "status": "Current" }, { "asset": "BOND \u00b7 Short bond fund", - "type": "Bond", - "units": 155, - "price": 51.16, "value": 7930, "weight": 7.6, - "day": 0.1 + "status": "Current" }, { "asset": "BTC \u00b7 Bitcoin", - "type": "Crypto", - "units": 0.062, - "price": 65000, "value": 4030, "weight": 3.8, - "day": 1.4 + "status": "Older" }, { "asset": "ETH \u00b7 Ethereum", - "type": "Crypto", - "units": 0.42, - "price": 2548, "value": 1070, "weight": 1, - "day": -0.8 + "status": "Older" } - ] + ], + "searchable": true }, { - "id": "account-connections", - "kind": "list", - "title": "Connections", - "description": "Only normalized totals are retained\u2014never account numbers or statements.", - "span": "full", + "id": "money-goals", + "kind": "progress", + "title": "Progress toward your goals", + "span": "one", "items": [ { - "id": "bank", - "title": "Banking summary", - "description": "Daily cash and emergency reserve refreshed at 06:40 UTC.", - "status": "Connected", - "tone": "positive" - }, - { - "id": "brokerage", - "title": "Brokerage summary", - "description": "Three long-term positions refreshed at 06:35 UTC.", - "status": "Connected", + "label": "Emergency reserve", + "value": 5.1, + "max": 6, + "value_label": "5.1 of 6 months", "tone": "positive" }, { - "id": "crypto", - "title": "Crypto summary", - "description": "Two positions last priced yesterday at 18:00 UTC.", - "status": "Stale", - "tone": "warning" + "label": "Annual investing", + "value": 14400, + "max": 21600, + "value_label": "$14.4k of $21.6k", + "tone": "info" } - ] + ], + "description": "The 15,800 emergency reserve covers 5.1 months of living costs; daily cash is excluded." }, { "id": "money-warning", "kind": "notice", - "title": "Projection, not promise", - "body": "These scenarios assume a fixed monthly contribution and illustrative returns. Crypto prices are stale, taxes and fees are excluded, and the page is context\u2014not financial advice or an automated decision.", + "title": "Refresh prices before comparing returns", + "body": "The crypto balance uses yesterday\u2019s prices. This view is suitable for an approximate balance overview, but not for a daily return comparison.", "tone": "warning", "span": "full" } diff --git a/examples/snapshots/news/briefing/2026-08-24.json b/examples/snapshots/news/briefing/2026-08-24.json new file mode 100644 index 0000000..24b6563 --- /dev/null +++ b/examples/snapshots/news/briefing/2026-08-24.json @@ -0,0 +1,163 @@ +{ + "schema_version": "0.1.1", + "schema_ref": "schemas/domains/news.schema.json", + "snapshot_id": "news:briefing:2026-08-24", + "source_id": "news:briefing", + "domain": "news", + "source": "briefing", + "generated_at": "2026-08-24T06:55:00Z", + "effective_period": { + "start": "2026-08-23T06:55:00Z", + "end": "2026-08-24T06:55:00Z", + "timezone": "UTC" + }, + "status": "success", + "producer": { + "worker_id": "news-briefing-daily", + "workflow": "example", + "version": "0.1.1" + }, + "sources": [ + { + "label": "Synthetic public reporting", + "status": "ok", + "as_of": "2026-08-24T06:45:00Z", + "reference": "synthetic://news" + } + ], + "freshness": { + "expires_at": "2026-08-25T06:55:00Z" + }, + "quality": { + "confidence": "high", + "warnings": ["Stories and links in demo mode are illustrative, not current news."] + }, + "privacy": { + "classification": "public", + "contains_personal_data": false, + "synthetic": true + }, + "data": { + "title": "Briefing", + "summary": "Five developments selected for your interests, with context for what to investigate next. These example stories and links are fictional.", + "attention": "low", + "facts": { + "stories": [ + { + "id": "n1", + "title": "Open standards make agent workflows easier to move", + "summary": "Teams can change models without rebuilding every downstream view.", + "published_at": "2026-08-24T06:30:00Z", + "url": "https://example.com/ai-standards", + "topics": ["AI systems"] + }, + { + "id": "n2", + "title": "Smaller software releases outperform big-bang delivery", + "summary": "Reliability improves when feedback arrives before architecture hardens.", + "published_at": "2026-08-24T06:20:00Z", + "url": "https://example.com/software-delivery", + "topics": ["Engineering"] + }, + { + "id": "n3", + "title": "Privacy controls move closer to the data layer", + "summary": "Access boundaries become portable across hosting providers.", + "published_at": "2026-08-24T06:10:00Z", + "url": "https://example.com/privacy-controls", + "topics": ["Privacy"] + }, + { + "id": "n4", + "title": "Local-first tools add portable encrypted exports", + "summary": "Users can move durable records without giving the application permanent custody of their history.", + "published_at": "2026-08-24T05:50:00Z", + "url": "https://example.com/local-first-exports", + "topics": ["Privacy", "Product design"] + }, + { + "id": "n5", + "title": "Schema registries become a coordination layer for AI workflows", + "summary": "Teams are separating durable facts from model-specific presentation decisions.", + "published_at": "2026-08-24T05:40:00Z", + "url": "https://example.com/schema-registries", + "topics": ["AI systems", "Engineering"] + } + ] + }, + "presentation": { + "layout": "dashboard", + "blocks": [ + { + "id": "briefing", + "kind": "list", + "title": "Selected reading", + "span": "two", + "items": [ + { + "id": "n1", + "title": "Open standards make agent workflows easier to move", + "description": "Teams can change models without rebuilding every downstream view.", + "meta": "AI systems", + "tone": "neutral", + "href": "https://example.com/ai-standards" + }, + { + "id": "n2", + "title": "Smaller software releases outperform big-bang delivery", + "description": "Reliability improves when feedback arrives before architecture hardens.", + "meta": "Engineering", + "tone": "neutral", + "href": "https://example.com/software-delivery" + }, + { + "id": "n3", + "title": "Privacy controls move closer to the data layer", + "description": "Access boundaries become portable across hosting providers.", + "meta": "Privacy", + "tone": "neutral", + "href": "https://example.com/privacy-controls" + }, + { + "id": "n4", + "title": "Local-first tools add portable encrypted exports", + "description": "Users can move durable records without giving the application permanent custody of their history.", + "meta": "Privacy \u00b7 Product design", + "tone": "neutral", + "href": "https://example.com/local-first-exports" + }, + { + "id": "n5", + "title": "Schema registries become a coordination layer for AI workflows", + "description": "Teams are separating durable facts from model-specific presentation decisions.", + "meta": "AI systems \u00b7 Engineering", + "tone": "neutral", + "href": "https://example.com/schema-registries" + } + ], + "description": "Short summaries with source links; open a source before acting on a claim." + }, + { + "id": "watch", + "kind": "timeline", + "title": "Questions worth checking", + "span": "one", + "items": [ + { + "label": "Portability", + "title": "Can you export your data?", + "description": "Look for a documented export format before adopting a new tool.", + "tone": "neutral" + }, + { + "label": "Privacy", + "title": "Is access private by default?", + "description": "Check the actual access settings, not only the product description.", + "tone": "neutral" + } + ] + } + ] + } + } +} diff --git a/examples/snapshots/overview/daily/2026-08-24.json b/examples/snapshots/overview/daily/2026-08-24.json index ea9067d..5d9c891 100644 --- a/examples/snapshots/overview/daily/2026-08-24.json +++ b/examples/snapshots/overview/daily/2026-08-24.json @@ -63,8 +63,8 @@ "synthetic": true }, "data": { - "title": "Make room for your best work.", - "summary": "Two focus windows. Four replies to batch. One buffer to keep. Start with the work that is ready, and let the rest wait its turn.", + "title": "Today", + "summary": "Your schedule and priorities in one place. Protect 3.5 hours for the website refresh, handle the two message deadlines, and leave the late-afternoon buffer open.", "attention": "medium", "facts": { "decisions": [ @@ -77,7 +77,7 @@ { "id": "batch-replies", "title": "Use two bounded reply windows", - "reason": "The inbox has four actionable messages but only two carry hard timing constraints.", + "reason": "Three messages need a response; two have explicit deadlines today.", "source_ids": ["inbox:attention:2026-08-24", "agenda:primary:2026-08-24"] }, { @@ -96,8 +96,8 @@ }, { "id": "ship-slice", - "title": "Finish the complete renderer slice", - "reason": "It is startable, high priority, and supported by the protected morning block.", + "title": "Apply the homepage feedback", + "reason": "The current work snapshot marks this as in progress.", "source_ids": ["work:focus:2026-08-24", "agenda:primary:2026-08-24"] }, { @@ -114,132 +114,58 @@ { "id": "day-at-glance", "kind": "metric-group", - "title": "Day at a glance", + "title": "Today at a glance", "span": "full", "metrics": [ { - "label": "Focus time", + "label": "Focus scheduled", "value": 3.5, - "unit": "hours", - "tone": "positive" - }, - { - "label": "Needs a reply", - "value": 4, - "format": "number", - "tone": "warning" - }, - { - "label": "Open blockers", - "value": 1, - "format": "number", - "tone": "neutral" - } - ] - }, - { - "id": "attention-shape", - "kind": "line-chart", - "title": "Less interruption. More focus.", - "description": "Completed blocks and interruptions, August 17\u201323. Counts, not hours.", - "span": "two", - "y_format": "number", - "series": [ - { - "key": "focus", - "label": "Focus blocks" - }, - { - "key": "interruptions", - "label": "Interruptions" - } - ], - "points": [ - { - "x": "Mon", - "values": { - "focus": 2, - "interruptions": 5 - } - }, - { - "x": "Tue", - "values": { - "focus": 3, - "interruptions": 4 - } - }, - { - "x": "Wed", - "values": { - "focus": 3, - "interruptions": 3 - } - }, - { - "x": "Thu", - "values": { - "focus": 4, - "interruptions": 2 - } - }, - { - "x": "Fri", - "values": { - "focus": 3, - "interruptions": 3 - } + "unit": "hours" }, { - "x": "Sat", - "values": { - "focus": 1, - "interruptions": 1 - } + "label": "Messages due today", + "value": 2 }, { - "x": "Sun", - "values": { - "focus": 2, - "interruptions": 1 - } + "label": "Work in progress", + "value": 1 } ] }, { "id": "today", "kind": "calendar", - "title": "Today", - "description": "Two focus windows, one sync, and a protected buffer.", - "span": "one", + "title": "Coming up today", + "description": "The same schedule shown in Agenda, with time left after 15:30.", + "span": "two", "date": "2026-08-24", "events": [ { - "id": "focus-1", - "title": "Build the first usable slice", + "id": "a1", + "title": "Finish the homepage", "start": "2026-08-24T08:30:00Z", "end": "2026-08-24T10:30:00Z", "tone": "positive" }, { - "id": "sync", - "title": "Project sync", + "id": "a2", + "title": "Project check-in", "start": "2026-08-24T11:00:00Z", "end": "2026-08-24T11:30:00Z", "tone": "info" }, { - "id": "focus-2", - "title": "Review and ship", + "id": "a3", + "title": "Review the homepage", "start": "2026-08-24T13:00:00Z", "end": "2026-08-24T14:30:00Z", "tone": "positive" }, { - "id": "buffer", - "title": "Open buffer", - "start": "2026-08-24T15:30:00Z", - "end": "2026-08-24T16:00:00Z", + "id": "a4", + "title": "Walk and reset", + "start": "2026-08-24T15:00:00Z", + "end": "2026-08-24T15:30:00Z", "tone": "neutral" } ] @@ -247,110 +173,33 @@ { "id": "next-actions", "kind": "list", - "title": "Decide or do", + "title": "Needs attention", "description": "Only actions with a concrete next step appear here.", - "span": "two", - "items": [ - { - "id": "reply", - "title": "Reply to the scheduling question", - "description": "Confirm one of the two proposed windows.", - "meta": "Before the 08:30 focus window", - "status": "Needs reply", - "tone": "warning" - }, - { - "id": "review", - "title": "Review the small release", - "description": "Check the acceptance criteria and approve or request changes.", - "meta": "20 minutes", - "status": "Ready", - "tone": "positive" - }, - { - "id": "plan", - "title": "Choose tomorrow's first task", - "description": "Write a single startable action before ending the day.", - "meta": "End of day", - "status": "Later", - "tone": "neutral" - } - ] - }, - { - "id": "goal-progress", - "kind": "progress", - "title": "Last week, against your goals", - "span": "one", - "items": [ - { - "label": "Weekly deep work", - "value": 7.5, - "max": 10, - "value_label": "7.5 of 10 hours", - "tone": "positive" - }, - { - "label": "Movement", - "value": 4, - "max": 5, - "value_label": "4 of 5 sessions", - "tone": "info" - }, - { - "label": "Admin cleared", - "value": 6, - "max": 10, - "value_label": "6 of 10 items", - "tone": "warning" - } - ] - }, - { - "id": "protect-buffer", - "kind": "notice", - "title": "Protect the buffer", - "body": "The afternoon has enough space to absorb surprises. Filling it now would turn a calm plan into calendar Tetris.", - "tone": "info", - "span": "full" - }, - { - "id": "decision-sequence", - "kind": "timeline", - "title": "Decision sequence", "span": "one", "items": [ { - "label": "Before 08:30", - "title": "Open the acceptance criteria", - "description": "Start the focus block with the finish line visible.", - "tone": "positive" - }, - { - "label": "Before 08:30", - "title": "Resolve the scheduling choice", - "description": "Resolve this before the focus window rather than interrupting it.", + "id": "renewal", + "title": "Confirm the renewal choice", + "description": "Send a yes-or-no answer to the operations contact.", + "meta": "Due today, 15:00 UTC", + "status": "Deadline", "tone": "warning" }, { - "label": "13:00", - "title": "Review and ship", - "description": "Use the second focus block for evidence and release.", - "tone": "positive" - }, - { - "label": "15:30", - "title": "Stop scheduling", - "description": "Keep the buffer available for reality.", - "tone": "info" + "id": "planning", + "title": "Choose a planning window", + "description": "Pick one of the two proposed times for tomorrow.", + "meta": "Due today, 17:00 UTC", + "status": "Decision", + "tone": "neutral" } ] }, { "id": "source-register", "kind": "table", - "title": "Source state", - "span": "two", + "title": "What the sources support", + "span": "full", "columns": [ { "key": "source", @@ -374,12 +223,12 @@ { "source": "Inbox", "state": "Fresh", - "consequence": "Four actions retained" + "consequence": "Three responses needed; one conversation waiting" }, { "source": "Work", "state": "Fresh", - "consequence": "One startable priority" + "consequence": "One item in progress; two ready" }, { "source": "Money", diff --git a/examples/snapshots/review/weekly/2026-08-24.json b/examples/snapshots/review/weekly/2026-08-24.json new file mode 100644 index 0000000..aed0035 --- /dev/null +++ b/examples/snapshots/review/weekly/2026-08-24.json @@ -0,0 +1,266 @@ +{ + "schema_version": "0.1.1", + "schema_ref": "schemas/domains/review.schema.json", + "snapshot_id": "review:weekly:2026-08-24", + "source_id": "review:weekly", + "domain": "review", + "source": "weekly", + "generated_at": "2026-08-24T07:40:00Z", + "effective_period": { + "start": "2026-08-17T00:00:00Z", + "end": "2026-08-24T07:40:00Z", + "timezone": "UTC" + }, + "status": "partial", + "producer": { + "worker_id": "review-weekly", + "workflow": "example", + "version": "0.1.1" + }, + "sources": [ + { + "label": "Synthetic daily overviews", + "status": "ok", + "as_of": "2026-08-24T07:35:00Z", + "reference": "overview:daily:2026-08-24" + }, + { + "label": "Synthetic agenda", + "status": "ok", + "as_of": "2026-08-24T07:20:00Z", + "reference": "agenda:primary:2026-08-24" + }, + { + "label": "Synthetic inbox", + "status": "ok", + "as_of": "2026-08-24T07:15:00Z", + "reference": "inbox:attention:2026-08-24" + }, + { + "label": "Synthetic work", + "status": "ok", + "as_of": "2026-08-24T07:10:00Z", + "reference": "work:focus:2026-08-24" + }, + { + "label": "Synthetic money pulse", + "status": "stale", + "as_of": "2026-08-23T18:00:00Z", + "reference": "money:pulse:2026-08-24" + }, + { + "label": "Synthetic news", + "status": "ok", + "as_of": "2026-08-24T06:55:00Z", + "reference": "news:briefing:2026-08-24" + } + ], + "freshness": { + "expires_at": "2026-09-01T07:40:00Z", + "next_expected_at": "2026-08-31T07:40:00Z" + }, + "quality": { + "confidence": "medium", + "warnings": ["One input was stale, so money-related conclusions are intentionally limited."] + }, + "privacy": { + "classification": "public", + "contains_personal_data": false, + "synthetic": true + }, + "data": { + "title": "Weekly review", + "summary": "Review August 17\u201323 before planning the next week. You recorded 18 focus blocks and 19 interruptions. Thursday had the most focus blocks; this alone does not establish what caused the difference.", + "attention": "low", + "facts": { + "outcomes": [ + { + "id": "focus-count", + "label": "18 focus blocks and 19 interruptions recorded across seven days", + "status": "neutral", + "evidence_snapshot_ids": ["overview:daily:2026-08-24"] + }, + { + "id": "money-limited", + "label": "Daily financial returns cannot be confirmed from stale prices", + "status": "unknown", + "evidence_snapshot_ids": ["money:pulse:2026-08-24"] + } + ], + "patterns": [ + { + "id": "variation", + "label": "Daily focus varied from one to four blocks; no causal conclusion is supported", + "status": "neutral", + "evidence_snapshot_ids": ["overview:daily:2026-08-24"] + } + ], + "next_actions": [ + { + "id": "repeat-boundary", + "label": "Keep the first two hours meeting-free on three weekdays", + "status": "neutral", + "evidence_snapshot_ids": ["review:weekly:2026-08-24"] + }, + { + "id": "two-reply-windows", + "label": "Try reply windows at 08:00 and 16:00", + "status": "neutral", + "evidence_snapshot_ids": ["inbox:attention:2026-08-24", "overview:daily:2026-08-24"] + } + ] + }, + "presentation": { + "layout": "dashboard", + "blocks": [ + { + "id": "week-metrics", + "kind": "metric-group", + "title": "Week totals", + "span": "full", + "metrics": [ + { + "label": "Focus blocks", + "value": 18 + }, + { + "label": "Interruptions", + "value": 19 + }, + { + "label": "Days recorded", + "value": 7 + } + ] + }, + { + "id": "focus-trend", + "kind": "line-chart", + "title": "Focus through the week", + "description": "Daily recorded counts, August 17\u201323. Focus blocks and interruptions are different measures, not hours.", + "span": "two", + "series": [ + { + "key": "focus", + "label": "Focus blocks" + }, + { + "key": "interruptions", + "label": "Interruptions" + } + ], + "points": [ + { + "x": "Mon", + "values": { + "focus": 2, + "interruptions": 5 + } + }, + { + "x": "Tue", + "values": { + "focus": 3, + "interruptions": 4 + } + }, + { + "x": "Wed", + "values": { + "focus": 3, + "interruptions": 3 + } + }, + { + "x": "Thu", + "values": { + "focus": 4, + "interruptions": 2 + } + }, + { + "x": "Fri", + "values": { + "focus": 3, + "interruptions": 3 + } + }, + { + "x": "Sat", + "values": { + "focus": 1, + "interruptions": 1 + } + }, + { + "x": "Sun", + "values": { + "focus": 2, + "interruptions": 1 + } + } + ] + }, + { + "id": "next-week-actions", + "kind": "list", + "title": "Try next week", + "span": "one", + "items": [ + { + "id": "n1", + "title": "Reserve Monday, Wednesday, and Friday mornings", + "description": "Try keeping two hours free on three mornings, then compare next week\u2019s recorded focus.", + "status": "Calendar", + "tone": "positive" + }, + { + "id": "n2", + "title": "Set two inbox windows", + "description": "Reply at 08:00 and 16:00 so messages do not interrupt the 08:30 focus block.", + "status": "Boundary", + "tone": "info" + } + ] + }, + { + "id": "review-evidence", + "kind": "table", + "title": "What this review can establish", + "span": "full", + "columns": [ + { + "key": "measure", + "label": "Evidence" + }, + { + "key": "finding", + "label": "Finding" + }, + { + "key": "limit", + "label": "Limit" + } + ], + "rows": [ + { + "measure": "Seven daily observations", + "finding": "18 focus blocks; 19 interruptions", + "limit": "Counts do not measure output quality or explain causes" + }, + { + "measure": "Latest money snapshot", + "finding": "One price source is stale", + "limit": "No verified weekly return" + }, + { + "measure": "Current work queue", + "finding": "One item in progress", + "limit": "No complete shipment history for the week" + } + ] + } + ] + } + } +} diff --git a/examples/snapshots/work/focus/2026-08-24.json b/examples/snapshots/work/focus/2026-08-24.json index 68b529c..5e93fc1 100644 --- a/examples/snapshots/work/focus/2026-08-24.json +++ b/examples/snapshots/work/focus/2026-08-24.json @@ -38,46 +38,46 @@ "synthetic": true }, "data": { - "title": "One finished thing beats five starts.", - "summary": "Five owned items: one in progress, two ready, one waiting, and one blocked. Finish the renderer before pulling another task into progress.", + "title": "Work", + "summary": "One item is in progress and two are ready. Finish the homepage before starting another item; the usage report remains blocked on access.", "attention": "medium", "facts": { "items": [ { "id": "renderer", - "title": "Adaptive block renderer", + "title": "Website refresh", "status": "in-progress", "priority": "high", - "next_action": "Complete the table component" + "next_action": "Apply the final homepage feedback" }, { "id": "deployment", - "title": "Private deployment recipe", + "title": "Launch checklist", "status": "ready", "priority": "medium", - "next_action": "Verify the Access checklist" + "next_action": "Confirm the release owner" }, { "id": "connector", - "title": "Connector example", + "title": "Usage report", "status": "blocked", "priority": "medium", - "next_action": "Wait for source permission" + "next_action": "Request read-only reporting access" }, { "id": "fixtures", - "title": "Renderer fixture coverage", + "title": "Mobile review", "status": "ready", "priority": "high", - "next_action": "Add one valid example per audited block", + "next_action": "Check the homepage on a phone", "due_at": "2026-08-25T16:00:00Z" }, { "id": "docs", - "title": "Contract extension guide", + "title": "Support guide", "status": "waiting", "priority": "medium", - "next_action": "Review after fixtures land" + "next_action": "Review after the homepage is approved" } ] }, @@ -87,189 +87,105 @@ { "id": "work-counts", "kind": "metric-group", - "title": "Current work", + "title": "Work status", "span": "full", "metrics": [ - { - "label": "In progress", - "value": 1, - "tone": "positive" - }, - { - "label": "Ready", - "value": 2, - "tone": "info" - }, - { - "label": "Blocked", - "value": 1, - "tone": "warning" - }, - { - "label": "Waiting", - "value": 1, - "tone": "neutral" - } - ] - }, - { - "id": "work-distribution", - "kind": "donut-chart", - "title": "Where work stands", - "description": "All five owned items, grouped by state. Select a state to inspect its count.", - "span": "two", - "center_label": "Owned items", - "value_format": "number", - "segments": [ { "label": "In progress", "value": 1 }, { - "label": "Ready", + "label": "Ready to start", "value": 2 }, - { - "label": "Waiting", - "value": 1 - }, { "label": "Blocked", "value": 1 - } - ] - }, - { - "id": "work-next", - "kind": "list", - "title": "Best next moves", - "span": "one", - "items": [ - { - "id": "w1", - "title": "Finish one end-to-end path", - "description": "Prefer a complete thin slice over three half-started components.", - "meta": "90 minutes", - "tone": "positive" }, { - "id": "w2", - "title": "Write the open decision", - "description": "Make the blocker answerable in one message.", - "meta": "10 minutes", - "tone": "info" - } - ] - }, - { - "id": "delivery-progress", - "kind": "progress", - "title": "Delivery evidence", - "span": "one", - "items": [ - { - "label": "Complete slice", - "value": 7, - "max": 10, - "value_label": "7 of 10 checks", - "tone": "positive" - }, - { - "label": "Fixture coverage", - "value": 8, - "max": 11, - "value_label": "8 of 11 blocks", - "tone": "info" - }, - { - "label": "Release evidence", - "value": 3, - "max": 5, - "value_label": "3 of 5 gates", - "tone": "warning" - } - ] - }, - { - "id": "delivery-sequence", - "kind": "timeline", - "title": "Path to done", - "span": "two", - "items": [ - { - "label": "Now", - "title": "Finish the renderer path", - "description": "Implementation, fixture, and test move together.", - "tone": "positive" - }, - { - "label": "Next", - "title": "Run the full contract gate", - "description": "Fix only evidence-backed failures.", - "tone": "info" - }, - { - "label": "Then", - "title": "Publish the extension guide", - "description": "Document the exact safe boundary.", - "tone": "neutral" + "label": "Waiting", + "value": 1 } ] }, { "id": "owned-work", "kind": "table", - "title": "Owned work", + "title": "Active work", + "description": "Filter by status or priority, sort a column, and find the next step. This is a read-only snapshot of your task tracker.", "span": "full", "columns": [ { "key": "item", - "label": "Item" + "label": "Task" }, { "key": "state", - "label": "State" + "label": "Status", + "filterable": true, + "tones": { + "In progress": "info", + "Ready": "positive", + "Blocked": "warning", + "Waiting": "neutral" + } + }, + { + "key": "priority", + "label": "Priority", + "filterable": true, + "tones": { + "High": "warning", + "Medium": "neutral" + } + }, + { + "key": "due", + "label": "Due (UTC)" }, { "key": "next", - "label": "Next action" + "label": "Next step" } ], "rows": [ { - "item": "Adaptive block renderer", + "item": "Website refresh", "state": "In progress", - "next": "Complete the table component" + "next": "Apply the final homepage feedback", + "priority": "High", + "due": "No deadline" }, { - "item": "Private deployment recipe", + "item": "Launch checklist", "state": "Ready", - "next": "Verify Access checklist" + "next": "Confirm the release owner", + "priority": "Medium", + "due": "No deadline" }, { - "item": "Connector example", + "item": "Usage report", "state": "Blocked", - "next": "Wait for source permission" + "next": "Request read-only reporting access", + "priority": "Medium", + "due": "No deadline" }, { - "item": "Renderer fixture coverage", + "item": "Mobile review", "state": "Ready", - "next": "Add one example per audited block" + "next": "Check the homepage on a phone", + "priority": "High", + "due": "Aug 25, 16:00" }, { - "item": "Contract extension guide", + "item": "Support guide", "state": "Waiting", - "next": "Review after fixtures land" + "next": "Review after the homepage is approved", + "priority": "Medium", + "due": "No deadline" } - ] - }, - { - "id": "external-blocker", - "kind": "notice", - "title": "The connector is not startable", - "body": "Source permission is outside the current work boundary. Keep it visible, but do not let it interrupt the complete renderer slice.", - "tone": "warning", - "span": "full" + ], + "searchable": true } ] } diff --git a/prompts/base-worker.md b/prompts/base-worker.md index 4043808..afdad12 100644 --- a/prompts/base-worker.md +++ b/prompts/base-worker.md @@ -57,6 +57,10 @@ Presentation rules: - Choose UI blocks based on the information, not decoration. - Use metric-group for a few decision-relevant measures, line-chart for ordered trends, bar-chart for categorical comparison, donut-chart for parts of one meaningful whole, calendar for timed events, table for exact repeated fields, progress for explicit goals, timeline for sequence, list for actions or ranked items, notice for one important caveat, and text only when structure would reduce clarity. - Use at most 16 blocks and prefer one dominant view with quieter supporting detail. +- Normally use 3–6 blocks: a small metric strip when useful, one primary view, and supporting context. A briefing may need only a reading list and watchpoints. Do not fill space to meet a block count. +- Use a plain page title and a summary that explains the page's purpose and the most useful finding. Avoid slogans, motivational advice, and implementation jargon. +- Derive every count and comparison from retained evidence. State the period, units, and assumptions. Separate items needing the user's response from items waiting on others. Do not claim causation from a short trend or generate projection curves without a reproducible model. +- Keep one fact in one primary representation. Use two-column spans for prose-heavy tables or dominant charts, with a one-column supporting list. All layouts share the same outer page width. - Do not request empty charts, repeat one fact across blocks, or create arbitrary metrics. - Never output HTML, CSS, JavaScript, SVG, executable Markdown, or a component name outside schemas/ui-blocks.schema.json. - Every external href must use HTTPS. @@ -78,3 +82,5 @@ Validation and retry behavior: Return a short run report with effective date, status, target path, source freshness, warnings, validation performed, and pull request or commit reference. Never repeat sensitive snapshot values in the report. ``` + +For a richer overview, combine a compact metric panel with a wider trend chart when recorded history exists. Optional metric `trend` needs 2–24 dated numeric points and a period label, with matching evidence in facts. Never manufacture history. Optional table `searchable`, column `filterable`, and semantic column `tones` support exploring detailed rows; they do not mutate the source. Keep comparisons in the same unit, state their baseline, and do not describe balance change as investment return. diff --git a/schemas/domains/money.schema.json b/schemas/domains/money.schema.json index 13e36ce..b70e66e 100644 --- a/schemas/domains/money.schema.json +++ b/schemas/domains/money.schema.json @@ -3,7 +3,9 @@ "$id": "https://zaati-os.dev/schemas/domains/money.schema.json", "title": "Zaati OS money facts", "allOf": [ - { "$ref": "https://zaati-os.dev/schemas/domains/domain-base.schema.json" }, + { + "$ref": "https://zaati-os.dev/schemas/domains/domain-base.schema.json" + }, { "type": "object", "properties": { @@ -20,13 +22,33 @@ "additionalProperties": false, "required": ["id", "label", "kind", "value", "as_of"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "label": { "type": "string", "minLength": 1, "maxLength": 80 }, - "kind": { "enum": ["money", "percent", "number", "duration"] }, - "value": { "type": "number" }, - "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, - "unit": { "type": "string", "maxLength": 24 }, - "as_of": { "type": "string", "format": "date-time" } + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "kind": { + "enum": ["money", "percent", "number", "duration"] + }, + "value": { + "type": "number" + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "unit": { + "type": "string", + "maxLength": 24 + }, + "as_of": { + "type": "string", + "format": "date-time" + } } } }, @@ -38,11 +60,27 @@ "additionalProperties": false, "required": ["id", "label", "current", "target"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "label": { "type": "string", "minLength": 1, "maxLength": 80 }, - "current": { "type": "number", "minimum": 0 }, - "target": { "type": "number", "exclusiveMinimum": 0 }, - "unit": { "type": "string", "maxLength": 24 } + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "current": { + "type": "number", + "minimum": 0 + }, + "target": { + "type": "number", + "exclusiveMinimum": 0 + }, + "unit": { + "type": "string", + "maxLength": 24 + } } } }, @@ -54,13 +92,32 @@ "additionalProperties": false, "required": ["id", "label", "type", "status", "balance", "currency", "as_of"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "label": { "type": "string", "minLength": 1, "maxLength": 80 }, - "type": { "enum": ["cash", "investment", "retirement", "credit", "other"] }, - "status": { "enum": ["connected", "manual", "stale", "unavailable"] }, - "balance": { "type": "number" }, - "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, - "as_of": { "type": "string", "format": "date-time" } + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "type": { + "enum": ["cash", "investment", "retirement", "credit", "other"] + }, + "status": { + "enum": ["connected", "manual", "stale", "unavailable"] + }, + "balance": { + "type": "number" + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "as_of": { + "type": "string", + "format": "date-time" + } } } }, @@ -72,17 +129,50 @@ "additionalProperties": false, "required": ["id", "label", "asset_type", "units", "unit_price", "market_value", "currency", "weight_percent", "as_of"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "label": { "type": "string", "minLength": 1, "maxLength": 80 }, - "symbol": { "type": "string", "maxLength": 16 }, - "asset_type": { "enum": ["stock", "fund", "bond", "crypto", "cash", "other"] }, - "units": { "type": "number", "minimum": 0 }, - "unit_price": { "type": "number", "minimum": 0 }, - "market_value": { "type": "number", "minimum": 0 }, - "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, - "weight_percent": { "type": "number", "minimum": 0, "maximum": 100 }, - "day_change_percent": { "type": "number" }, - "as_of": { "type": "string", "format": "date-time" } + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "symbol": { + "type": "string", + "maxLength": 16 + }, + "asset_type": { + "enum": ["stock", "fund", "bond", "crypto", "cash", "other"] + }, + "units": { + "type": "number", + "minimum": 0 + }, + "unit_price": { + "type": "number", + "minimum": 0 + }, + "market_value": { + "type": "number", + "minimum": 0 + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + }, + "weight_percent": { + "type": "number", + "minimum": 0, + "maximum": 100 + }, + "day_change_percent": { + "type": "number" + }, + "as_of": { + "type": "string", + "format": "date-time" + } } } }, @@ -94,10 +184,50 @@ "additionalProperties": false, "required": ["year", "scenario", "value", "currency"], "properties": { - "year": { "type": "integer", "minimum": 2000, "maximum": 2200 }, - "scenario": { "enum": ["conservative", "baseline", "stretch"] }, - "value": { "type": "number", "minimum": 0 }, - "currency": { "type": "string", "pattern": "^[A-Z]{3}$" } + "year": { + "type": "integer", + "minimum": 2000, + "maximum": 2200 + }, + "scenario": { + "enum": ["conservative", "baseline", "stretch"] + }, + "value": { + "type": "number", + "minimum": 0 + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + } + } + } + }, + "balance_history": { + "type": "array", + "minItems": 2, + "maxItems": 24, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["date", "cash", "invested", "currency"], + "properties": { + "date": { + "type": "string", + "format": "date" + }, + "cash": { + "type": "number", + "minimum": 0 + }, + "invested": { + "type": "number", + "minimum": 0 + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$" + } } } } diff --git a/schemas/ui-blocks.schema.json b/schemas/ui-blocks.schema.json index 0f1bbc3..2532934 100644 --- a/schemas/ui-blocks.schema.json +++ b/schemas/ui-blocks.schema.json @@ -3,21 +3,77 @@ "$id": "https://zaati-os.dev/schemas/ui-blocks.schema.json", "title": "Zaati OS safe presentation contract", "$defs": { - "span": { "enum": ["one", "two", "full"] }, - "tone": { "enum": ["neutral", "positive", "warning", "danger", "info"] }, - "format": { "enum": ["text", "number", "compact-number", "currency", "percent", "date", "time", "duration"] }, + "span": { + "enum": ["one", "two", "full"] + }, + "tone": { + "enum": ["neutral", "positive", "warning", "danger", "info"] + }, + "format": { + "enum": ["text", "number", "compact-number", "currency", "percent", "date", "time", "duration"] + }, "metric": { "type": "object", "additionalProperties": false, "required": ["label", "value"], "properties": { - "label": { "type": "string", "minLength": 1, "maxLength": 60 }, - "value": { "type": ["string", "number"] }, - "format": { "$ref": "#/$defs/format" }, - "unit": { "type": "string", "maxLength": 16 }, - "change": { "type": "number" }, - "change_label": { "type": "string", "maxLength": 40 }, - "tone": { "$ref": "#/$defs/tone" } + "label": { + "type": "string", + "minLength": 1, + "maxLength": 60 + }, + "value": { + "type": ["string", "number"] + }, + "format": { + "$ref": "#/$defs/format" + }, + "unit": { + "type": "string", + "maxLength": 16 + }, + "change": { + "type": "number" + }, + "change_label": { + "type": "string", + "maxLength": 40 + }, + "tone": { + "$ref": "#/$defs/tone" + }, + "trend": { + "type": "object", + "additionalProperties": false, + "required": ["label", "points"], + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "points": { + "type": "array", + "minItems": 2, + "maxItems": 24, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["label", "value"], + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "value": { + "type": "number" + } + } + } + } + } + } } }, "metric-group": { @@ -25,12 +81,33 @@ "additionalProperties": false, "required": ["id", "kind", "title", "metrics"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "kind": { "const": "metric-group" }, - "title": { "type": "string", "minLength": 1, "maxLength": 80 }, - "description": { "type": "string", "maxLength": 220 }, - "span": { "$ref": "#/$defs/span" }, - "metrics": { "type": "array", "minItems": 1, "maxItems": 6, "items": { "$ref": "#/$defs/metric" } } + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "kind": { + "const": "metric-group" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "description": { + "type": "string", + "maxLength": 220 + }, + "span": { + "$ref": "#/$defs/span" + }, + "metrics": { + "type": "array", + "minItems": 1, + "maxItems": 6, + "items": { + "$ref": "#/$defs/metric" + } + } } }, "list": { @@ -38,11 +115,25 @@ "additionalProperties": false, "required": ["id", "kind", "title", "items"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "kind": { "const": "list" }, - "title": { "type": "string", "minLength": 1, "maxLength": 80 }, - "description": { "type": "string", "maxLength": 220 }, - "span": { "$ref": "#/$defs/span" }, + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "kind": { + "const": "list" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "description": { + "type": "string", + "maxLength": 220 + }, + "span": { + "$ref": "#/$defs/span" + }, "items": { "type": "array", "maxItems": 20, @@ -51,13 +142,36 @@ "additionalProperties": false, "required": ["id", "title"], "properties": { - "id": { "type": "string", "minLength": 1, "maxLength": 80 }, - "title": { "type": "string", "minLength": 1, "maxLength": 140 }, - "description": { "type": "string", "maxLength": 360 }, - "meta": { "type": "string", "maxLength": 80 }, - "status": { "type": "string", "maxLength": 40 }, - "tone": { "$ref": "#/$defs/tone" }, - "href": { "type": "string", "format": "uri", "pattern": "^https://" } + "id": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 140 + }, + "description": { + "type": "string", + "maxLength": 360 + }, + "meta": { + "type": "string", + "maxLength": 80 + }, + "status": { + "type": "string", + "maxLength": 40 + }, + "tone": { + "$ref": "#/$defs/tone" + }, + "href": { + "type": "string", + "format": "uri", + "pattern": "^https://" + } } } } @@ -68,13 +182,32 @@ "additionalProperties": false, "required": ["id", "kind", "title", "series", "points"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "kind": { "const": "line-chart" }, - "title": { "type": "string", "minLength": 1, "maxLength": 80 }, - "description": { "type": "string", "maxLength": 220 }, - "span": { "$ref": "#/$defs/span" }, - "x_label": { "type": "string", "maxLength": 40 }, - "y_format": { "$ref": "#/$defs/format" }, + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "kind": { + "const": "line-chart" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "description": { + "type": "string", + "maxLength": 220 + }, + "span": { + "$ref": "#/$defs/span" + }, + "x_label": { + "type": "string", + "maxLength": 40 + }, + "y_format": { + "$ref": "#/$defs/format" + }, "series": { "type": "array", "minItems": 1, @@ -84,8 +217,15 @@ "additionalProperties": false, "required": ["key", "label"], "properties": { - "key": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, - "label": { "type": "string", "minLength": 1, "maxLength": 40 } + "key": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 + } } } }, @@ -98,8 +238,19 @@ "additionalProperties": false, "required": ["x", "values"], "properties": { - "x": { "type": "string", "minLength": 1, "maxLength": 40 }, - "values": { "type": "object", "minProperties": 1, "maxProperties": 5, "additionalProperties": { "type": "number" } } + "x": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "values": { + "type": "object", + "minProperties": 1, + "maxProperties": 5, + "additionalProperties": { + "type": "number" + } + } } } } @@ -110,12 +261,28 @@ "additionalProperties": false, "required": ["id", "kind", "title", "bars"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "kind": { "const": "bar-chart" }, - "title": { "type": "string", "minLength": 1, "maxLength": 80 }, - "description": { "type": "string", "maxLength": 220 }, - "span": { "$ref": "#/$defs/span" }, - "value_format": { "$ref": "#/$defs/format" }, + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "kind": { + "const": "bar-chart" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "description": { + "type": "string", + "maxLength": 220 + }, + "span": { + "$ref": "#/$defs/span" + }, + "value_format": { + "$ref": "#/$defs/format" + }, "bars": { "type": "array", "minItems": 1, @@ -125,9 +292,17 @@ "additionalProperties": false, "required": ["label", "value"], "properties": { - "label": { "type": "string", "minLength": 1, "maxLength": 60 }, - "value": { "type": "number" }, - "tone": { "$ref": "#/$defs/tone" } + "label": { + "type": "string", + "minLength": 1, + "maxLength": 60 + }, + "value": { + "type": "number" + }, + "tone": { + "$ref": "#/$defs/tone" + } } } } @@ -138,13 +313,32 @@ "additionalProperties": false, "required": ["id", "kind", "title", "segments"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "kind": { "const": "donut-chart" }, - "title": { "type": "string", "minLength": 1, "maxLength": 80 }, - "description": { "type": "string", "maxLength": 220 }, - "span": { "$ref": "#/$defs/span" }, - "value_format": { "$ref": "#/$defs/format" }, - "center_label": { "type": "string", "maxLength": 40 }, + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "kind": { + "const": "donut-chart" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "description": { + "type": "string", + "maxLength": 220 + }, + "span": { + "$ref": "#/$defs/span" + }, + "value_format": { + "$ref": "#/$defs/format" + }, + "center_label": { + "type": "string", + "maxLength": 40 + }, "segments": { "type": "array", "minItems": 2, @@ -154,8 +348,15 @@ "additionalProperties": false, "required": ["label", "value"], "properties": { - "label": { "type": "string", "minLength": 1, "maxLength": 60 }, - "value": { "type": "number", "exclusiveMinimum": 0 } + "label": { + "type": "string", + "minLength": 1, + "maxLength": 60 + }, + "value": { + "type": "number", + "exclusiveMinimum": 0 + } } } } @@ -166,12 +367,29 @@ "additionalProperties": false, "required": ["id", "kind", "title", "date", "events"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "kind": { "const": "calendar" }, - "title": { "type": "string", "minLength": 1, "maxLength": 80 }, - "description": { "type": "string", "maxLength": 220 }, - "span": { "$ref": "#/$defs/span" }, - "date": { "type": "string", "format": "date" }, + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "kind": { + "const": "calendar" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "description": { + "type": "string", + "maxLength": 220 + }, + "span": { + "$ref": "#/$defs/span" + }, + "date": { + "type": "string", + "format": "date" + }, "events": { "type": "array", "maxItems": 24, @@ -180,13 +398,34 @@ "additionalProperties": false, "required": ["id", "title", "start"], "properties": { - "id": { "type": "string", "minLength": 1, "maxLength": 80 }, - "title": { "type": "string", "minLength": 1, "maxLength": 120 }, - "start": { "type": "string", "format": "date-time" }, - "end": { "type": "string", "format": "date-time" }, - "all_day": { "type": "boolean" }, - "location": { "type": "string", "maxLength": 100 }, - "tone": { "$ref": "#/$defs/tone" } + "id": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "start": { + "type": "string", + "format": "date-time" + }, + "end": { + "type": "string", + "format": "date-time" + }, + "all_day": { + "type": "boolean" + }, + "location": { + "type": "string", + "maxLength": 100 + }, + "tone": { + "$ref": "#/$defs/tone" + } } } } @@ -197,11 +436,25 @@ "additionalProperties": false, "required": ["id", "kind", "title", "columns", "rows"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "kind": { "const": "table" }, - "title": { "type": "string", "minLength": 1, "maxLength": 80 }, - "description": { "type": "string", "maxLength": 220 }, - "span": { "$ref": "#/$defs/span" }, + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "kind": { + "const": "table" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "description": { + "type": "string", + "maxLength": 220 + }, + "span": { + "$ref": "#/$defs/span" + }, "columns": { "type": "array", "minItems": 1, @@ -211,9 +464,28 @@ "additionalProperties": false, "required": ["key", "label"], "properties": { - "key": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, - "label": { "type": "string", "minLength": 1, "maxLength": 40 }, - "format": { "$ref": "#/$defs/format" } + "key": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]*$" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "format": { + "$ref": "#/$defs/format" + }, + "filterable": { + "type": "boolean" + }, + "tones": { + "type": "object", + "maxProperties": 12, + "additionalProperties": { + "$ref": "#/$defs/tone" + } + } } } }, @@ -224,8 +496,13 @@ "type": "object", "minProperties": 1, "maxProperties": 8, - "additionalProperties": { "type": ["string", "number", "boolean", "null"] } + "additionalProperties": { + "type": ["string", "number", "boolean", "null"] + } } + }, + "searchable": { + "type": "boolean" } } }, @@ -234,11 +511,25 @@ "additionalProperties": false, "required": ["id", "kind", "title", "items"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "kind": { "const": "progress" }, - "title": { "type": "string", "minLength": 1, "maxLength": 80 }, - "description": { "type": "string", "maxLength": 220 }, - "span": { "$ref": "#/$defs/span" }, + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "kind": { + "const": "progress" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "description": { + "type": "string", + "maxLength": 220 + }, + "span": { + "$ref": "#/$defs/span" + }, "items": { "type": "array", "minItems": 1, @@ -248,11 +539,26 @@ "additionalProperties": false, "required": ["label", "value", "max"], "properties": { - "label": { "type": "string", "minLength": 1, "maxLength": 80 }, - "value": { "type": "number", "minimum": 0 }, - "max": { "type": "number", "exclusiveMinimum": 0 }, - "value_label": { "type": "string", "maxLength": 40 }, - "tone": { "$ref": "#/$defs/tone" } + "label": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "value": { + "type": "number", + "minimum": 0 + }, + "max": { + "type": "number", + "exclusiveMinimum": 0 + }, + "value_label": { + "type": "string", + "maxLength": 40 + }, + "tone": { + "$ref": "#/$defs/tone" + } } } } @@ -263,19 +569,44 @@ "additionalProperties": false, "required": ["id", "kind", "title", "body", "tone"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "kind": { "const": "notice" }, - "title": { "type": "string", "minLength": 1, "maxLength": 80 }, - "body": { "type": "string", "minLength": 1, "maxLength": 600 }, - "tone": { "$ref": "#/$defs/tone" }, - "span": { "$ref": "#/$defs/span" }, + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "kind": { + "const": "notice" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "body": { + "type": "string", + "minLength": 1, + "maxLength": 600 + }, + "tone": { + "$ref": "#/$defs/tone" + }, + "span": { + "$ref": "#/$defs/span" + }, "action": { "type": "object", "additionalProperties": false, "required": ["label", "href"], "properties": { - "label": { "type": "string", "minLength": 1, "maxLength": 40 }, - "href": { "type": "string", "format": "uri", "pattern": "^https://" } + "label": { + "type": "string", + "minLength": 1, + "maxLength": 40 + }, + "href": { + "type": "string", + "format": "uri", + "pattern": "^https://" + } } } } @@ -285,11 +616,25 @@ "additionalProperties": false, "required": ["id", "kind", "title", "items"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "kind": { "const": "timeline" }, - "title": { "type": "string", "minLength": 1, "maxLength": 80 }, - "description": { "type": "string", "maxLength": 220 }, - "span": { "$ref": "#/$defs/span" }, + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "kind": { + "const": "timeline" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "description": { + "type": "string", + "maxLength": 220 + }, + "span": { + "$ref": "#/$defs/span" + }, "items": { "type": "array", "maxItems": 20, @@ -298,10 +643,23 @@ "additionalProperties": false, "required": ["label", "title"], "properties": { - "label": { "type": "string", "minLength": 1, "maxLength": 60 }, - "title": { "type": "string", "minLength": 1, "maxLength": 120 }, - "description": { "type": "string", "maxLength": 300 }, - "tone": { "$ref": "#/$defs/tone" } + "label": { + "type": "string", + "minLength": 1, + "maxLength": 60 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "description": { + "type": "string", + "maxLength": 300 + }, + "tone": { + "$ref": "#/$defs/tone" + } } } } @@ -312,26 +670,63 @@ "additionalProperties": false, "required": ["id", "kind", "title", "body"], "properties": { - "id": { "type": "string", "pattern": "^[a-z0-9-]+$" }, - "kind": { "const": "text" }, - "title": { "type": "string", "minLength": 1, "maxLength": 80 }, - "body": { "type": "string", "minLength": 1, "maxLength": 1200 }, - "span": { "$ref": "#/$defs/span" } + "id": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "kind": { + "const": "text" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "body": { + "type": "string", + "minLength": 1, + "maxLength": 1200 + }, + "span": { + "$ref": "#/$defs/span" + } } }, "block": { "oneOf": [ - { "$ref": "#/$defs/metric-group" }, - { "$ref": "#/$defs/list" }, - { "$ref": "#/$defs/line-chart" }, - { "$ref": "#/$defs/bar-chart" }, - { "$ref": "#/$defs/donut-chart" }, - { "$ref": "#/$defs/calendar" }, - { "$ref": "#/$defs/table" }, - { "$ref": "#/$defs/progress" }, - { "$ref": "#/$defs/notice" }, - { "$ref": "#/$defs/timeline" }, - { "$ref": "#/$defs/text" } + { + "$ref": "#/$defs/metric-group" + }, + { + "$ref": "#/$defs/list" + }, + { + "$ref": "#/$defs/line-chart" + }, + { + "$ref": "#/$defs/bar-chart" + }, + { + "$ref": "#/$defs/donut-chart" + }, + { + "$ref": "#/$defs/calendar" + }, + { + "$ref": "#/$defs/table" + }, + { + "$ref": "#/$defs/progress" + }, + { + "$ref": "#/$defs/notice" + }, + { + "$ref": "#/$defs/timeline" + }, + { + "$ref": "#/$defs/text" + } ] }, "presentation": { @@ -339,8 +734,17 @@ "additionalProperties": false, "required": ["layout", "blocks"], "properties": { - "layout": { "enum": ["dashboard", "focus", "timeline"] }, - "blocks": { "type": "array", "minItems": 1, "maxItems": 16, "items": { "$ref": "#/$defs/block" } } + "layout": { + "enum": ["dashboard", "focus", "timeline"] + }, + "blocks": { + "type": "array", + "minItems": 1, + "maxItems": 16, + "items": { + "$ref": "#/$defs/block" + } + } } } } diff --git a/scripts/build-data-index.mjs b/scripts/build-data-index.mjs index f834daa..6a2d598 100644 --- a/scripts/build-data-index.mjs +++ b/scripts/build-data-index.mjs @@ -32,7 +32,7 @@ const localInstance = await readFile(path.join(root, "config/instance.local.json .then(() => true) .catch(() => false) const rawInstance = await readJson(localInstance && !showcase ? "config/instance.local.json" : "config/instance.example.json") -if (showcase) rawInstance.enabled_sources = ["overview:daily", "money:pulse", "work:focus"] + const instance = { ...rawInstance, experience: resolvedExperience(rawInstance, { @@ -62,7 +62,9 @@ const demoMode = usingExamples const syntheticData = snapshots.length > 0 && snapshots.every((snapshot) => snapshot.privacy?.synthetic === true) const registry = await readJson("config/sources.json") const enabled = new Set(instance.enabled_sources) -const sourceDefinitions = registry.sources.filter((source) => enabled.has(source.id)) +const sourceDefinitions = registry.sources + .filter((source) => enabled.has(source.id)) + .sort((a, b) => instance.enabled_sources.indexOf(a.id) - instance.enabled_sources.indexOf(b.id)) const bySource = Object.fromEntries(sourceDefinitions.map((source) => [source.id, []])) for (const snapshot of snapshots) if (enabled.has(snapshot.source_id)) bySource[snapshot.source_id].push(snapshot) for (const values of Object.values(bySource)) values.sort((a, b) => a.generated_at.localeCompare(b.generated_at)) @@ -107,14 +109,18 @@ const demoPromptsBySource = demoMode ) : {} const exampleKinds = new Set() +const catalogBlocks = demoMode ? await readJson("data/component-examples.json") : [] const componentExamples = demoMode - ? snapshots - .flatMap((snapshot) => (snapshot.data?.presentation?.blocks || []).map((block) => ({ sourceId: snapshot.source_id, block }))) - .filter(({ block }) => { - if (exampleKinds.has(block.kind)) return false - exampleKinds.add(block.kind) - return true - }) + ? [ + ...snapshots.flatMap((snapshot) => + (snapshot.data?.presentation?.blocks || []).map((block) => ({ sourceId: snapshot.source_id, block })), + ), + ...catalogBlocks.map((block) => ({ sourceId: "Component catalog", block })), + ].filter(({ block }) => { + if (exampleKinds.has(block.kind)) return false + exampleKinds.add(block.kind) + return true + }) : [] const output = { generatedAt: new Date().toISOString(), diff --git a/scripts/check-accessibility.mjs b/scripts/check-accessibility.mjs index 42c6dd5..88eb516 100644 --- a/scripts/check-accessibility.mjs +++ b/scripts/check-accessibility.mjs @@ -200,12 +200,44 @@ try { returnByValue: true, }) if (overflow.result?.value) throw new Error(`${width}px ${mode} ${view} overflows the document.`) - if ([390, 1440].includes(width) && ["overview:daily", "money:pulse", "work:focus"].includes(view)) + if ([390, 1440].includes(width) && dashboard.sources.some((source) => source.definition.id === view)) await capture(client, `${view.split(":")[0]}-${width}-${mode}.png`) } } } + const tableSource = dashboard.sources.find(({ snapshot }) => + snapshot?.data.presentation.blocks.some((b) => b.kind === "table" && b.searchable && b.columns.some((c) => c.filterable)), + ) + if (tableSource) { + await client.send("Page.navigate", { url: viewUrl(tableSource.definition.id) }) + await waitForApp(client) + await client.send("Runtime.evaluate", { expression: "document.querySelector('[role=search] input').focus()" }) + await client.send("Input.insertText", { text: "no-matching-example-item-98765" }) + const tableInteraction = await client.send("Runtime.evaluate", { + expression: `(async () => { + const wait = () => new Promise(r => setTimeout(r, 80)); await wait(); + const search = document.querySelector('[role=search]'); + const panel = search.closest('.zaati-block'); + const empty = panel.textContent.includes('No matching items'); + [...search.querySelectorAll('button')].find(b => b.textContent === 'Clear').click(); await wait(); + const restored = panel.querySelectorAll('tbody tr').length > 1; + const select = search.querySelector('select'); select.value = select.options[1].value; + select.dispatchEvent(new Event('change', { bubbles: true })); await wait(); + const filtered = [...panel.querySelectorAll('tbody tr')].every(row => row.textContent.includes(select.value)); + [...search.querySelectorAll('button')].find(b => b.textContent === 'Clear').click(); await wait(); + const header = panel.querySelector('th'); header.querySelector('button').click(); await wait(); + const ascending = header.getAttribute('aria-sort') === 'ascending'; + header.querySelector('button').click(); await wait(); + return empty && restored && filtered && ascending && header.getAttribute('aria-sort') === 'descending'; + })()`, + returnByValue: true, + awaitPromise: true, + }) + if (!tableInteraction.result?.value) throw new Error("Table search, filters, clear, or sorting failed.") + console.log("Table search, empty results, filtering, clearing, and sorting passed.") + } + const chartSource = dashboard.sources.find(({ snapshot }) => { const blocks = snapshot?.data.presentation.blocks || [] return blocks.some((block) => block.kind === "donut-chart") && blocks.some((block) => ["line-chart", "bar-chart"].includes(block.kind)) diff --git a/scripts/validate-data.mjs b/scripts/validate-data.mjs index 48a1806..e5ce199 100644 --- a/scripts/validate-data.mjs +++ b/scripts/validate-data.mjs @@ -71,6 +71,10 @@ const schemas = await Promise.all(schemaPaths.map(readJson)) const ajv = new Ajv2020({ allErrors: true, strict: true, allowUnionTypes: true }) addFormats(ajv) schemas.forEach((schema) => ajv.addSchema(schema)) +const validateCatalogBlock = ajv.compile({ $ref: "https://zaati-os.dev/schemas/ui-blocks.schema.json#/$defs/block" }) +for (const block of await readJson("data/component-examples.json")) { + if (!validateCatalogBlock(block)) errors.push(...formatAjvErrors("data/component-examples.json", validateCatalogBlock.errors)) +} const registry = await readJson("config/sources.json") const validateRegistry = ajv.getSchema("https://zaati-os.dev/schemas/source-registry.schema.json") diff --git a/src/App.tsx b/src/App.tsx index 8eee83c..bea35bf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -72,12 +72,18 @@ function initialView(data: DashboardData, fallback: string) { if (requested === "start") return START_ID if (requested === "components" && data.demoMode) return COMPONENTS_ID if (data.sources.some((item) => item.definition.id === requested)) return requested as string - return data.demoMode || data.sources.every((item) => !item.snapshot) ? START_ID : fallback + return data.sources.every((item) => !item.snapshot) ? START_ID : fallback } function initialNow(data: DashboardData) { const injected = data.demoMode ? new URL(window.location.href).searchParams.get("at") : null - const parsed = injected ? Date.parse(injected) : Number.NaN + const exampleDate = data.demoMode + ? data.sources + .flatMap(({ snapshot }) => (snapshot ? [snapshot.generated_at] : [])) + .sort() + .at(-1) + : undefined + const parsed = injected || exampleDate ? Date.parse(injected || exampleDate!) : Number.NaN return Number.isFinite(parsed) ? parsed : Date.now() } @@ -212,9 +218,10 @@ function DashboardApp({ data }: { data: DashboardData }) { }, [data, overviewId]) useEffect(() => { + if (data.demoMode) return const timer = window.setInterval(() => setNow(Date.now()), 60_000) return () => window.clearInterval(timer) - }, []) + }, [data.demoMode]) const select = (id: string) => { setSelectedId(id) @@ -260,7 +267,7 @@ function DashboardApp({ data }: { data: DashboardData }) { onSelect={select} selectedId={selectedId} /> -
+
@@ -284,9 +290,9 @@ function DashboardApp({ data }: { data: DashboardData }) { - {data.demoMode ? "Synthetic demo" : "Synthetic test data"} + {data.demoMode ? "Sample data" : "Synthetic test data"} ) : ( @@ -328,7 +334,7 @@ function DashboardApp({ data }: { data: DashboardData }) {
-
+
{selectedId === START_ID ? ( }>
-

{snapshot.data.title}

-

{snapshot.data.summary}

+

{snapshot.data.title}

+

{snapshot.data.summary}

@@ -747,10 +747,8 @@ function ComponentLab({ data }: { data: DashboardData }) { return (
-

Safe presentation contract / Synthetic

-

- Build richer pages without shipping UI code -

+

Safe presentation contract / Synthetic

+

Build richer pages without shipping UI code

Your LLM chooses an audited block, a semantic page layout, and a span. Zaati OS owns rendering, responsive behavior, theme tokens, focus handling, and accessibility. @@ -760,7 +758,7 @@ function ComponentLab({ data }: { data: DashboardData }) { {[ ["dashboard", "Three-column canvas", "Use one dominant two-column block with supporting evidence."], ["focus", "Focused decision", "The first block leads; supporting blocks stay quieter."], - ["timeline", "Linear narrative", "Sequence and review pages remain deliberately narrow."], + ["timeline", "Linear narrative", "A sequential reading order within the same page frame."], ].map(([name, label, description]) => (

{name} @@ -776,7 +774,7 @@ function ComponentLab({ data }: { data: DashboardData }) {
-

{sourceId}

+

{sourceId}

{block.kind}

Audited JSON only @@ -824,18 +822,18 @@ function PageEyebrow({ }) { const date = snapshot ? new Intl.DateTimeFormat(instance.locale, { weekday: "long", month: "long", day: "numeric", timeZone: instance.timezone }).format( - new Date(snapshot.effective_period.start), + new Date(snapshot.generated_at), ) : "No snapshot yet" return ( -
+
{date} / {definition.domain} {snapshot?.privacy.synthetic ? ( <> / - Synthetic + Example date ) : null}
diff --git a/src/components/BlockRenderer.tsx b/src/components/BlockRenderer.tsx index 91c03dd..d0bac4a 100644 --- a/src/components/BlockRenderer.tsx +++ b/src/components/BlockRenderer.tsx @@ -1,10 +1,12 @@ -import { lazy, Suspense } from "react" +import { lazy, Suspense, useState } from "react" import { ArrowUpRight, CalendarDays, CheckCircle2, ChevronDown, Info, TriangleAlert } from "lucide-react" import { Badge } from "@/components/ui/badge" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Progress } from "@/components/ui/progress" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" import { cn } from "@/lib/utils" -import type { DashboardBlock, InstanceConfig, ListBlock, Span, TableBlock, Tone, ValueFormat } from "@/types" +import type { DashboardBlock, InstanceConfig, ListBlock, MetricGroupBlock, Span, TableBlock, Tone, ValueFormat } from "@/types" const toneDot: Record = { neutral: "bg-muted-foreground", @@ -20,7 +22,7 @@ const toneBadge: Record = { one: "lg:col-span-1", two: "lg:col-span-2", full: "lg:col-span-3" } +const spanClass: Record = { one: "xl:col-span-1", two: "xl:col-span-2", full: "xl:col-span-3" } const ChartVisual = lazy(() => import("@/components/ChartVisual")) type Layout = "dashboard" | "focus" | "timeline" @@ -71,7 +73,7 @@ function ListRows({ items }: { items: ListBlock["items"] }) { ) return item.href ? ( ) : ( -
+
{content}
) }) } +function MetricTrend({ metric, instance }: { metric: MetricGroupBlock["metrics"][number]; instance: InstanceConfig }) { + if (!metric.trend) return null + const { points, label } = metric.trend + const values = points.map((p) => p.value) + const min = Math.min(...values), + max = Math.max(...values) + const coordinates = points + .map((p, i) => `${(i / (points.length - 1)) * 240},${max === min ? 24 : 42 - ((p.value - min) / (max - min)) * 36}`) + .join(" ") + const color = metric.tone === "danger" ? "var(--destructive)" : metric.tone === "positive" ? "var(--positive)" : "var(--chart-1)" + return ( +
+ + + + +
+ + {label} + +
+ {points.map((point, i) => ( +
+
{point.label}
+
{formatValue(point.value, metric.format, instance)}
+
+ ))} +
+
+
+ ) +} + function DataTable({ block, instance, @@ -98,40 +147,168 @@ function DataTable({ rows: TableBlock["rows"] suffix?: string }) { + const [query, setQuery] = useState("") + const [filters, setFilters] = useState>({}) + const [sort, setSort] = useState<{ key: string; descending: boolean } | null>(null) + const [page, setPage] = useState(0) + const matched = rows.filter( + (row) => + block.columns.some((col) => + String(row[col.key] ?? "") + .toLocaleLowerCase() + .includes(query.toLocaleLowerCase()), + ) && Object.entries(filters).every(([key, value]) => !value || String(row[key]) === value), + ) + if (sort) + matched.sort( + (a, b) => + (typeof a[sort.key] === "number" && typeof b[sort.key] === "number" + ? Number(a[sort.key]) - Number(b[sort.key]) + : String(a[sort.key] ?? "").localeCompare(String(b[sort.key] ?? ""), instance.locale, { numeric: true })) * + (sort.descending ? -1 : 1), + ) + const pages = Math.max(1, Math.ceil(matched.length / 10)) + const currentPage = Math.min(page, pages - 1) + const visible = block.searchable ? matched.slice(currentPage * 10, currentPage * 10 + 10) : rows return ( -
- - - - - {block.columns.map((column) => ( - + <> + {block.searchable ? ( +
+ + {block.columns + .filter((col) => col.filterable) + .map((col) => ( + ))} -
- - - {rows.map((row, index) => ( - - {block.columns.map((column) => ( - + {query || Object.values(filters).some(Boolean) ? ( + + ) : null} + + ) : null} +
+
- {block.title} - {suffix} -
- {column.label} -
- {formatValue(row[column.key] ?? null, column.format, instance)} -
+ + + + {block.columns.map((col) => ( + ))} - ))} - -
+ {block.title} + {suffix} +
+ {block.searchable ? ( + + ) : ( + col.label + )} +
-
+ + + {visible.map((row, index) => ( + + {block.columns.map((col) => ( + + {col.tones && Object.hasOwn(col.tones, String(row[col.key])) ? ( + + {formatValue(row[col.key] ?? null, col.format, instance)} + + ) : ( + formatValue(row[col.key] ?? null, col.format, instance) + )} + + ))} + + ))} + {!visible.length ? ( + + + No matching items. Clear the filters to see all items. + + + ) : null} + + +
+ {block.searchable ? ( +
+ + {matched.length} of {rows.length} items + + {pages > 1 ? ( +
+ + + {currentPage + 1} / {pages} + + +
+ ) : null} +
+ ) : null} + ) } @@ -180,16 +357,13 @@ export function BlockRenderer({ }) { if (block.kind === "metric-group") { return ( - -
+ +
{block.metrics.map((metric) => ( -
-
-
+
+
{metric.label}
- + {formatValue(metric.value, metric.format, instance)} {metric.unit ? {metric.unit} : null} @@ -198,11 +372,12 @@ export function BlockRenderer({

{metric.change > 0 ? "+" : ""} - {formatValue(metric.change, metric.format === "percent" ? "percent" : "number", instance)} + {formatValue(metric.change, metric.format || "number", instance)} {metric.change_label ? {metric.change_label} : null}

) : null} +
))}
@@ -302,6 +477,12 @@ export function BlockRenderer({ } if (block.kind === "table") { + if (block.searchable) + return ( + + + + ) const visibleRows = block.rows.slice(0, 10) const remainingRows = block.rows.slice(10) return ( diff --git a/src/components/ChartVisual.tsx b/src/components/ChartVisual.tsx index c886f33..7c129ed 100644 --- a/src/components/ChartVisual.tsx +++ b/src/components/ChartVisual.tsx @@ -1,12 +1,23 @@ -import { useId, useState } from "react" +import { useEffect, useId, useRef, useState } from "react" import type { BarChartBlock, DonutChartBlock, InstanceConfig, LineChartBlock, ValueFormat } from "@/types" const colors = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"] const dashPatterns = [undefined, "9 5", "2 5", "12 4 2 4", "5 4"] -const width = 640 const height = 260 const plot = { left: 72, right: 18, top: 18, bottom: 48 } +function useChartWidth() { + const ref = useRef(null) + const [width, setWidth] = useState(640) + useEffect(() => { + if (!ref.current) return + const observer = new ResizeObserver(([entry]) => setWidth(Math.max(240, Math.round(entry.contentRect.width)))) + observer.observe(ref.current) + return () => observer.disconnect() + }, []) + return { ref, width } +} + function format(value: number, kind: ValueFormat, instance: InstanceConfig) { if (kind === "currency") return new Intl.NumberFormat(instance.locale, { style: "currency", currency: instance.currency, maximumFractionDigits: 0 }).format( @@ -36,27 +47,18 @@ function axisFormat(value: number, kind: ValueFormat, instance: InstanceConfig) return new Intl.NumberFormat(instance.locale, { maximumFractionDigits: 2 }).format(rounded) } -function lineDomain(values: number[]) { - let min = Math.min(...values) - let max = Math.max(...values) +function chartDomain(values: number[], includeZero = false) { + let min = Math.min(...values, ...(includeZero ? [0] : [])) + let max = Math.max(...values, ...(includeZero ? [0] : [])) if (min === max) { const fallback = Math.max(Math.abs(min) * 0.05, 1) min -= fallback max += fallback } - const padding = (max - min) * 0.08 - return { min: min - padding, max: max + padding } -} - -function barDomain(values: number[]) { - const min = Math.min(...values, 0) - const max = Math.max(...values, 0) - if (min === max) return { min: min - 1, max: max + 1 } - const padding = (max - min) * 0.08 - return { - min: min >= 0 ? 0 : min - padding, - max: max <= 0 ? 0 : max + padding, - } + const rawStep = (max - min) / 4 + const magnitude = 10 ** Math.floor(Math.log10(rawStep)) + const step = ([1, 2, 2.5, 5, 10].find((factor) => factor * magnitude >= rawStep) || 10) * magnitude + return { min: Math.floor(min / step) * step, max: Math.ceil(max / step) * step, step } } function yPosition(value: number, min: number, max: number) { @@ -64,7 +66,7 @@ function yPosition(value: number, min: number, max: number) { return plot.top + ((max - value) / (max - min)) * available } -function ChartTooltip({ x, y, title, value }: { x: number; y: number; title: string; value: string }) { +function ChartTooltip({ x, y, title, value, width }: { x: number; y: number; title: string; value: string; width: number }) { const tooltipWidth = Math.min(220, Math.max(112, Math.max(title.length, value.length) * 7 + 24)) const tooltipX = Math.max(4, Math.min(width - tooltipWidth - 4, x - tooltipWidth / 2)) const tooltipY = y < 76 ? y + 18 : y - 58 @@ -179,20 +181,24 @@ function DonutVisual({ block, descriptionId, instance }: { block: DonutChartBloc } function LineVisual({ block, descriptionId, instance }: { block: LineChartBlock; descriptionId: string; instance: InstanceConfig }) { + const { ref, width } = useChartWidth() const [activePoint, setActivePoint] = useState<{ pointIndex: number; seriesIndex: number } | null>(null) const valueFormat = block.y_format || "number" const values = block.points.flatMap((point) => block.series.map((series) => point.values[series.key]).filter((value): value is number => Number.isFinite(value)), ) - const range = lineDomain(values) + const range = chartDomain(values) const x = (index: number) => plot.left + (index / Math.max(1, block.points.length - 1)) * (width - plot.left - plot.right) - const ticks = Array.from({ length: 5 }, (_, index) => range.min + ((range.max - range.min) * index) / 4).reverse() + const ticks = Array.from( + { length: Math.round((range.max - range.min) / range.step) + 1 }, + (_, index) => range.min + range.step * index, + ).reverse() return (
{block.title} line chart. Exact values follow in an accessible table.
-
+
`${x(index)},${yPosition(point.values[series.key], range.min, range.max)}`) .join(" ") return ( - + + {block.series.length === 1 ? ( + + ) : null} + + ) })} @@ -274,6 +289,7 @@ function LineVisual({ block, descriptionId, instance }: { block: LineChartBlock; <> (null) const valueFormat = block.value_format || "number" - const range = barDomain(block.bars.map((bar) => bar.value)) + const range = chartDomain( + block.bars.map((bar) => bar.value), + true, + ) const available = width - plot.left - plot.right const slot = available / block.bars.length const barWidth = Math.min(44, slot * 0.66) const baseline = yPosition(0, range.min, range.max) - const ticks = Array.from({ length: 5 }, (_, index) => range.min + ((range.max - range.min) * index) / 4).reverse() + const ticks = Array.from( + { length: Math.round((range.max - range.min) / range.step) + 1 }, + (_, index) => range.min + range.step * index, + ).reverse() const labelEvery = Math.max(1, Math.ceil(block.bars.length / 8)) return (
{block.title} bar chart. Exact values follow in an accessible table.
-
+
+ return ( + + ) })() : null} diff --git a/src/components/Onboarding.tsx b/src/components/Onboarding.tsx index 119a369..c529fff 100644 --- a/src/components/Onboarding.tsx +++ b/src/components/Onboarding.tsx @@ -64,12 +64,12 @@ export default function Onboarding({ Start here -

- {demoMode ? "See how it works. Then make it private." : "Your private workspace is ready."} +

+ {demoMode ? "Welcome to Zaati OS" : "Your private workspace is ready."}

{demoMode - ? "Take the guided tour, inspect the synthetic source pages, and compare JSON contracts with their rendered components. When you run setup, Zaati OS creates an ignored private workspace and removes every demo-only surface." + ? "Explore a sample day, see what needs a reply, and understand where your money stands. All demo data is fictional. When you are ready, use the setup steps below to connect your own sources." : "Synthetic example pages, Component Lab, copy-prompt guides, and the automatic demo tour are disabled. Connect your preferred workflow, validate one manual snapshot, then deploy behind Access. The dashboard remains read-only."}

diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index b950d58..884c84c 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -10,7 +10,7 @@ export const CardHeader = React.forwardRef>(({ className, ...props }, ref) => ( -

+

)) CardTitle.displayName = "CardTitle" export const CardDescription = React.forwardRef>( diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..3795cdb --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,14 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +export const Input = React.forwardRef>(({ className, ...props }, ref) => ( + +)) +Input.displayName = "Input" diff --git a/src/index.css b/src/index.css index e799905..d3724b1 100644 --- a/src/index.css +++ b/src/index.css @@ -43,23 +43,23 @@ --radius: 0.9rem; --font-ui: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --font-heading: var(--font-ui); - --background: #f6f7f4; - --foreground: #17201c; + --background: #ffffff; + --foreground: #18181b; --card: #ffffff; - --card-foreground: #17201c; + --card-foreground: #18181b; --popover: #ffffff; - --popover-foreground: #17201c; + --popover-foreground: #18181b; --primary: #2f6f59; --primary-foreground: #ffffff; - --secondary: #e9ece8; - --secondary-foreground: #22312a; - --muted: #ecefeb; - --muted-foreground: #68736d; + --secondary: #f4f4f5; + --secondary-foreground: #27272a; + --muted: #f4f4f5; + --muted-foreground: #64646c; --accent: #e2eee8; --accent-foreground: #245543; --destructive: #c83f4d; - --border: #dde2de; - --input: #dde2de; + --border: #e4e4e7; + --input: #e4e4e7; --ring: #2f6f59; --positive: #3c8b68; --positive-foreground: #226044; @@ -72,34 +72,34 @@ --chart-3: #5287a4; --chart-4: #8c6e9b; --chart-5: #8f7d61; - --sidebar: #f0f2ee; - --sidebar-foreground: #26312b; + --sidebar: #fafafa; + --sidebar-foreground: #27272a; --sidebar-primary: #2f6f59; --sidebar-primary-foreground: #ffffff; - --sidebar-accent: #e0e6e1; - --sidebar-accent-foreground: #17201c; - --sidebar-border: #d9dfda; + --sidebar-accent: #f0f0f2; + --sidebar-accent-foreground: #18181b; + --sidebar-border: #e4e4e7; } .dark { color-scheme: dark; - --background: #111512; - --foreground: #eef2ef; - --card: #171c18; - --card-foreground: #eef2ef; - --popover: #1b211d; - --popover-foreground: #eef2ef; + --background: #101012; + --foreground: #f4f4f5; + --card: #18181b; + --card-foreground: #f4f4f5; + --popover: #1c1c20; + --popover-foreground: #f4f4f5; --primary: #75b99d; --primary-foreground: #0e2119; - --secondary: #252b27; - --secondary-foreground: #e4e9e5; - --muted: #202622; - --muted-foreground: #99a39c; + --secondary: #27272a; + --secondary-foreground: #e4e4e7; + --muted: #222225; + --muted-foreground: #a1a1aa; --accent: #233a30; --accent-foreground: #bfe3d3; --destructive: #f0707c; - --border: #2b332e; - --input: #2b332e; + --border: #303035; + --input: #303035; --ring: #75b99d; --positive: #62aa84; --positive-foreground: #89d0a8; @@ -112,12 +112,12 @@ --chart-3: #65a0c0; --chart-4: #a98cb8; --chart-5: #ad9a7d; - --sidebar: #141915; - --sidebar-foreground: #dce3de; + --sidebar: #151518; + --sidebar-foreground: #e4e4e7; --sidebar-primary: #75b99d; --sidebar-primary-foreground: #0e2119; - --sidebar-accent: #222a25; - --sidebar-accent-foreground: #f0f4f1; + --sidebar-accent: #26262b; + --sidebar-accent-foreground: #fafafa; --sidebar-border: #29312c; } @@ -247,46 +247,42 @@ h3 { gap: 0.75rem; } -.metric-tile { - --metric-accent: var(--primary); - background: color-mix(in srgb, var(--metric-accent) 9%, var(--card)); - border-top: 3px solid var(--metric-accent); -} -.metric-tile[data-tone="info"] { - --metric-accent: var(--info); -} -.metric-tile[data-tone="warning"] { - --metric-accent: var(--warning); -} -.metric-tile[data-tone="danger"] { - --metric-accent: var(--destructive); -} -.metric-tile[data-tone="neutral"] { - --metric-accent: var(--muted-foreground); -} -.metric-tile .text-muted-foreground { - color: var(--foreground); -} -.metric-tile:first-child { - background: var(--primary); - color: var(--primary-foreground); -} -.metric-tile:first-child .text-muted-foreground, -.metric-tile:first-child .text-foreground, -.metric-tile:first-child .text-positive-foreground, -.metric-tile:first-child .text-destructive { - color: inherit; +.metric-panel { + display: flex; + flex-direction: column; +} +.metric-panel > :first-child { + padding: 0; +} +.metric-panel > :first-child h3 { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); +} +.metric-panel > :first-child p { + padding: 16px 20px 0; +} +.metric-panel > :last-child { + padding: 0; + display: flex; + flex: 1; +} +.metric-strip { + display: flex; + flex-wrap: wrap; + flex: 1; + gap: 1px; + background: var(--border); } -.metric-tile:first-child .rounded-full { - background: currentColor; +.metric-tile { + flex: 1 0 min(100%, 8rem); + background: var(--card); } .zaati-block { container-type: inline-size; } -.zaati-block[data-kind="line-chart"], -.zaati-block[data-kind="bar-chart"] { - border-top: 3px solid var(--chart-1); -} .chart-scroll { overflow-x: auto; padding-block: 8px; @@ -294,7 +290,7 @@ h3 { .chart-cartesian { display: block; width: 100%; - min-width: 480px; + min-width: 240px; height: auto; } @container (min-width: 480px) { @@ -415,13 +411,6 @@ h3 { transform 180ms ease; } -@media (hover: hover) { - .zaati-block:hover { - border-color: color-mix(in srgb, var(--primary) 24%, var(--border)); - box-shadow: 0 8px 24px color-mix(in srgb, var(--foreground) 7%, transparent); - } -} - .chart-series-reveal { animation: zaati-chart-reveal 520ms cubic-bezier(0.22, 1, 0.36, 1) both; transform-box: fill-box; @@ -494,3 +483,12 @@ h3 { transition-duration: 0.01ms !important; } } + +@container (max-width: 420px) { + .metric-tile { + padding: 16px 12px; + } + .metric-value { + font-size: clamp(1.25rem, 6.5cqi, 1.5rem); + } +} diff --git a/src/types.ts b/src/types.ts index eb482b0..4bbdbe0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,7 @@ export type MetricGroupBlock = BlockBase & { change?: number change_label?: string tone?: Tone + trend?: { label: string; points: Array<{ label: string; value: number }> } }> } export type ListBlock = BlockBase & { @@ -44,7 +45,8 @@ export type CalendarBlock = BlockBase & { } export type TableBlock = BlockBase & { kind: "table" - columns: Array<{ key: string; label: string; format?: ValueFormat }> + searchable?: boolean + columns: Array<{ key: string; label: string; format?: ValueFormat; filterable?: boolean; tones?: Record }> rows: Array> } export type ProgressBlock = BlockBase & { diff --git a/tests/contracts.test.mjs b/tests/contracts.test.mjs index 0e4e16c..64e3c03 100644 --- a/tests/contracts.test.mjs +++ b/tests/contracts.test.mjs @@ -102,13 +102,14 @@ test("synthetic pages demonstrate every audited block kind", async () => { const snapshot = await readJson(`data/examples/${source.domain}/${source.source}/2026-08-24.json`) for (const block of snapshot.data.presentation.blocks) demonstrated.add(block.kind) } + for (const block of await readJson("data/component-examples.json")) demonstrated.add(block.kind) assert.deepEqual([...demonstrated].sort(), [...expected].sort()) }) test("line charts require exact unique series coverage", async () => { const registry = await readJson("config/sources.json") - const registration = registry.sources.find((source) => source.id === "money:pulse") - const snapshot = await readJson("data/examples/money/pulse/2026-08-24.json") + const registration = registry.sources.find((source) => source.id === "review:weekly") + const snapshot = await readJson("data/examples/review/weekly/2026-08-24.json") const chart = snapshot.data.presentation.blocks.find((block) => block.kind === "line-chart") chart.series.push({ ...chart.series[0] }) chart.points[0].values = { unexpected: 1 } @@ -174,3 +175,63 @@ test("custom themes reject low-contrast semantic pairs in both modes", () => { assert.ok(errors.some((error) => error.includes("custom_tokens/dark"))) assert.deepEqual(validateCustomTheme({ theme: { preset: "sage" } }), []) }) + +test("optional trend and table controls remain bounded safe data", async () => { + const { default: Ajv2020 } = await import("ajv/dist/2020.js") + const ajv = new Ajv2020({ strict: true, allowUnionTypes: true }) + const { default: addFormats } = await import("ajv-formats") + addFormats(ajv) + const schema = await readJson("schemas/ui-blocks.schema.json") + ajv.addSchema(schema) + const validate = ajv.compile({ $ref: `${schema.$id}#/$defs/block` }) + const money = await readJson("data/examples/money/pulse/2026-08-24.json") + const work = await readJson("data/examples/work/focus/2026-08-24.json") + const metric = structuredClone(money.data.presentation.blocks[0]) + const table = structuredClone(work.data.presentation.blocks[1]) + assert.equal(validate(metric), true) + assert.equal(validate(table), true) + metric.metrics[0].trend.points = [{ label: "Only one date", value: 1 }] + assert.equal(validate(metric), false, "one observation is not a trend") + metric.metrics[0].trend.points = Array.from({ length: 25 }, (_, i) => ({ label: String(i), value: i })) + assert.equal(validate(metric), false, "sparkline payloads are bounded") + table.columns[1].tones.Ready = "arbitrary-class-name" + assert.equal(validate(table), false, "status colors must use semantic tones") + table.columns[1].tones.Ready = "positive" + table.onClick = "execute()" + assert.equal(validate(table), false, "snapshot controls cannot contain handlers") +}) + +test("money history, sparklines, allocation, and final balances reconcile", async () => { + const { data } = await readJson("data/examples/money/pulse/2026-08-24.json") + const history = data.facts.balance_history + const blocks = data.presentation.blocks + const metrics = blocks.find((b) => b.kind === "metric-group").metrics + const chart = blocks.find((b) => b.kind === "line-chart") + const allocation = blocks.find((b) => b.kind === "donut-chart") + const latest = history.at(-1) + assert.equal( + latest.cash + latest.invested, + data.facts.accounts.reduce((sum, a) => sum + a.balance, 0), + ) + assert.equal( + latest.invested, + data.facts.holdings.reduce((sum, h) => sum + h.market_value, 0), + ) + assert.equal( + allocation.segments.reduce((sum, s) => sum + s.value, 0), + metrics[0].value, + ) + assert.deepEqual( + chart.points.map((p) => p.values.total), + history.map((p) => p.cash + p.invested), + ) + for (const [index, key] of ["total", "invested", "cash"].entries()) { + const values = history.map((p) => (key === "total" ? p.cash + p.invested : p[key])) + assert.deepEqual( + metrics[index].trend.points.map((p) => p.value), + values, + ) + assert.equal(metrics[index].value, values.at(-1)) + assert.equal(metrics[index].change, values.at(-1) - values[0]) + } +}) diff --git a/tests/examples.test.mjs b/tests/examples.test.mjs index 846676e..8d748f8 100644 --- a/tests/examples.test.mjs +++ b/tests/examples.test.mjs @@ -8,10 +8,10 @@ import test from "node:test" test("showcase build excludes private data and local identity even when both exist", async () => { const fixture = await mkdtemp(path.join(tmpdir(), "zaati-showcase-")) try { - for (const directory of ["config", "examples/snapshots", "data/examples", "schemas", "prompts", "docs"]) + for (const directory of ["config", "examples/snapshots", "data/examples", "data/component-examples.json", "schemas", "prompts", "docs"]) await cp(directory, path.join(fixture, directory), { recursive: true }) const config = JSON.parse(await readFile("config/instance.example.json", "utf8")) - config.name = "PRIVATE_IDENTITY_SENTINEL" + config.brand_name = "PRIVATE_IDENTITY_SENTINEL" await writeFile(path.join(fixture, "config/instance.local.json"), JSON.stringify(config)) await mkdir(path.join(fixture, "private-input")) await writeFile(path.join(fixture, "private-input/secret.json"), "PRIVATE_SNAPSHOT_SENTINEL") @@ -26,8 +26,27 @@ test("showcase build excludes private data and local identity even when both exi const dashboard = JSON.parse(output) assert.equal(dashboard.demoMode, true) assert.equal(dashboard.syntheticData, true) - assert.deepEqual(dashboard.sources.map((s) => s.definition.id).sort(), ["money:pulse", "overview:daily", "work:focus"]) + assert.deepEqual(dashboard.sources.map((s) => s.definition.id).sort(), [ + "agenda:primary", + "inbox:attention", + "money:pulse", + "news:briefing", + "overview:daily", + "review:weekly", + "work:focus", + ]) } finally { await rm(fixture, { recursive: true, force: true }) } }) + +test("default and showcase pages stay identical", async () => { + const registry = JSON.parse(await readFile("config/sources.json", "utf8")) + for (const source of registry.sources) { + const relative = `${source.domain}/${source.source}/2026-08-24.json` + assert.deepEqual( + JSON.parse(await readFile(`data/examples/${relative}`, "utf8")), + JSON.parse(await readFile(`examples/snapshots/${relative}`, "utf8")), + ) + } +})