@bendyline/gilde 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/data/project-types/ca/caregiving-binder/versions/1.0.1/about.md +8 -0
- package/data/project-types/ca/caregiving-binder/versions/1.0.1/care.json +4 -0
- package/data/project-types/ca/caregiving-binder/versions/1.0.1/manifest.json +177 -0
- package/data/project-types/ca/caregiving-binder/versions/1.0.1/meds.json +8 -0
- package/data/project-types/ca/caregiving-binder/versions/1.0.1/mission.md +8 -0
- package/data/project-types/ca/caregiving-binder/versions/1.0.1/pages/dashboard/index.html +167 -0
- package/data/project-types/ch/checkers/versions/1.1.2/about.md +9 -0
- package/data/project-types/ch/checkers/versions/1.1.2/game.json +77 -0
- package/data/project-types/ch/checkers/versions/1.1.2/manifest.json +166 -0
- package/data/project-types/ch/checkers/versions/1.1.2/mission.md +9 -0
- package/data/project-types/ch/checkers/versions/1.1.2/pages/board/index.html +1091 -0
- package/data/project-types/ch/chess/versions/1.0.2/about.md +9 -0
- package/data/project-types/ch/chess/versions/1.0.2/game.json +127 -0
- package/data/project-types/ch/chess/versions/1.0.2/manifest.json +180 -0
- package/data/project-types/ch/chess/versions/1.0.2/mission.md +8 -0
- package/data/project-types/ch/chess/versions/1.0.2/pages/board/index.html +627 -0
- package/data/project-types/ch/chess/versions/1.0.3/about.md +9 -0
- package/data/project-types/ch/chess/versions/1.0.3/game.json +127 -0
- package/data/project-types/ch/chess/versions/1.0.3/manifest.json +180 -0
- package/data/project-types/ch/chess/versions/1.0.3/mission.md +8 -0
- package/data/project-types/ch/chess/versions/1.0.3/pages/board/index.html +769 -0
- package/data/project-types/de/design-scheme/versions/1.0.1/about.md +6 -0
- package/data/project-types/de/design-scheme/versions/1.0.1/manifest.json +69 -0
- package/data/project-types/de/design-scheme/versions/1.0.1/mission.md +3 -0
- package/data/project-types/de/design-scheme/versions/1.0.1/pages/gallery/index.html +105 -0
- package/data/project-types/de/design-scheme/versions/1.0.1/schemes/index.json +9 -0
- package/data/project-types/de/design-scheme/versions/1.0.1/schemes/warm-dusk.json +4 -0
- package/data/project-types/ev/event-planner/versions/1.0.1/about.md +7 -0
- package/data/project-types/ev/event-planner/versions/1.0.1/budget.json +5 -0
- package/data/project-types/ev/event-planner/versions/1.0.1/guests.json +10 -0
- package/data/project-types/ev/event-planner/versions/1.0.1/manifest.json +170 -0
- package/data/project-types/ev/event-planner/versions/1.0.1/mission.md +6 -0
- package/data/project-types/ev/event-planner/versions/1.0.1/pages/dashboard/index.html +186 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.1/about.md +7 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.1/manifest.json +125 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.1/mission.md +7 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.1/pages/dashboard/index.html +172 -0
- package/data/project-types/fi/fitness-coach/versions/1.0.1/training.json +6 -0
- package/data/project-types/fl/flashcards/versions/1.0.1/about.md +8 -0
- package/data/project-types/fl/flashcards/versions/1.0.1/deck.json +7 -0
- package/data/project-types/fl/flashcards/versions/1.0.1/manifest.json +167 -0
- package/data/project-types/fl/flashcards/versions/1.0.1/mission.md +6 -0
- package/data/project-types/fl/flashcards/versions/1.0.1/pages/review/index.html +396 -0
- package/data/project-types/fr/freelance-office/versions/1.0.1/about.md +6 -0
- package/data/project-types/fr/freelance-office/versions/1.0.1/invoices.json +9 -0
- package/data/project-types/fr/freelance-office/versions/1.0.1/manifest.json +142 -0
- package/data/project-types/fr/freelance-office/versions/1.0.1/mission.md +6 -0
- package/data/project-types/fr/freelance-office/versions/1.0.1/pages/dashboard/index.html +157 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.1/about.md +7 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.1/donations.json +7 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.1/manifest.json +126 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.1/mission.md +6 -0
- package/data/project-types/fu/fundraiser-hq/versions/1.0.1/pages/dashboard/index.html +160 -0
- package/data/project-types/go/go/versions/1.0.1/about.md +9 -0
- package/data/project-types/go/go/versions/1.0.1/game.json +103 -0
- package/data/project-types/go/go/versions/1.0.1/manifest.json +166 -0
- package/data/project-types/go/go/versions/1.0.1/mission.md +8 -0
- package/data/project-types/go/go/versions/1.0.1/pages/board/index.html +778 -0
- package/data/project-types/ho/household-budget/versions/1.0.1/about.md +6 -0
- package/data/project-types/ho/household-budget/versions/1.0.1/ledger.json +5 -0
- package/data/project-types/ho/household-budget/versions/1.0.1/manifest.json +163 -0
- package/data/project-types/ho/household-budget/versions/1.0.1/mission.md +6 -0
- package/data/project-types/ho/household-budget/versions/1.0.1/pages/dashboard/index.html +233 -0
- package/data/project-types/ho/household-manual/versions/1.0.1/about.md +7 -0
- package/data/project-types/ho/household-manual/versions/1.0.1/manifest.json +152 -0
- package/data/project-types/ho/household-manual/versions/1.0.1/mission.md +8 -0
- package/data/project-types/ho/household-manual/versions/1.0.1/pages/dashboard/index.html +157 -0
- package/data/project-types/ho/household-manual/versions/1.0.1/systems.json +8 -0
- package/data/project-types/index.json +1 -1
- package/data/project-types/jo/job-hunt/versions/1.0.1/about.md +10 -0
- package/data/project-types/jo/job-hunt/versions/1.0.1/activity.json +4 -0
- package/data/project-types/jo/job-hunt/versions/1.0.1/manifest.json +212 -0
- package/data/project-types/jo/job-hunt/versions/1.0.1/mission.md +9 -0
- package/data/project-types/jo/job-hunt/versions/1.0.1/pages/dashboard/index.html +327 -0
- package/data/project-types/jo/job-hunt/versions/1.0.1/pipeline.json +6 -0
- package/data/project-types/la/language-trainer/versions/1.0.1/about.md +6 -0
- package/data/project-types/la/language-trainer/versions/1.0.1/manifest.json +79 -0
- package/data/project-types/la/language-trainer/versions/1.0.1/mission.md +4 -0
- package/data/project-types/la/language-trainer/versions/1.0.1/pages/dashboard/index.html +153 -0
- package/data/project-types/la/language-trainer/versions/1.0.1/progress.json +7 -0
- package/data/project-types/li/life-binder/versions/1.0.1/about.md +7 -0
- package/data/project-types/li/life-binder/versions/1.0.1/documents.json +145 -0
- package/data/project-types/li/life-binder/versions/1.0.1/manifest.json +137 -0
- package/data/project-types/li/life-binder/versions/1.0.1/mission.md +7 -0
- package/data/project-types/li/life-binder/versions/1.0.1/pages/dashboard/index.html +147 -0
- package/data/project-types/me/meal-planner/versions/1.0.1/about.md +6 -0
- package/data/project-types/me/meal-planner/versions/1.0.1/manifest.json +159 -0
- package/data/project-types/me/meal-planner/versions/1.0.1/menu.json +5 -0
- package/data/project-types/me/meal-planner/versions/1.0.1/mission.md +5 -0
- package/data/project-types/me/meal-planner/versions/1.0.1/pages/dashboard/index.html +152 -0
- package/data/project-types/me/meal-planner/versions/1.0.1/pantry.json +9 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.1/about.md +7 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.1/chapters.json +10 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.1/manifest.json +157 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.1/mission.md +7 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.1/pages/dashboard/index.html +165 -0
- package/data/project-types/no/novel-writing-room/versions/1.0.1/sessions.json +6 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/about.md +7 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/journal.json +4 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/manifest.json +193 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/mission.md +4 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/packing.json +8 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/pages/dashboard/index.html +215 -0
- package/data/project-types/tr/trip-planner/versions/1.0.1/trip.json +7 -0
- package/package.json +4 -2
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
The household ledger, kept honest. Tell your Penningmeester when money moves —
|
|
2
|
+
"14.20 on groceries", "salary landed" — and it goes straight into the ledger on
|
|
3
|
+
this machine. Nothing leaves your disk.
|
|
4
|
+
|
|
5
|
+
The **Output** tab shows the month at a glance: in, out, net ({{currency}}), the
|
|
6
|
+
biggest categories, and a quick-add box for the receipt in your hand.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"releasedAt": "2026-08-01T00:00:00Z",
|
|
5
|
+
"meesterManaged": false,
|
|
6
|
+
"params": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"currency": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"title": "Currency",
|
|
12
|
+
"description": "Shown on the dashboard; entries are unit-agnostic.",
|
|
13
|
+
"default": "USD"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"required": [
|
|
17
|
+
"currency"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"nameTemplate": "Household Budget",
|
|
21
|
+
"aboutTemplate": "about.md",
|
|
22
|
+
"missionTemplate": "mission.md",
|
|
23
|
+
"gezels": [
|
|
24
|
+
{
|
|
25
|
+
"templateId": "penningmeester",
|
|
26
|
+
"voorman": true
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"tools": [
|
|
30
|
+
{
|
|
31
|
+
"name": "add_expense",
|
|
32
|
+
"description": "Record spending the moment it happens (amount in ordinary units).",
|
|
33
|
+
"script": "budget-store",
|
|
34
|
+
"inputs": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"amount": {
|
|
38
|
+
"type": "number"
|
|
39
|
+
},
|
|
40
|
+
"category": {
|
|
41
|
+
"type": "string"
|
|
42
|
+
},
|
|
43
|
+
"note": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"required": [
|
|
48
|
+
"amount",
|
|
49
|
+
"category"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"bind": {
|
|
53
|
+
"action": "add_expense"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "add_income",
|
|
58
|
+
"description": "Record money coming in.",
|
|
59
|
+
"script": "budget-store",
|
|
60
|
+
"inputs": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"amount": {
|
|
64
|
+
"type": "number"
|
|
65
|
+
},
|
|
66
|
+
"category": {
|
|
67
|
+
"type": "string"
|
|
68
|
+
},
|
|
69
|
+
"note": {
|
|
70
|
+
"type": "string"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"required": [
|
|
74
|
+
"amount",
|
|
75
|
+
"category"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"bind": {
|
|
79
|
+
"action": "add_income"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "budget_status",
|
|
84
|
+
"description": "This month's shape: in, out, net, and the biggest categories.",
|
|
85
|
+
"script": "budget-store",
|
|
86
|
+
"inputs": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"properties": {}
|
|
89
|
+
},
|
|
90
|
+
"bind": {
|
|
91
|
+
"action": "status"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "month_summary",
|
|
96
|
+
"description": "Summarize any month ('YYYY-MM').",
|
|
97
|
+
"script": "budget-store",
|
|
98
|
+
"inputs": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"properties": {
|
|
101
|
+
"month": {
|
|
102
|
+
"type": "string"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"required": [
|
|
106
|
+
"month"
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
"bind": {
|
|
110
|
+
"action": "month_summary"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "record_expense",
|
|
115
|
+
"description": "The dashboard's quick-add expense entry.",
|
|
116
|
+
"script": "budget-store",
|
|
117
|
+
"inputs": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"properties": {
|
|
120
|
+
"amount": {
|
|
121
|
+
"type": "number"
|
|
122
|
+
},
|
|
123
|
+
"category": {
|
|
124
|
+
"type": "string"
|
|
125
|
+
},
|
|
126
|
+
"note": {
|
|
127
|
+
"type": "string"
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"required": [
|
|
131
|
+
"amount",
|
|
132
|
+
"category"
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"bind": {
|
|
136
|
+
"action": "add_expense"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
],
|
|
140
|
+
"craftbooks": [
|
|
141
|
+
"subscription-audit",
|
|
142
|
+
"month-close"
|
|
143
|
+
],
|
|
144
|
+
"schedules": [],
|
|
145
|
+
"pages": {
|
|
146
|
+
"entry": "dashboard/index.html",
|
|
147
|
+
"reads": [
|
|
148
|
+
{
|
|
149
|
+
"source": "workspace",
|
|
150
|
+
"path": "ledger.json"
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
"tools": [
|
|
154
|
+
"record_expense"
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
"workspaceSeed": [
|
|
158
|
+
"ledger.json"
|
|
159
|
+
],
|
|
160
|
+
"scripts": {
|
|
161
|
+
"budget-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\nimport { ledgerStore } from '@bendyline/gezel-sdk/stores';\n\nexport const meta = defineScript({\n name: 'budget-store',\n description:\n \"Read and update the household ledger (ledger.json). Call action:'status' for this month's shape (in/out/net, top categories); 'add_expense' or 'add_income' the moment money moves (amount in ordinary units, e.g. 12.50); 'month_summary' with month 'YYYY-MM' for any past month.\",\n inputs: {\n action: {\n type: 'string',\n description: \"One of 'status', 'add_expense', 'add_income', or 'month_summary'.\",\n required: true,\n },\n amount: { type: 'number', description: 'Amount in ordinary currency units, e.g. 12.50.' },\n category: { type: 'string', description: \"Category, e.g. 'groceries', 'rent', 'salary'.\" },\n note: { type: 'string', description: 'Short note on the entry.' },\n month: { type: 'string', description: \"Month for month_summary, e.g. '2026-06'.\" },\n },\n outputs: {\n entries: { type: 'number', description: 'Total ledger entries.' },\n summary: { type: 'string', description: 'Human-readable money summary.' },\n netCents: { type: 'number', description: 'Net for the summarized month, in cents.' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\n// ── budget-rules-start ──\n// Pure derivations shared with tests: cents conversion and the month\n// summary line, kept engine-side so the dashboard, the tools, and the\n// tests agree on one rendering.\n\nfunction toCents(amount) {\n if (typeof amount !== 'number' || !isFinite(amount) || amount <= 0) return null;\n return Math.round(amount * 100);\n}\n\nfunction fmtCents(cents) {\n var sign = cents < 0 ? '-' : '';\n var abs = Math.abs(cents);\n return sign + Math.floor(abs / 100) + '.' + String(abs % 100).padStart(2, '0');\n}\n\nfunction topCategories(byCategory, kind, limit) {\n return Object.entries(byCategory)\n .map(function (pair) {\n return { category: pair[0], cents: kind === 'out' ? pair[1].outCents : pair[1].inCents };\n })\n .filter(function (row) {\n return row.cents > 0;\n })\n .sort(function (a, b) {\n return b.cents - a.cents;\n })\n .slice(0, limit);\n}\n\nfunction monthLine(month, totals) {\n var top = topCategories(totals.byCategory, 'out', 3)\n .map(function (row) {\n return row.category + ' ' + fmtCents(row.cents);\n })\n .join(', ');\n return (\n month +\n ': in ' +\n fmtCents(totals.inCents) +\n ', out ' +\n fmtCents(totals.outCents) +\n ', net ' +\n fmtCents(totals.netCents) +\n (top ? '. Biggest: ' + top + '.' : '. No spending recorded.')\n );\n}\n// ── budget-rules-end ──\n\nconst ledger = ledgerStore(gezel.fs, 'ledger.json');\nconst currentMonth = new Date().toISOString().slice(0, 7);\n\nif (input.action === 'add_expense' || input.action === 'add_income') {\n const cents = toCents(input.amount);\n if (cents === null) throw new Error(\"Pass a positive 'amount' in ordinary units, e.g. 12.50.\");\n if (!input.category) throw new Error(\"Pass a 'category', e.g. 'groceries' or 'salary'.\");\n await ledger.add({\n amountCents: cents,\n kind: input.action === 'add_expense' ? 'out' : 'in',\n category: input.category.toLowerCase(),\n ...(input.note ? { note: input.note } : {}),\n });\n} else if (input.action !== 'status' && input.action !== 'month_summary') {\n throw new Error(\n `Unknown action '${input.action}'. Use status, add_expense, add_income, or month_summary.`,\n );\n}\n\nconst month =\n input.action === 'month_summary' && input.month ? input.month : currentMonth;\nconst totals = await ledger.totals({ month });\nconst all = await ledger.list();\n\ngezel.output({\n entries: all.length,\n summary: monthLine(month, totals),\n netCents: totals.netCents,\n});\n"
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
- Every expense and income lands in the ledger the day it happens.
|
|
2
|
+
- The month gets closed with a twenty-minute review: in, out, net, biggest
|
|
3
|
+
categories, one observation.
|
|
4
|
+
- Quiet leaks get surfaced — recurring charges and drifting categories, with
|
|
5
|
+
figures attached, never a lecture.
|
|
6
|
+
- The data never leaves this machine.
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="gezel-demo" content="standalone" />
|
|
7
|
+
<title>Household budget</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: light dark;
|
|
11
|
+
--bg: #faf7f2; --card: #fff; --ink: #2b2620; --muted: #7a7166;
|
|
12
|
+
--accent: #b5651d; --track: #ece5da; --good: #4a7c59; --warn: #a4552e;
|
|
13
|
+
}
|
|
14
|
+
@media (prefers-color-scheme: dark) {
|
|
15
|
+
:root {
|
|
16
|
+
--bg: #1c1a17; --card: #262320; --ink: #efe9e0; --muted: #a59a8c;
|
|
17
|
+
--accent: #e0894a; --track: #3a352f; --good: #7fae8d; --warn: #d98a63;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
* { box-sizing: border-box; }
|
|
21
|
+
body { margin: 0; font: 15px/1.5 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--ink); padding: 1.5rem 1.25rem; }
|
|
22
|
+
main { max-width: 760px; margin: 0 auto; }
|
|
23
|
+
h1 { font-family: Georgia, 'PT Serif', 'Times New Roman', serif; font-size: 1.35rem; font-weight: 600; margin: 0 0 0.15rem; }
|
|
24
|
+
.sub { color: var(--muted); font-size: 0.9rem; margin: 0 0 1rem; }
|
|
25
|
+
.tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.6rem; margin-bottom: 1rem; }
|
|
26
|
+
.tile { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.7rem 0.85rem; }
|
|
27
|
+
.tile .n { font-size: 1.35rem; font-weight: 700; color: var(--accent); line-height: 1.2; }
|
|
28
|
+
.tile .n.good { color: var(--good); }
|
|
29
|
+
.tile .n.warn { color: var(--warn); }
|
|
30
|
+
.tile .l { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
31
|
+
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
|
|
32
|
+
.panel { background: var(--card); border: 1px solid var(--track); border-radius: 10px; padding: 0.85rem 1rem; font-size: 0.85rem; }
|
|
33
|
+
.panel h2 { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); margin: 0 0 0.5rem; }
|
|
34
|
+
.bar-row { display: grid; grid-template-columns: 7.5rem 1fr 4.5rem; gap: 0.5rem; align-items: center; margin: 0.3rem 0; }
|
|
35
|
+
.bar-row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
36
|
+
.bar { background: var(--track); border-radius: 4px; height: 10px; overflow: hidden; }
|
|
37
|
+
.bar > span { display: block; height: 100%; background: var(--accent); border-radius: 4px; }
|
|
38
|
+
.bar-row .amt { text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
39
|
+
.entries { margin: 0; padding-left: 1.05rem; }
|
|
40
|
+
.entries li { margin: 0.25rem 0; color: var(--muted); }
|
|
41
|
+
.entries li strong { color: var(--ink); font-weight: 600; }
|
|
42
|
+
form { display: grid; grid-template-columns: 5.5rem 1fr auto; gap: 0.4rem; margin-top: 0.5rem; }
|
|
43
|
+
input { font: inherit; color: var(--ink); background: var(--bg); border: 1px solid var(--track); border-radius: 6px; padding: 0.35rem 0.5rem; min-width: 0; }
|
|
44
|
+
button { font: inherit; border: 1px solid var(--track); background: var(--card); color: var(--ink); border-radius: 6px; padding: 0.35rem 0.8rem; cursor: pointer; }
|
|
45
|
+
button:hover { border-color: var(--accent); }
|
|
46
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
47
|
+
.banner { border: 1px solid var(--accent); border-radius: 6px; padding: 0.45rem 0.7rem; font-size: 0.85rem; margin-bottom: 0.75rem; display: none; }
|
|
48
|
+
.banner.show { display: block; }
|
|
49
|
+
.empty { color: var(--muted); font-style: italic; }
|
|
50
|
+
@media (max-width: 640px) { .cols { grid-template-columns: 1fr; } .tiles { grid-template-columns: 1fr 1fr; } }
|
|
51
|
+
+ .demo-banner {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: space-between;
|
|
55
|
+
gap: 0.75rem;
|
|
56
|
+
margin: 0 0 0.85rem;
|
|
57
|
+
padding: 0.5rem 0.65rem;
|
|
58
|
+
border: 1px solid var(--accent, #8a5a2b);
|
|
59
|
+
border-radius: 6px;
|
|
60
|
+
background: color-mix(in srgb, var(--card, var(--surface, #fffdf8)) 88%, var(--accent, #8a5a2b));
|
|
61
|
+
color: var(--ink, #2b2620);
|
|
62
|
+
font-size: 0.8rem;
|
|
63
|
+
}
|
|
64
|
+
.demo-banner[hidden] { display: none; }
|
|
65
|
+
.demo-banner strong { font-weight: 700; }
|
|
66
|
+
.demo-reset {
|
|
67
|
+
flex: none;
|
|
68
|
+
padding: 0.25rem 0.5rem;
|
|
69
|
+
border: 1px solid var(--accent, #8a5a2b);
|
|
70
|
+
border-radius: 6px;
|
|
71
|
+
background: var(--card, var(--surface, #fffdf8));
|
|
72
|
+
color: var(--ink, #2b2620);
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
font: inherit;
|
|
75
|
+
}
|
|
76
|
+
.demo-reset:hover { border-color: var(--ink, #2b2620); }
|
|
77
|
+
</style>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
<main>
|
|
81
|
+
+ <div class="demo-banner" data-gezel-demo-banner hidden>
|
|
82
|
+
<span><strong>Demo mode</strong> — sample data stays in this page.</span>
|
|
83
|
+
<button type="button" class="demo-reset" onclick="location.reload()">Reset demo</button>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="banner" id="banner"></div>
|
|
86
|
+
<h1>Household budget</h1>
|
|
87
|
+
<p class="sub" id="sub">Loading the ledger…</p>
|
|
88
|
+
<div class="tiles">
|
|
89
|
+
<div class="tile"><div class="n good" id="t-in">—</div><div class="l">In this month</div></div>
|
|
90
|
+
<div class="tile"><div class="n warn" id="t-out">—</div><div class="l">Out this month</div></div>
|
|
91
|
+
<div class="tile"><div class="n" id="t-net">—</div><div class="l">Net</div></div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="cols">
|
|
94
|
+
<div class="panel">
|
|
95
|
+
<h2>Where it goes</h2>
|
|
96
|
+
<div id="cats"><p class="empty">No spending recorded this month.</p></div>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="panel">
|
|
99
|
+
<h2>Recent entries</h2>
|
|
100
|
+
<ul class="entries" id="entries"><li class="empty">Nothing yet — tell your Penningmeester, or use quick add.</li></ul>
|
|
101
|
+
<h2 style="margin-top: 0.9rem">Quick add expense</h2>
|
|
102
|
+
<form id="quick">
|
|
103
|
+
<input id="q-amount" type="number" step="0.01" min="0.01" placeholder="12.50" required />
|
|
104
|
+
<input id="q-category" type="text" placeholder="groceries" required />
|
|
105
|
+
<button type="submit" id="q-go">Add</button>
|
|
106
|
+
</form>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</main>
|
|
110
|
+
<script>
|
|
111
|
+
(function () {
|
|
112
|
+
'use strict';
|
|
113
|
+
var gezelPage = (function () {
|
|
114
|
+
var seq = 0; var pending = {};
|
|
115
|
+
window.addEventListener('message', function (event) {
|
|
116
|
+
var d = event.data;
|
|
117
|
+
if (!d || d.__gezelPageResult !== true || typeof d.id !== 'string') return;
|
|
118
|
+
var entry = pending[d.id]; if (!entry) return;
|
|
119
|
+
delete pending[d.id]; clearTimeout(entry.timer);
|
|
120
|
+
if (d.ok) entry.resolve(d); else entry.reject(new Error(d.error || 'invoke failed'));
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
embedded: function () { return demoMode || Boolean(capability && projectId); },
|
|
124
|
+
invoke: function (tool, input) {
|
|
125
|
+
return new Promise(function (resolve, reject) {
|
|
126
|
+
if (demoMode) { resolve(demoInvoke(tool, input)); return; }
|
|
127
|
+
if (window.parent === window) { reject(new Error('not embedded in Gezel')); return; }
|
|
128
|
+
seq += 1; var id = 'pi-' + Date.now() + '-' + seq;
|
|
129
|
+
pending[id] = { resolve: resolve, reject: reject, timer: setTimeout(function () { delete pending[id]; reject(new Error('invoke timed out')); }, 45000) };
|
|
130
|
+
window.parent.postMessage({ __gezelPageInvoke: true, id: id, tool: tool, input: input || {} }, '*');
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
refresh: function () { if (!demoMode && window.parent !== window) window.parent.postMessage({ __gezelPageRefresh: true }, '*'); },
|
|
134
|
+
};
|
|
135
|
+
})();
|
|
136
|
+
|
|
137
|
+
var parts = location.pathname.split('/').filter(Boolean);
|
|
138
|
+
var typeIdx = parts.indexOf('type');
|
|
139
|
+
var capability = typeIdx > 0 ? parts[typeIdx - 1] : null;
|
|
140
|
+
var projectId = typeIdx >= 0 ? parts[typeIdx + 1] : null;
|
|
141
|
+
var failedPolls = 0;
|
|
142
|
+
var demoMode = !capability || !projectId;
|
|
143
|
+
var demoLedger = { currency: 'USD', entries: [
|
|
144
|
+
{ kind: 'in', amountCents: 520000, category: 'Income', note: 'Monthly pay', at: new Date().toISOString() },
|
|
145
|
+
{ kind: 'out', amountCents: 146000, category: 'Housing', note: 'Rent', at: new Date().toISOString() },
|
|
146
|
+
{ kind: 'out', amountCents: 58200, category: 'Groceries', note: 'Weekly shops', at: new Date().toISOString() },
|
|
147
|
+
{ kind: 'out', amountCents: 22400, category: 'Transport', note: 'Train and fuel', at: new Date().toISOString() },
|
|
148
|
+
{ kind: 'out', amountCents: 18900, category: 'Eating out', note: 'Dinner with friends', at: new Date().toISOString() },
|
|
149
|
+
] };
|
|
150
|
+
if (demoMode) document.querySelector('[data-gezel-demo-banner]').hidden = false;
|
|
151
|
+
|
|
152
|
+
function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); }
|
|
153
|
+
function fmt(cents) {
|
|
154
|
+
var sign = cents < 0 ? '-' : '';
|
|
155
|
+
var abs = Math.abs(cents);
|
|
156
|
+
return sign + Math.floor(abs / 100) + '.' + String(abs % 100).padStart(2, '0');
|
|
157
|
+
}
|
|
158
|
+
function setBanner(text) { var el = document.getElementById('banner'); el.textContent = text || ''; el.className = text ? 'banner show' : 'banner'; }
|
|
159
|
+
function demoInvoke(tool, input) {
|
|
160
|
+
if (tool !== 'record_expense') throw new Error('Unknown demo action: ' + tool);
|
|
161
|
+
demoLedger.entries.push({
|
|
162
|
+
kind: 'out',
|
|
163
|
+
amountCents: Math.round(Number(input.amount) * 100),
|
|
164
|
+
category: input.category,
|
|
165
|
+
at: new Date().toISOString(),
|
|
166
|
+
});
|
|
167
|
+
return { ok: true };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function render(ledger) {
|
|
171
|
+
var month = new Date().toISOString().slice(0, 7);
|
|
172
|
+
var entries = (ledger.entries || []).filter(function (e) { return typeof e.at === 'string'; });
|
|
173
|
+
var monthEntries = entries.filter(function (e) { return e.at.slice(0, 7) === month; });
|
|
174
|
+
var inC = 0, outC = 0; var byCat = {};
|
|
175
|
+
monthEntries.forEach(function (e) {
|
|
176
|
+
if (e.kind === 'in') inC += e.amountCents;
|
|
177
|
+
else { outC += e.amountCents; byCat[e.category] = (byCat[e.category] || 0) + e.amountCents; }
|
|
178
|
+
});
|
|
179
|
+
document.getElementById('sub').textContent = month + ' · ' + (ledger.currency || '') + ' · ' + entries.length + ' entr' + (entries.length === 1 ? 'y' : 'ies') + ' all time';
|
|
180
|
+
document.getElementById('t-in').textContent = fmt(inC);
|
|
181
|
+
document.getElementById('t-out').textContent = fmt(outC);
|
|
182
|
+
var net = document.getElementById('t-net');
|
|
183
|
+
net.textContent = fmt(inC - outC);
|
|
184
|
+
net.className = 'n ' + (inC - outC >= 0 ? 'good' : 'warn');
|
|
185
|
+
|
|
186
|
+
var cats = Object.entries(byCat).sort(function (a, b) { return b[1] - a[1]; }).slice(0, 7);
|
|
187
|
+
var max = cats.length ? cats[0][1] : 1;
|
|
188
|
+
document.getElementById('cats').innerHTML = cats.length
|
|
189
|
+
? cats.map(function (pair) {
|
|
190
|
+
return '<div class="bar-row"><span class="name">' + esc(pair[0]) + '</span><span class="bar"><span style="width:' + Math.max(4, Math.round((pair[1] / max) * 100)) + '%"></span></span><span class="amt">' + fmt(pair[1]) + '</span></div>';
|
|
191
|
+
}).join('')
|
|
192
|
+
: '<p class="empty">No spending recorded this month.</p>';
|
|
193
|
+
|
|
194
|
+
var recent = entries.slice(-7).reverse();
|
|
195
|
+
if (recent.length) {
|
|
196
|
+
document.getElementById('entries').innerHTML = recent.map(function (e) {
|
|
197
|
+
return '<li><strong>' + (e.kind === 'in' ? '+' : '-') + fmt(e.amountCents) + '</strong> ' + esc(e.category) + (e.note ? ' — ' + esc(e.note) : '') + '</li>';
|
|
198
|
+
}).join('');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function poll() {
|
|
203
|
+
if (demoMode) { render(demoLedger); return Promise.resolve(); }
|
|
204
|
+
if (!capability || !projectId) return Promise.resolve();
|
|
205
|
+
return fetch('/preview/' + encodeURIComponent(capability) + '/workspace/' + encodeURIComponent(projectId) + '/ledger.json', { cache: 'no-store' })
|
|
206
|
+
.then(function (r) { if (!r.ok) throw new Error('poll ' + r.status); return r.json(); })
|
|
207
|
+
.then(function (next) { failedPolls = 0; render(next); })
|
|
208
|
+
.catch(function () { failedPolls += 1; if (failedPolls === 3) gezelPage.refresh(); });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
document.getElementById('quick').addEventListener('submit', function (ev) {
|
|
212
|
+
ev.preventDefault();
|
|
213
|
+
var amount = parseFloat(document.getElementById('q-amount').value);
|
|
214
|
+
var category = document.getElementById('q-category').value.trim();
|
|
215
|
+
if (!(amount > 0) || !category) return;
|
|
216
|
+
var btn = document.getElementById('q-go');
|
|
217
|
+
btn.disabled = true;
|
|
218
|
+
gezelPage.invoke('record_expense', { amount: amount, category: category })
|
|
219
|
+
.then(function () {
|
|
220
|
+
setBanner('');
|
|
221
|
+
document.getElementById('q-amount').value = '';
|
|
222
|
+
document.getElementById('q-category').value = '';
|
|
223
|
+
return poll();
|
|
224
|
+
})
|
|
225
|
+
.catch(function (err) { setBanner(String(err && err.message ? err.message : err)); })
|
|
226
|
+
.then(function () { btn.disabled = false; });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
poll().then(function () { setInterval(function () { void poll(); }, 5000); });
|
|
230
|
+
})();
|
|
231
|
+
</script>
|
|
232
|
+
</body>
|
|
233
|
+
</html>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
The operations book for {{home}}. Tell your Huismeester about the systems that
|
|
2
|
+
need regular care — the furnace, the gutters, the smoke alarms — and each gets a
|
|
3
|
+
card: what it is, where it lives, what care it needs, how often. Maintenance
|
|
4
|
+
gets logged; the schedule takes care of itself.
|
|
5
|
+
|
|
6
|
+
The **Output** tab shows what the house needs right now: care due and anything
|
|
7
|
+
flagged for attention.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"releasedAt": "2026-08-01T00:00:00Z",
|
|
5
|
+
"meesterManaged": false,
|
|
6
|
+
"params": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"home": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"title": "What kind of home?",
|
|
12
|
+
"description": "e.g. 'a 1960s house', 'a third-floor apartment'.",
|
|
13
|
+
"default": "our home"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"required": [
|
|
17
|
+
"home"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"nameTemplate": "Household Manual",
|
|
21
|
+
"aboutTemplate": "about.md",
|
|
22
|
+
"missionTemplate": "mission.md",
|
|
23
|
+
"gezels": [
|
|
24
|
+
{
|
|
25
|
+
"templateId": "huismeester",
|
|
26
|
+
"voorman": true
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"tools": [
|
|
30
|
+
{
|
|
31
|
+
"name": "record_system",
|
|
32
|
+
"description": "Add a system or appliance card (with a care interval in days when known).",
|
|
33
|
+
"script": "house-store",
|
|
34
|
+
"inputs": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"properties": {
|
|
37
|
+
"name": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
},
|
|
40
|
+
"kind": {
|
|
41
|
+
"type": "string"
|
|
42
|
+
},
|
|
43
|
+
"location": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"care": {
|
|
47
|
+
"type": "string"
|
|
48
|
+
},
|
|
49
|
+
"intervalDays": {
|
|
50
|
+
"type": "number"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"required": [
|
|
54
|
+
"name"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"bind": {
|
|
58
|
+
"action": "record_system"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "log_maintenance",
|
|
63
|
+
"description": "Log that care happened (resets the due clock).",
|
|
64
|
+
"script": "house-store",
|
|
65
|
+
"inputs": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": {
|
|
68
|
+
"id": {
|
|
69
|
+
"type": "string"
|
|
70
|
+
},
|
|
71
|
+
"name": {
|
|
72
|
+
"type": "string"
|
|
73
|
+
},
|
|
74
|
+
"note": {
|
|
75
|
+
"type": "string"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"bind": {
|
|
80
|
+
"action": "log_maintenance"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "flag_attention",
|
|
85
|
+
"description": "Flag a problem on a system (a noise, a drip, a trip).",
|
|
86
|
+
"script": "house-store",
|
|
87
|
+
"inputs": {
|
|
88
|
+
"type": "object",
|
|
89
|
+
"properties": {
|
|
90
|
+
"id": {
|
|
91
|
+
"type": "string"
|
|
92
|
+
},
|
|
93
|
+
"name": {
|
|
94
|
+
"type": "string"
|
|
95
|
+
},
|
|
96
|
+
"note": {
|
|
97
|
+
"type": "string"
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"required": [
|
|
101
|
+
"note"
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
"bind": {
|
|
105
|
+
"action": "flag_attention"
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"name": "house_status",
|
|
110
|
+
"description": "What the house needs: care due and flagged problems.",
|
|
111
|
+
"script": "house-store",
|
|
112
|
+
"inputs": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"properties": {}
|
|
115
|
+
},
|
|
116
|
+
"bind": {
|
|
117
|
+
"action": "status"
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "house_list",
|
|
122
|
+
"description": "Read every system card.",
|
|
123
|
+
"script": "house-store",
|
|
124
|
+
"inputs": {
|
|
125
|
+
"type": "object",
|
|
126
|
+
"properties": {}
|
|
127
|
+
},
|
|
128
|
+
"bind": {
|
|
129
|
+
"action": "house_list"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"craftbooks": [
|
|
134
|
+
"seasonal-maintenance-sweep"
|
|
135
|
+
],
|
|
136
|
+
"schedules": [],
|
|
137
|
+
"pages": {
|
|
138
|
+
"entry": "dashboard/index.html",
|
|
139
|
+
"reads": [
|
|
140
|
+
{
|
|
141
|
+
"source": "workspace",
|
|
142
|
+
"path": "systems.json"
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
"workspaceSeed": [
|
|
147
|
+
"systems.json"
|
|
148
|
+
],
|
|
149
|
+
"scripts": {
|
|
150
|
+
"house-store": "import { type InferredInput, defineScript, gezel } from '@bendyline/gezel-sdk';\nimport { rosterStore } from '@bendyline/gezel-sdk/stores';\n\nexport const meta = defineScript({\n name: 'house-store',\n description:\n \"Read and update the home's system cards (systems.json). Call action:'status' for what's due or flagged; 'record_system' when an appliance/system comes up (with care interval in days when known); 'log_maintenance' when care happens; 'flag_attention' for problems; 'house_list' to read every card.\",\n inputs: {\n action: {\n type: 'string',\n description:\n \"One of 'status', 'record_system', 'log_maintenance', 'flag_attention', or 'house_list'.\",\n required: true,\n },\n name: { type: 'string', description: \"System name, e.g. 'Furnace', 'Smoke alarms'.\" },\n kind: { type: 'string', description: 'What it is (appliance, safety, structure, utility).' },\n location: { type: 'string', description: 'Where it lives in the house.' },\n care: { type: 'string', description: \"The care it needs, e.g. 'replace filter'.\" },\n intervalDays: { type: 'number', description: 'Care interval in days, when known.' },\n note: { type: 'string', description: 'Maintenance or attention note.' },\n id: { type: 'string', description: \"System id, e.g. 'sys-2'.\" },\n },\n outputs: {\n systems: { type: 'number', description: 'System cards in the manual.' },\n due: { type: 'number', description: 'Systems with care due.' },\n flagged: { type: 'number', description: 'Systems flagged for attention.' },\n summary: { type: 'string', description: 'What the house needs.' },\n },\n requires: ['workspace.read', 'workspace.write'],\n} as const);\n\nconst input = gezel.input as InferredInput<typeof meta>;\n\n// ── house-rules-start ──\n// Pure derivations over rosterStore-shaped system cards: statuses are\n// ok -> attention; \"due\" derives from lastDoneAt + intervalDays.\n\nvar DAY_MS = 24 * 60 * 60 * 1000;\n\nfunction isDue(record, nowMs) {\n var f = record.fields || {};\n if (typeof f.intervalDays !== 'number' || f.intervalDays <= 0) return false;\n var last = typeof f.lastDoneAt === 'string' ? Date.parse(f.lastDoneAt) : Date.parse(record.createdAt);\n return isFinite(last) && nowMs - last >= f.intervalDays * DAY_MS;\n}\n\nfunction houseShape(records, nowMs) {\n var due = [];\n var flagged = [];\n records.forEach(function (r) {\n if (isDue(r, nowMs)) due.push(r);\n if (r.status === 'attention') flagged.push(r);\n });\n return { due: due, flagged: flagged };\n}\n\nfunction houseLine(records, nowMs) {\n var shape = houseShape(records, nowMs);\n var names = function (rows) {\n return rows\n .map(function (r) {\n return (r.fields && r.fields.name) || r.id;\n })\n .join(', ');\n };\n if (records.length === 0) return 'No systems recorded yet — start with the ones that need regular care.';\n var bits = [];\n if (shape.due.length) bits.push('care due: ' + names(shape.due));\n if (shape.flagged.length) bits.push('flagged: ' + names(shape.flagged));\n return records.length + ' system(s) on record. ' + (bits.length ? bits.join('; ') + '.' : 'Nothing due — the house is quiet.');\n}\n// ── house-rules-end ──\n\nconst house = rosterStore(gezel.fs, 'systems.json', {\n stages: ['ok', 'attention'],\n idPrefix: 'sys',\n});\n\nasync function resolveSystem(): Promise<string> {\n if (input.id) return input.id;\n if (input.name) {\n const wanted = input.name.toLowerCase();\n const matches = (await house.list()).filter(\n (r) => String(r.fields.name ?? '').toLowerCase() === wanted,\n );\n if (matches.length === 1) return matches[0]!.id;\n }\n throw new Error(\"Pass 'id' (e.g. 'sys-2') or an exact system 'name'.\");\n}\n\nif (input.action === 'record_system') {\n if (!input.name) throw new Error(\"record_system needs 'name'.\");\n await house.add({\n status: 'ok',\n fields: {\n name: input.name,\n ...(input.kind ? { kind: input.kind } : {}),\n ...(input.location ? { location: input.location } : {}),\n ...(input.care ? { care: input.care } : {}),\n ...(typeof input.intervalDays === 'number' ? { intervalDays: input.intervalDays } : {}),\n lastDoneAt: new Date().toISOString(),\n },\n });\n} else if (input.action === 'log_maintenance') {\n const id = await resolveSystem();\n await house.update(id, {\n fields: { lastDoneAt: new Date().toISOString() },\n ...(input.note ? { note: input.note } : {}),\n });\n await house.transition(id, 'ok');\n} else if (input.action === 'flag_attention') {\n const id = await resolveSystem();\n await house.transition(id, 'attention', input.note ? { note: input.note } : {});\n} else if (input.action !== 'status' && input.action !== 'house_list') {\n throw new Error(\n `Unknown action '${input.action}'. Use status, record_system, log_maintenance, flag_attention, or house_list.`,\n );\n}\n\nconst records = await house.list();\nconst shape = houseShape(records, Date.now());\nconst listing =\n input.action === 'house_list'\n ? records\n .map(\n (r) =>\n `${r.id} [${r.status}${isDue(r, Date.now()) ? ', due' : ''}] ${String(r.fields.name ?? '?')}${r.fields.location ? ` — ${String(r.fields.location)}` : ''}${r.fields.care ? ` (${String(r.fields.care)}${r.fields.intervalDays ? ` every ${String(r.fields.intervalDays)}d` : ''})` : ''}`,\n )\n .join('\\n') || 'No systems recorded yet.'\n : '';\n\ngezel.output({\n systems: records.length,\n due: shape.due.length,\n flagged: shape.flagged.length,\n summary: listing || houseLine(records, Date.now()),\n});\n"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
- Every system that needs regular care has a card with its interval; nothing
|
|
2
|
+
relies on memory.
|
|
3
|
+
- Care gets logged the day it happens; "what does the house need?" is answered
|
|
4
|
+
from the record, not a generic checklist.
|
|
5
|
+
- Problems get flagged on the system's card the day they're noticed, so repairs
|
|
6
|
+
start from history.
|
|
7
|
+
- Safety honesty: gas, mains electricity, and structure always get the "call a
|
|
8
|
+
professional" line when it belongs.
|