diff --git a/plugins/financial-advisor/src/index.ts b/plugins/financial-advisor/src/index.ts index ed01c3e..4e681aa 100644 --- a/plugins/financial-advisor/src/index.ts +++ b/plugins/financial-advisor/src/index.ts @@ -5,11 +5,12 @@ * inference rules, budgets, goals, reports, and contextual advice. */ -import type { RadixPlugin } from '@plures/pares-radix'; +import type { RadixPlugin, PluginContext } from '@plures/pares-radix'; import { recurringAmountPattern } from './rules/recurring-amount.js'; import { vendorClustering } from './rules/vendor-clustering.js'; import { refundDetection } from './rules/refund-detection.js'; import { taxVarianceDetection } from './rules/tax-variance.js'; +import { setPluginContext, clearPluginContext } from './lib/context.js'; const financialAdvisor: RadixPlugin = { id: 'financial-advisor', @@ -219,13 +220,15 @@ const financialAdvisor: RadixPlugin = { constraints: [], - async onActivate() { + async onActivate(ctx: PluginContext) { console.log('[financial-advisor] Plugin activated'); - // TODO: Initialize PluresDB collections, load inference rules + setPluginContext(ctx); + // TODO: load inference rules into inference engine }, async onDeactivate() { console.log('[financial-advisor] Plugin deactivated'); + clearPluginContext(); }, async onDataExport() { diff --git a/plugins/financial-advisor/src/lib/accounts.ts b/plugins/financial-advisor/src/lib/accounts.ts new file mode 100644 index 0000000..ffa779b --- /dev/null +++ b/plugins/financial-advisor/src/lib/accounts.ts @@ -0,0 +1,35 @@ +/** + * Account types and helpers for the fa-accounts PluresDB collection. + */ + +export type AccountType = 'checking' | 'savings' | 'credit' | 'investment'; + +export interface Account { + id: string; + name: string; + institution: string; + type: AccountType; + balance: number; + createdAt: string; + updatedAt: string; +} + +export const FA_ACCOUNTS_COLLECTION = 'fa-accounts'; + +export function generateAccountId(): string { + return `acct-${crypto.randomUUID()}`; +} + +export const ACCOUNT_TYPE_LABELS: Record = { + checking: 'Checking', + savings: 'Savings', + credit: 'Credit Card', + investment: 'Investment', +}; + +export const ACCOUNT_TYPE_ICONS: Record = { + checking: '🏦', + savings: 'πŸ’°', + credit: 'πŸ’³', + investment: 'πŸ“ˆ', +}; diff --git a/plugins/financial-advisor/src/lib/context.ts b/plugins/financial-advisor/src/lib/context.ts new file mode 100644 index 0000000..35a8895 --- /dev/null +++ b/plugins/financial-advisor/src/lib/context.ts @@ -0,0 +1,24 @@ +/** + * Plugin context singleton for financial-advisor. + * + * `setPluginContext` is called from `onActivate` so that Svelte page + * components can retrieve the PluresDB data API, notify API, etc. + * without needing Svelte's own `setContext` / `getContext` mechanism + * (which is scoped to component trees and unavailable in lifecycle hooks). + */ + +import type { PluginContext } from '@plures/pares-radix'; + +let _context: PluginContext | null = null; + +export function setPluginContext(ctx: PluginContext): void { + _context = ctx; +} + +export function clearPluginContext(): void { + _context = null; +} + +export function getPluginContext(): PluginContext | null { + return _context; +} diff --git a/plugins/financial-advisor/src/pages/Accounts.svelte b/plugins/financial-advisor/src/pages/Accounts.svelte new file mode 100644 index 0000000..b7e71f0 --- /dev/null +++ b/plugins/financial-advisor/src/pages/Accounts.svelte @@ -0,0 +1,787 @@ + + + +
+ + + + + {#if loading} +
+ {#each [1, 2, 3] as _} +
+ {/each} +
+ + + {:else if accounts.length === 0} +
+ +

No accounts yet

+

+ Add your first bank account, credit card, or investment account to + start tracking your finances. +

+ +
+ + + {:else} + + {/if} +
+ + +{#if showForm} + +
+

+ {isEditMode ? 'Edit Account' : 'Add Account'} +

+ +
+ +
{ + e.preventDefault(); + saveAccount().catch(() => { + formError = 'Failed to save account. Please try again.'; + }); + }} + novalidate + > + {#if formError} + + {/if} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + +
+
+
+{/if} + + +{#if confirmDeleteId !== null} + (confirmDeleteId = null) }} + class="dialog dialog--sm" + aria-modal="true" + aria-labelledby="confirm-title" + > +
+

Delete Account?

+
+
+

+ Are you sure you want to delete + {confirmDeleteTarget?.name ?? 'this account'}? + This action cannot be undone. +

+
+
+ + +
+
+{/if} + + diff --git a/types/pares-radix.d.ts b/types/pares-radix.d.ts index 187fe71..1d7e8a2 100644 --- a/types/pares-radix.d.ts +++ b/types/pares-radix.d.ts @@ -103,6 +103,56 @@ export interface InferenceRule { evaluate(input: InferenceInput): InferenceResult | null; } +export interface DataCollection> { + put(id: string, data: T): Promise; + get(id: string): Promise; + query(filter?: Record): Promise; + count(): Promise; + delete(id: string): Promise; +} + +export interface NotifyAPI { + success(message: string): void; + error(message: string): void; + info(message: string): void; + warning(message: string): void; +} + +export interface NavigationAPI { + goto(path: string): void; + setBreadcrumbs(crumbs: Array<{ label: string; href?: string }>): void; +} + +export interface SettingsAPI { + get(key: string): T; + set(key: string, value: unknown): void; + subscribe(key: string, callback: (value: unknown) => void): () => void; +} + +export interface LLMAPI { + available(): boolean; + remainingBudget(): number; + complete(prompt: string, context: Record): Promise; +} + +export interface InferenceAPI { + infer(type: string, record: Record): Promise; + getInferences(sourceId: string): Promise; + confirm(inferenceId: string, accepted: boolean): Promise; + getDecisionChain(inferenceId: string): Promise; +} + +export interface PluginContext { + data: { + collection>(name: string): DataCollection; + }; + notify: NotifyAPI; + navigation: NavigationAPI; + settings: SettingsAPI; + llm: LLMAPI; + inference: InferenceAPI; +} + export interface RadixPlugin { id: string; name: string; @@ -118,7 +168,7 @@ export interface RadixPlugin { rules?: InferenceRule[]; expectations?: Expectation[]; constraints?: unknown[]; - onActivate?: (ctx: unknown) => Promise; + onActivate?: (ctx: PluginContext) => Promise; onDeactivate?: () => Promise; onDataExport?: () => Promise>; onDataImport?: (data: unknown) => Promise;