4a931832dd072383d2e65c0c4595dec45a68b7d2561b153c5f1de541ceb89ed5| Command | Method | Description |
|---|---|---|
fin [message...] | POST | Finance. Log spending, check budgets, review. |
enrichContext1/**
2 * Finance Core
3 *
4 * Track accounts, log transactions, monitor spending.
5 * The tree is the ledger.
6 */
7
8import log from "../../seed/log.js";
9import { setNodeMode } from "../../seed/modes/registry.js";
10
11let _Node = null;
12let _Note = null;
13let _metadata = null;
14
15export function configure({ Node, Note, metadata }) {
16 _Node = Node;
17 _Note = Note;
18 _metadata = metadata;
19}
20
21const ROLES = {
22 LOG: "log",
23 ACCOUNTS: "accounts",
24 CATEGORIES: "categories",
25 BUDGET: "budget",
26 PROFILE: "profile",
27 HISTORY: "history",
28};
29
30export { ROLES };
31
32// ── Scaffold ──
33
34export async function scaffold(rootId, userId) {
35 if (!_Node) throw new Error("Finance core not configured");
36 const { createNode } = await import("../../seed/tree/treeManagement.js");
37
38 const logNode = await createNode({ name: "Log", parentId: rootId, userId });
39 const accountsNode = await createNode({ name: "Accounts", parentId: rootId, userId });
40 const categoriesNode = await createNode({ name: "Categories", parentId: rootId, userId });
41 const budgetNode = await createNode({ name: "Budget", parentId: rootId, userId });
42 const profileNode = await createNode({ name: "Profile", parentId: rootId, userId });
43 const historyNode = await createNode({ name: "History", parentId: rootId, userId });
44
45 // Default accounts
46 const checking = await createNode({ name: "Checking", parentId: String(accountsNode._id), userId });
47 const savings = await createNode({ name: "Savings", parentId: String(accountsNode._id), userId });
48 const cash = await createNode({ name: "Cash", parentId: String(accountsNode._id), userId });
49 const creditCard = await createNode({ name: "Credit Card", parentId: String(accountsNode._id), userId });
50
51 // Default spending categories
52 for (const cat of ["Food", "Housing", "Transport", "Health", "Entertainment", "Shopping", "Bills", "Other"]) {
53 const catNode = await createNode({ name: cat, parentId: String(categoriesNode._id), userId });
54 await _metadata.setExtMeta(catNode, "finance", { role: "category" });
55 }
56
57 const tags = [
58 [logNode, ROLES.LOG],
59 [accountsNode, ROLES.ACCOUNTS],
60 [categoriesNode, ROLES.CATEGORIES],
61 [budgetNode, ROLES.BUDGET],
62 [profileNode, ROLES.PROFILE],
63 [historyNode, ROLES.HISTORY],
64 ];
65
66 for (const [node, role] of tags) {
67 await _metadata.setExtMeta(node, "finance", { role });
68 }
69
70 // Tag accounts with their type
71 for (const [node, type] of [[checking, "checking"], [savings, "savings"], [cash, "cash"], [creditCard, "credit"]]) {
72 await _metadata.setExtMeta(node, "finance", { role: "account", accountType: type });
73 }
74
75 await setNodeMode(rootId, "respond", "tree:finance-coach");
76 await setNodeMode(logNode._id, "respond", "tree:finance-log");
77
78 const root = await _Node.findById(rootId);
79 if (root) {
80 await _metadata.setExtMeta(root, "finance", {
81 initialized: true,
82 setupPhase: "complete",
83 currency: "USD",
84 });
85 }
86
87 const ids = {};
88 for (const [node, role] of tags) ids[role] = String(node._id);
89
90 log.info("Finance", `Scaffolded under ${rootId}`);
91 return ids;
92}
93
94// ── Find nodes ──
95
96export async function findFinanceNodes(rootId) {
97 if (!_Node) return null;
98 const children = await _Node.find({ parent: rootId }).select("_id name metadata").lean();
99 const result = {};
100 for (const child of children) {
101 const meta = child.metadata instanceof Map
102 ? child.metadata.get("finance")
103 : child.metadata?.finance;
104 if (meta?.role) result[meta.role] = { id: String(child._id), name: child.name };
105 }
106 return result;
107}
108
109export async function isInitialized(rootId) {
110 if (!_Node) return false;
111 const root = await _Node.findById(rootId).select("metadata").lean();
112 if (!root) return false;
113 const meta = root.metadata instanceof Map
114 ? root.metadata.get("finance")
115 : root.metadata?.finance;
116 return !!meta?.initialized;
117}
118
119export async function getSetupPhase(rootId) {
120 if (!_Node) return null;
121 const root = await _Node.findById(rootId).select("metadata").lean();
122 if (!root) return null;
123 const meta = root.metadata instanceof Map
124 ? root.metadata.get("finance")
125 : root.metadata?.finance;
126 return meta?.setupPhase || (meta?.initialized ? "complete" : null);
127}
128
129export async function completeSetup(rootId) {
130 const root = await _Node.findById(rootId);
131 if (!root) return;
132 const existing = _metadata.getExtMeta(root, "finance") || {};
133 await _metadata.setExtMeta(root, "finance", { ...existing, setupPhase: "complete" });
134}
135
136// ── Accounts ──
137
138export async function getAccounts(rootId) {
139 const nodes = await findFinanceNodes(rootId);
140 if (!nodes?.accounts) return [];
141
142 const children = await _Node.find({ parent: nodes.accounts.id })
143 .select("_id name metadata").lean();
144
145 return children.map(c => {
146 const meta = c.metadata instanceof Map
147 ? c.metadata.get("finance")
148 : c.metadata?.finance;
149 const values = c.metadata instanceof Map
150 ? c.metadata.get("values")
151 : c.metadata?.values;
152 return {
153 id: String(c._id),
154 name: c.name,
155 accountType: meta?.accountType || "other",
156 balance: values?.balance || 0,
157 };
158 });
159}
160
161// ── Categories ──
162
163export async function getCategories(rootId) {
164 const nodes = await findFinanceNodes(rootId);
165 if (!nodes?.categories) return [];
166
167 const children = await _Node.find({ parent: nodes.categories.id })
168 .select("_id name metadata").lean();
169
170 return children.map(c => {
171 const values = c.metadata instanceof Map
172 ? c.metadata.get("values")
173 : c.metadata?.values;
174 const goals = c.metadata instanceof Map
175 ? c.metadata.get("goals")
176 : c.metadata?.goals;
177 return {
178 id: String(c._id),
179 name: c.name,
180 spentThisMonth: values?.monthSpent || 0,
181 budget: goals?.monthBudget || 0,
182 };
183 });
184}
185
186// ── Spending summary ──
187
188export async function getMonthSummary(rootId) {
189 const accounts = await getAccounts(rootId);
190 const categories = await getCategories(rootId);
191
192 const totalBalance = accounts.reduce((sum, a) => sum + a.balance, 0);
193 const totalSpent = categories.reduce((sum, c) => sum + c.spentThisMonth, 0);
194 const totalBudget = categories.reduce((sum, c) => sum + c.budget, 0);
195
196 return {
197 accounts,
198 categories: categories.filter(c => c.spentThisMonth > 0 || c.budget > 0),
199 totalBalance,
200 totalSpent,
201 totalBudget,
202 budgetRemaining: totalBudget > 0 ? totalBudget - totalSpent : null,
203 };
204}
205
206// ── Recent transactions from log ──
207
208export async function getRecentTransactions(rootId, limit = 15) {
209 const nodes = await findFinanceNodes(rootId);
210 if (!nodes?.log) return [];
211
212 const { getNotes } = await import("../../seed/tree/notes.js");
213 const result = await getNotes({ nodeId: nodes.log.id, limit });
214 return result?.notes || [];
215}
2161/**
2 * No data work needed. The AI handles everything via modes.
3 * Returns null so the orchestrator routes to the default mode.
4 */
5
6export async function handleMessage() {
7 return null;
8}
91import express from "express";
2import { sendError, ERR } from "../../seed/protocol.js";
3import urlAuth from "../html-rendering/urlAuth.js";
4import { htmlOnly } from "../html-rendering/htmlHelpers.js";
5import Node from "../../seed/models/node.js";
6import { isInitialized, getMonthSummary, getRecentTransactions } from "./core.js";
7import { renderFinanceDashboard } from "./pages/dashboard.js";
8
9const router = express.Router();
10
11router.get("/root/:rootId/finance", urlAuth, htmlOnly, async (req, res) => {
12 try {
13 const { rootId } = req.params;
14 const root = await Node.findById(rootId).select("name metadata").lean();
15 if (!root) return sendError(res, 404, ERR.TREE_NOT_FOUND, "Finance tree not found");
16
17 if (!(await isInitialized(rootId))) {
18 return sendError(res, 404, ERR.TREE_NOT_FOUND, "Finance not initialized");
19 }
20
21 const summary = await getMonthSummary(rootId);
22 const recentTransactions = await getRecentTransactions(rootId, 20);
23
24 res.send(renderFinanceDashboard({
25 rootId,
26 rootName: root.name,
27 summary,
28 recentTransactions,
29 token: req.query.token || null,
30 userId: req.user?._id?.toString() || req.user?.id || null,
31 inApp: !!req.query.inApp,
32 }));
33 } catch (err) {
34 const log = (await import("../../seed/log.js")).default;
35 log.error("Finance", `Dashboard error: ${err.message}`);
36 sendError(res, 500, ERR.INTERNAL, "Finance dashboard failed");
37 }
38});
39
40export default router;
411import log from "../../seed/log.js";
2import { configure, findFinanceNodes, getAccounts, getCategories, getMonthSummary, isInitialized, scaffold } from "./core.js";
3import { handleMessage } from "./handler.js";
4
5export async function init(core) {
6 configure({
7 Node: core.models.Node,
8 Note: core.models.Note,
9 metadata: core.metadata,
10 });
11
12 // ── Modes ──
13 const logMode = (await import("./modes/log.js")).default;
14 const coachMode = (await import("./modes/coach.js")).default;
15 const reviewMode = (await import("./modes/review.js")).default;
16
17 core.modes.registerMode(logMode.name, logMode, "finance");
18 core.modes.registerMode(coachMode.name, coachMode, "finance");
19 core.modes.registerMode(reviewMode.name, reviewMode, "finance");
20
21 // ── enrichContext: inject financial awareness into ALL tree nodes ──
22 const _financeCache = new Map(); // treeRootId -> { summary, ts }
23 const FINANCE_CACHE_TTL = 60000;
24
25 core.hooks.register("enrichContext", async ({ context, node, meta }) => {
26 if (!node?._id) return;
27
28 // At finance nodes: full detail
29 const finMeta = meta?.finance;
30 if (finMeta?.role === "account") {
31 const values = meta?.values || {};
32 context.financeAccount = { name: node.name, type: finMeta.accountType, balance: values.balance || 0 };
33 return;
34 }
35 if (finMeta?.role === "category") {
36 const values = meta?.values || {};
37 const goals = meta?.goals || {};
38 context.financeCategory = { name: node.name, spent: values.monthSpent || 0, budget: goals.monthBudget || 0 };
39 return;
40 }
41
42 // For all other tree nodes: compact spending summary
43 const treeRootId = node.rootOwner ? String(node.rootOwner) : null;
44 if (!treeRootId) return;
45
46 const cached = _financeCache.get(treeRootId);
47 if (cached && Date.now() - cached.ts < FINANCE_CACHE_TTL) {
48 if (cached.summary) context.financeSummary = cached.summary;
49 return;
50 }
51
52 try {
53 const { getExtension } = await import("../loader.js");
54 const life = getExtension("life");
55 if (!life?.exports?.getDomainNodes) return;
56
57 const domains = await life.exports.getDomainNodes(treeRootId);
58 if (!domains.finance?.id) {
59 _financeCache.set(treeRootId, { summary: null, ts: Date.now() });
60 return;
61 }
62
63 const summary = await getMonthSummary(domains.finance.id);
64 const compact = {
65 totalBalance: summary.totalBalance,
66 spentThisMonth: summary.totalSpent,
67 budgetRemaining: summary.budgetRemaining,
68 };
69
70 _financeCache.set(treeRootId, { summary: compact, ts: Date.now() });
71 if (summary.totalBalance > 0 || summary.totalSpent > 0) {
72 context.financeSummary = compact;
73 }
74 } catch {}
75 }, "finance");
76
77 log.info("Finance", "Loaded. The tree tracks money.");
78
79 // ── Register apps-grid slot ──
80 try {
81 const { getExtension } = await import("../loader.js");
82 const base = getExtension("treeos-base");
83 base?.exports?.registerSlot?.("apps-grid", "finance", ({ userId, rootMap, tokenParam, tokenField, esc: e }) => {
84 const entries = rootMap.get("Finance") || [];
85 const existing = entries.map(entry =>
86 `<a class="app-active" href="/api/v1/root/${entry.id}/finance?html${tokenParam}" style="margin-right:8px;margin-bottom:6px;">${e(entry.name)}</a>`
87 ).join("");
88 return `<div class="app-card">
89 <div class="app-header"><span class="app-emoji">💰</span><span class="app-name">Finance</span></div>
90 <div class="app-desc">Track accounts, log spending in natural language. Budget goals per category. The AI reflects on patterns and helps you think about money.</div>
91 ${entries.length > 0
92 ? `<div style="display:flex;flex-wrap:wrap;">${existing}</div>`
93 : `<form class="app-form" method="POST" action="/api/v1/user/${userId}/apps/create">
94 ${tokenField}<input type="hidden" name="app" value="finance" />
95 <input class="app-input" name="message" placeholder="How much did you spend today?" required />
96 <button class="app-start" type="submit">Start Finance</button>
97 </form>`}
98 </div>`;
99 }, { priority: 60 });
100 } catch {}
101
102 // ── HTML dashboard route ──
103 let router = null;
104 try {
105 const { getExtension } = await import("../loader.js");
106 const htmlExt = getExtension("html-rendering");
107 if (htmlExt) {
108 router = (await import("./htmlRoutes.js")).default;
109 }
110 } catch {}
111
112 return {
113 router,
114 exports: {
115 scaffold,
116 isInitialized,
117 findFinanceNodes,
118 getAccounts,
119 getCategories,
120 getMonthSummary,
121 handleMessage,
122 },
123 };
124}
1251export default {
2 name: "finance",
3 version: "1.0.1",
4 builtFor: "TreeOS",
5 description:
6 "Personal finance. Track accounts, log transactions in natural language, " +
7 "monitor spending by category. Each account is a node with a balance. " +
8 "Say what you spent or earned and the tree updates. Budget goals per category. " +
9 "The AI reflects on patterns, flags overspending, helps you think through " +
10 "decisions. Cross-domain: knows your food spending, gym membership status, " +
11 "how finances relate to your whole life. Type 'be' for a guided check-in " +
12 "on your financial health.",
13
14 territory: "money, spending, budget, income, savings, bills, rent, paycheck, debt, invest, account, bank, credit, debit, purchase, cost, price, afford",
15 classifierHints: [
16 /\$\d+/, // "$45", "$1200"
17 /\b\d+\s*(dollars?|bucks?|usd|eur|gbp)\b/i, // "45 dollars"
18 /\b(spent|paid|bought|cost|earned|received|deposited|withdrew|transferred|owe|owes)\b/i,
19 /\b(rent|mortgage|groceries|subscription|paycheck|salary|income|savings?|budget|invest|crypto|stock)\b/i,
20 /\b(electric|phone|water|internet|gas|utility|medical|cable)\s*bill\b/i,
21 /\b(bank|checking|credit card|debit|account|balance|net worth)\b/i,
22 ],
23
24 needs: {
25 models: ["Node", "Note"],
26 services: ["hooks", "llm", "metadata"],
27 },
28
29 optional: {
30 extensions: [
31 "transactions",
32 "channels",
33 "html-rendering",
34 "treeos-base",
35 ],
36 },
37
38 provides: {
39 models: {},
40 routes: false,
41 tools: false,
42 jobs: false,
43 modes: true,
44
45 hooks: {
46 fires: [],
47 listens: ["enrichContext"],
48 },
49
50 cli: [
51 {
52 command: "fin [message...]",
53 scope: ["tree"],
54 description: "Finance. Log spending, check budgets, review.",
55 method: "POST",
56 endpoint: "/root/:rootId/chat",
57 body: ["message"],
58 },
59 ],
60 },
61};
621import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findFinanceNodes, getAccounts, getCategories, getMonthSummary } from "../core.js";
3
4export default {
5 name: "tree:finance-coach",
6 emoji: "📊",
7 label: "Finance Coach",
8 bigMode: "tree",
9 hidden: true,
10
11 maxMessagesBeforeLoop: 6,
12 preserveContextOnLoop: true,
13
14 toolNames: [
15 "navigate-tree",
16 "get-tree-context",
17 "get-tree",
18 "get-node-notes",
19 "create-new-node",
20 "create-node-note",
21 "edit-node-note",
22 "edit-node-value",
23 "get-searched-notes-by-user",
24 ],
25
26 async buildSystemPrompt({ username, rootId, currentNodeId }) {
27 const finRoot = await findExtensionRoot(currentNodeId || rootId, "finance") || rootId;
28 const summary = finRoot ? await getMonthSummary(finRoot) : null;
29
30 const accountList = summary?.accounts?.length > 0
31 ? summary.accounts.map(a => `- ${a.name} (${a.accountType}): $${a.balance}`).join("\n")
32 : "No accounts set up.";
33
34 const categoryList = summary?.categories?.length > 0
35 ? summary.categories.map(c => {
36 const pct = c.budget > 0 ? ` (${Math.round((c.spentThisMonth / c.budget) * 100)}%)` : "";
37 return `- ${c.name}: $${c.spentThisMonth}${c.budget > 0 ? `/$${c.budget}` : ""}${pct}`;
38 }).join("\n")
39 : "";
40
41 const totalsBlock = summary
42 ? `Total balance: $${summary.totalBalance}\nSpent this month: $${summary.totalSpent}${summary.totalBudget > 0 ? `\nBudget remaining: $${summary.budgetRemaining}` : ""}`
43 : "";
44
45 const hasActivity = summary && (summary.totalBalance > 0 || summary.totalSpent > 0 || summary.totalBudget > 0);
46
47 return `You are ${username}'s financial coach.
48
49${hasActivity ? "STATUS: Accounts active." : "STATUS: Fresh start. Help them set up accounts and budgets."}
50
51${totalsBlock ? `FINANCIAL SNAPSHOT:\n${totalsBlock}\n` : ""}
52${accountList ? `ACCOUNTS:\n${accountList}\n` : ""}
53${categoryList ? `SPENDING THIS MONTH:\n${categoryList}\n` : ""}
54
55Your role: help ${username} think about money clearly. Budget setting, savings goals, spending awareness, debt strategy. You track and reflect. You don't predict markets or give investment advice.
56
57CAPABILITIES:
58- Set budget goals on category nodes (edit-node-value, key "monthBudget")
59- Create new accounts or categories
60- Review spending patterns from Log notes
61- Set account balances
62
63BEHAVIOR:
64- Be direct about numbers. Don't sugarcoat overspending.
65- When they ask "can I afford X", look at balances and upcoming obligations.
66- When they ask about budgets, show what they've spent vs. their goals.
67- Suggest concrete actions: "move $200 to savings" not "consider saving more."
68- If they ask about investments, stocks, crypto: help them think through the decision but be clear you track, you don't predict. Ask about their timeline, risk tolerance, and what they can afford to lose.
69- Cross-domain awareness: if you see food spending data, fitness membership costs, or other life context, use it naturally. "Your food spending is up 30% since you started bulking. That tracks."
70- Never expose node IDs or metadata to the user.`.trim();
71 },
72};
731import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { findFinanceNodes, getAccounts, getCategories } from "../core.js";
3
4export default {
5 name: "tree:finance-log",
6 emoji: "💰",
7 label: "Finance Log",
8 bigMode: "tree",
9 hidden: true,
10
11 maxMessagesBeforeLoop: 6,
12 preserveContextOnLoop: true,
13
14 toolNames: [
15 "navigate-tree",
16 "get-tree-context",
17 "get-tree",
18 "get-node-notes",
19 "create-new-node",
20 "create-node-note",
21 "edit-node-note",
22 "edit-node-value",
23 "edit-node-goal",
24 ],
25
26 async buildSystemPrompt({ username, rootId, currentNodeId }) {
27 const finRoot = await findExtensionRoot(currentNodeId || rootId, "finance") || rootId;
28 const nodes = finRoot ? await findFinanceNodes(finRoot) : null;
29 const accounts = finRoot ? await getAccounts(finRoot) : [];
30 const categories = finRoot ? await getCategories(finRoot) : [];
31
32 const accountList = accounts.length > 0
33 ? accounts.map(a => `- ${a.name} (${a.accountType}): $${a.balance}`).join("\n")
34 : "No accounts yet.";
35
36 const categoryList = categories.length > 0
37 ? categories.map(c => {
38 const budgetPart = c.budget > 0 ? ` / $${c.budget} budget` : "";
39 return `- ${c.name}: $${c.spentThisMonth} this month${budgetPart}`;
40 }).join("\n")
41 : "Default categories available.";
42
43 const logId = nodes?.log?.id;
44 const accountsId = nodes?.accounts?.id;
45 const categoriesId = nodes?.categories?.id;
46
47 return `You are logging financial transactions for ${username}.
48
49ACCOUNTS:
50${accountList}
51
52CATEGORIES:
53${categoryList}
54
55The user tells you about money they spent, earned, or moved. Parse it and record it.
56
57WORKFLOW:
581. Parse the transaction: amount, what it was for, which account (default: Checking).
592. Write a note to Log (${logId || "find it"}) with: "$AMOUNT on DESCRIPTION from ACCOUNT".
603. Find the right category under Categories (${categoriesId || "find it"}). If none fits, use "Other" or create a new one.
614. Increment the category's monthSpent value: edit-node-value on the category node, key "monthSpent", amount spent.
625. Update the account balance: edit-node-value on the account node, key "balance", negative for spending, positive for income.
636. Confirm: "Spent $45 on groceries from Checking. Food: $57/$200 this month."
64
65PARSING RULES:
66- "spent $45 on groceries" = debit $45 from default account, category Food
67- "paid rent $1200" = debit $1200, category Housing
68- "got paid $2000" = credit $2000 to default account, category Income
69- "transferred $500 to savings" = debit Checking, credit Savings
70- If no amount given, ask.
71- If category is ambiguous, pick the closest match. Don't ask unless truly unclear.
72- Currency is USD unless configured differently.
73
74RULES:
75- One log note per transaction. Terse. "$45 groceries (Checking)"
76- Always update both the category spent AND the account balance.
77- Never expose node IDs or metadata to the user.
78- Confirm in one line with running totals.`.trim();
79 },
80};
811import { findExtensionRoot } from "../../../seed/tree/extensionMetadata.js";
2import { getMonthSummary, getRecentTransactions } from "../core.js";
3
4export default {
5 name: "tree:finance-review",
6 emoji: "🔍",
7 label: "Finance Review",
8 bigMode: "tree",
9 hidden: true,
10
11 maxMessagesBeforeLoop: 6,
12 preserveContextOnLoop: true,
13
14 toolNames: [
15 "navigate-tree",
16 "get-tree-context",
17 "get-tree",
18 "get-node-notes",
19 "get-searched-notes-by-user",
20 ],
21
22 async buildSystemPrompt({ username, rootId, currentNodeId }) {
23 const finRoot = await findExtensionRoot(currentNodeId || rootId, "finance") || rootId;
24 const summary = finRoot ? await getMonthSummary(finRoot) : null;
25 const recent = finRoot ? await getRecentTransactions(finRoot, 20) : [];
26
27 const accountList = summary?.accounts?.length > 0
28 ? summary.accounts.map(a => `- ${a.name} (${a.accountType}): $${a.balance}`).join("\n")
29 : "No accounts.";
30
31 const categoryList = summary?.categories?.length > 0
32 ? summary.categories
33 .sort((a, b) => b.spentThisMonth - a.spentThisMonth)
34 .map(c => {
35 const pct = c.budget > 0 ? ` (${Math.round((c.spentThisMonth / c.budget) * 100)}%)` : "";
36 const over = c.budget > 0 && c.spentThisMonth > c.budget ? " OVER BUDGET" : "";
37 return `- ${c.name}: $${c.spentThisMonth}${c.budget > 0 ? `/$${c.budget}` : ""}${pct}${over}`;
38 }).join("\n")
39 : "";
40
41 const recentList = recent.length > 0
42 ? recent.slice(0, 15).map(n => `- ${n.content}`).join("\n")
43 : "No recent transactions.";
44
45 return `You are reviewing ${username}'s finances. Show the full picture. Be honest.
46
47ACCOUNTS:
48${accountList}
49
50${categoryList ? `SPENDING BY CATEGORY (this month):\n${categoryList}\n` : ""}
51TOTAL SPENT: $${summary?.totalSpent || 0}
52${summary?.totalBudget > 0 ? `TOTAL BUDGET: $${summary.totalBudget}\nREMAINING: $${summary.budgetRemaining}` : ""}
53
54RECENT TRANSACTIONS:
55${recentList}
56
57BEHAVIOR:
58- Answer their question directly with numbers.
59- "How much did I spend on food?" = look at Food category total.
60- "What's my balance?" = show all accounts.
61- "How am I doing this month?" = compare spending to budgets, highlight overages.
62- Flag concerning patterns: overspending in one category, declining balances, no savings.
63- Be factual. Don't moralize. Just show the numbers and let them decide.
64- Never expose node IDs or metadata to the user.`.trim();
65 },
66};
671/**
2 * Finance Dashboard
3 *
4 * Builds from getMonthSummary() data. Renders via the generic app dashboard.
5 * Accounts show balances. Categories show spending vs budget.
6 * Recent transactions from the log.
7 */
8
9import { renderAppDashboard } from "../../html-rendering/html/appDashboard.js";
10
11const CATEGORY_COLORS = {
12 Food: "#48bb78",
13 Housing: "#667eea",
14 Transport: "#f6ad55",
15 Health: "#fc8181",
16 Entertainment: "#a78bfa",
17 Shopping: "#ed64a6",
18 Bills: "#4fd1c5",
19 Other: "#718096",
20};
21
22export function renderFinanceDashboard({ rootId, rootName, summary, recentTransactions, token, userId, inApp }) {
23 const s = summary || {};
24 const accounts = s.accounts || [];
25 const categories = s.categories || [];
26 const totalBalance = s.totalBalance || 0;
27 const totalSpent = s.totalSpent || 0;
28 const totalBudget = s.totalBudget || 0;
29 const budgetRemaining = s.budgetRemaining;
30
31 // Hero: total balance
32 const hero = {
33 value: `$${totalBalance.toLocaleString()}`,
34 label: totalBudget > 0
35 ? `$${totalSpent.toLocaleString()} spent of $${totalBudget.toLocaleString()} budget`
36 : `$${totalSpent.toLocaleString()} spent this month`,
37 color: budgetRemaining != null && budgetRemaining < 0 ? "#fc8181" : "#48bb78",
38 sub: budgetRemaining != null && budgetRemaining >= 0
39 ? `$${budgetRemaining.toLocaleString()} remaining`
40 : budgetRemaining != null
41 ? `$${Math.abs(budgetRemaining).toLocaleString()} over budget`
42 : null,
43 };
44
45 // Stats: account balances
46 const stats = accounts.map(a => ({
47 value: `$${a.balance.toLocaleString()}`,
48 label: a.name,
49 }));
50
51 // Bars: category spending vs budget
52 const bars = categories
53 .filter(c => c.spentThisMonth > 0 || c.budget > 0)
54 .sort((a, b) => b.spentThisMonth - a.spentThisMonth)
55 .map(c => ({
56 label: c.name,
57 current: c.spentThisMonth,
58 goal: c.budget || 0,
59 color: CATEGORY_COLORS[c.name] || "#a78bfa",
60 unit: "$",
61 }));
62
63 // Cards: recent transactions
64 const txItems = (recentTransactions || []).slice(0, 20).map(n => ({
65 text: n.content || "Transaction",
66 sub: n.createdAt ? new Date(n.createdAt).toLocaleDateString() : null,
67 }));
68
69 const cards = [];
70 if (txItems.length > 0) {
71 cards.push({ title: "Recent Transactions", items: txItems });
72 }
73
74 // Account details card
75 if (accounts.length > 0) {
76 cards.push({
77 title: "Accounts",
78 items: accounts.map(a => ({
79 text: `${a.name} (${a.accountType})`,
80 detail: [`$${a.balance.toLocaleString()}`],
81 })),
82 });
83 }
84
85 return renderAppDashboard({
86 rootId,
87 rootName: rootName || "Finance",
88 token,
89 userId,
90 inApp: !!inApp,
91 subtitle: new Date().toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" }),
92 hero,
93 stats,
94 bars,
95 cards,
96 chatBar: {
97 placeholder: "Log a transaction or ask about spending...",
98 endpoint: `/api/v1/root/${rootId}/chat`,
99 },
100 emptyState: totalSpent === 0 && accounts.every(a => a.balance === 0)
101 ? { title: "No transactions yet", message: "Tell me what you spent or earned. The tree tracks it." }
102 : null,
103 });
104}
105
treeos ext star finance
Post comments from the CLI: treeos ext comment finance "your comment"
Max 3 comments per extension. One star and one flag per user.
Loading comments...