feat: Transactions page — browse, filter, manual categorize#15
Conversation
Agent-Logs-Url: https://github.com/plures/pares-modulus/sessions/e7741fd9-5509-41b8-8279-e8adf690ef20 Co-authored-by: kayodebristol <3579196+kayodebristol@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a new Transactions page to the financial-advisor plugin, plus supporting transaction-inference data model helpers, to enable browsing, filtering/sorting, confidence display, decision-chain inspection, and manual category overrides.
Changes:
- Introduces
fa-transaction-inferencescollection andTransactionInferencemodel utilities (confidence tiering + deterministic inference IDs). - Adds
Transactions.sveltepage with filter bar, sortable list view, expandable decision-chain panel, and category override modal with optimistic updates.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| plugins/financial-advisor/src/pages/Transactions.svelte | Implements the Transactions UI (load/query, filter+sort derived rows, decision-chain expand, manual categorization modal, and styling). |
| plugins/financial-advisor/src/lib/transactions.ts | Adds inference collection/model definitions and helpers used by the Transactions page. |
| async function toggleExpand(id: string): Promise<void> { | ||
| if (expandedId === id) { | ||
| expandedId = null; | ||
| decisionChain = []; | ||
| return; | ||
| } | ||
| expandedId = id; | ||
| decisionChain = []; | ||
|
|
||
| const inf = inferenceMap.get(id); | ||
| if (!inf || !ctx) return; | ||
|
|
||
| loadingChain = true; | ||
| try { | ||
| decisionChain = (await ctx.inference.getDecisionChain(inf.id)) ?? []; | ||
| } catch { | ||
| decisionChain = [{ note: 'Decision chain unavailable.' }]; | ||
| } finally { | ||
| loadingChain = false; | ||
| } |
There was a problem hiding this comment.
toggleExpand can leave loadingChain stuck true: if the newly-expanded transaction has no inference (!inf) or ctx is missing, the function returns early without resetting loadingChain. If the previous expansion was still loading, the UI will incorrectly show “Loading decision chain…” for rows with no inference. Ensure loadingChain is set to false on all early-return paths (and when collapsing).
|
|
||
| const inf = inferenceMap.get(id); | ||
| if (!inf || !ctx) return; | ||
|
|
||
| loadingChain = true; | ||
| try { | ||
| decisionChain = (await ctx.inference.getDecisionChain(inf.id)) ?? []; | ||
| } catch { | ||
| decisionChain = [{ note: 'Decision chain unavailable.' }]; | ||
| } finally { | ||
| loadingChain = false; |
There was a problem hiding this comment.
toggleExpand is vulnerable to an async race: if a user expands row A, then quickly expands row B, the in-flight getDecisionChain for A may resolve after B and overwrite decisionChain while B is expanded. Capture the requested transaction id locally and only apply the result if expandedId still matches (or cancel/ignore stale requests).
| const inf = inferenceMap.get(id); | |
| if (!inf || !ctx) return; | |
| loadingChain = true; | |
| try { | |
| decisionChain = (await ctx.inference.getDecisionChain(inf.id)) ?? []; | |
| } catch { | |
| decisionChain = [{ note: 'Decision chain unavailable.' }]; | |
| } finally { | |
| loadingChain = false; | |
| const requestedId = id; | |
| const inf = inferenceMap.get(id); | |
| if (!inf || !ctx) return; | |
| loadingChain = true; | |
| try { | |
| const chain = (await ctx.inference.getDecisionChain(inf.id)) ?? []; | |
| if (expandedId === requestedId) { | |
| decisionChain = chain; | |
| } | |
| } catch { | |
| if (expandedId === requestedId) { | |
| decisionChain = [{ note: 'Decision chain unavailable.' }]; | |
| } | |
| } finally { | |
| if (expandedId === requestedId) { | |
| loadingChain = false; | |
| } |
| return new Intl.DateTimeFormat('en-US', { | ||
| year: 'numeric', | ||
| month: 'short', | ||
| day: 'numeric', |
There was a problem hiding this comment.
formatDate constructs a UTC-midnight Date and then formats it in the user's local time zone. In negative-offset time zones this can render the previous calendar day (e.g., 2026-03-31 displaying as Mar 30). Consider formatting with timeZone: 'UTC', or parse the YYYY-MM-DD parts and construct a local date to avoid day-shift.
| day: 'numeric', | |
| day: 'numeric', | |
| timeZone: 'UTC', |
| .tx-table__th[onclick] { | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| .tx-table__th[onclick]:hover { |
There was a problem hiding this comment.
The CSS selector .tx-table__th[onclick] likely won’t match in Svelte because onclick={...} attaches a listener without emitting an onclick HTML attribute. As a result, the intended pointer cursor/hover styles for sortable headers may never apply. Use an explicit class for sortable headers (or target the button.tx-table__th elements directly) instead of [onclick].
| .tx-table__th[onclick] { | |
| cursor: pointer; | |
| } | |
| .tx-table__th[onclick]:hover { | |
| button.tx-table__th { | |
| cursor: pointer; | |
| } | |
| button.tx-table__th:hover { |
| export interface TransactionInference { | ||
| /** `inf-<transactionId>-<field>` — deterministic so that upserts are safe. */ | ||
| id: string; | ||
| transactionId: string; | ||
| /** Typically "category". */ | ||
| field: string; | ||
| value: string; | ||
| /** 0.0 – 1.0. User-confirmed overrides are stored with confidence = 1.0. */ | ||
| confidence: number; | ||
| /** Prose explanation produced by the inference rule. */ | ||
| reasoning: string; | ||
| /** Which inference rule (or "user") produced this result. */ | ||
| sourceId: string; | ||
| /** True when the user explicitly confirmed or overrode this inference. */ | ||
| userConfirmed: boolean; | ||
| /** ISO 8601 timestamp of last write. */ | ||
| updatedAt: string; | ||
| } |
There was a problem hiding this comment.
TransactionInference.value is typed as string, but the plugin’s inference engine contract (InferenceResult.value) is unknown, and the file header mentions storing other inference fields (e.g., tags) that may not be strings. Consider changing value to unknown (or a narrower union) so the storage model matches the inference API and avoids unsafe casts later.
kayodebristol
left a comment
There was a problem hiding this comment.
Auto-approved: CI green + Copilot code review complete.
|
@copilot apply changes based on the comments in this thread |
Agent-Logs-Url: https://github.com/plures/pares-modulus/sessions/18493b0a-5eed-42ad-9398-dec51d8a8611 Co-authored-by: kayodebristol <3579196+kayodebristol@users.noreply.github.com>
Applied all review comments in commit
|
Implements the Transactions page for the
financial-advisorplugin with full browsing, filtering, sorting, confidence visualization, decision chain inspection, and manual categorization.Data model (
lib/transactions.ts)FA_INFERENCES_COLLECTION— newfa-transaction-inferencesPluresDB collection nameTransactionInferenceinterface — per-transaction inference record withfield,value,confidence,reasoning,sourceId,userConfirmedconfidenceLevel(score)→'high' | 'medium' | 'low'tier helpergenerateInferenceId(transactionId, field)— deterministic ID enabling safe upsertsTransactions.svelte
aria-sortattributesctx.inference.getDecisionChain()and renders the step chain alongside inference metadata (reasoning, source, confirmed flag)fa-transaction-inferenceswithconfidence: 1.0anduserConfirmed: true; updates local state optimisticallyFiltering and sorting use a single
$derivedcomputed array so Svelte memoizes the result; the template referencesfilteredRowsdirectly rather than calling it as a function.