Commit Graph

42 Commits

Author SHA1 Message Date
4fde752b54 Default forecast pivot to value column with pf_iter rows
New forecasts opened the pivot with all dimensions stacked as
group_by and the date column as split_by — wide and slow to read.
Open with just the value column showing and pf_iter as rows so the
first thing you see is iteration totals.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:08:43 -04:00
408cb06150 Unify Baseline segment view/edit with filter groups and SQL override
The segment form is now one component rendered in either 'view' or
'edit' mode — the expanded segment row in the list and the
add/edit form below share the same layout, view mode just disables
the inputs. Edit and View are visually identical so toggling between
them feels like enabling fields, not switching tools.

Filters become groups (conditions AND-ed inside, groups OR-ed
between) with + AND condition and + Add OR group affordances. The
compiled WHERE renders live below the groups so you can see what's
being built. A "Switch to manual SQL" toggle flips to a textarea
seeded with the compiled clause; backend baseline POST/PUT and
reference POST accept raw_where alongside filters and store whichever
arrived in pf.log.params for round-tripping.

The Add form is hidden until you click "+ Add segment" at the
bottom of the segments table; Edit also opens it. Cancel/Close
returns the table to its compact state.

/versions/:id/log now also returns value_total, units_total, and the
column names so the segments table can show row count and value sum
inline (header uses the source's actual value column name).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 01:50:52 -04:00
74ff97400b Track todo with implementation notes
Annotates each item with the design choice or open question. Notes
where existing spec coverage already addresses items 3 and 4
(structured filter groups and raw_where escape hatch) and where the
remaining work is wiring vs. greenfield.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 01:31:52 -04:00
3a6062d723 Edit baseline/reference segments before forecast rollout
Adds PUT /versions/:id/baseline/:logid that, in one transaction, drops
the segment's rows and log entry and replays the baseline or reference
SQL with new params. The endpoint refuses (409) if any scale, recode,
or clone has been applied — those operations were calibrated against
the old totals and would silently misreconcile.

Baseline view gets an Edit button on each segment (hidden once
forecast operations exist), populating the form with the original
filters, offset, and note. Submit issues PUT in edit mode, POST
otherwise. POST baseline and POST reference now also persist the
structured filters in pf.log.params so edit can reload them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 01:31:49 -04:00
6a98c3f8fc Persist Perspective viewer state including column formatting
Save/restore went through both viewer and plugin, where the explicit
plugin.restore could stomp the column formatting the viewer had
already applied. Capture via viewer.save() alone (it includes
plugin_config) and restore via a single viewer.restore call with
edit_mode merged in. Added a perspective-config-update listener so
formatting, sort, and other in-place changes persist to the last-used
cache without an explicit Save.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 01:01:21 -04:00
a5e59f823a Workbench: per-iteration totals and price-driven scaling
The slice panel was a single muted line; now it shows a breakdown
table — value, units, and derived price for baseline / scale / recode /
clone, with a bold total row when more than one iteration applies.
Numbers use full text contrast so the current state is legible at a
glance during adjustments. Scale gains a price input that holds units
constant and translates to a value-target call (target value =
new_price × current_units).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:44:14 -04:00
15ab5a3a13 Tag operation result rows with pf_note and pf_op
Forecast tables don't carry pf_note; it's joined from pf.log on /data
fetch. RETURNING * from scale/recode/clone INSERTs lacks those fields,
so rows appended via table.update arrived with pf_note null until a
reload re-ran the join. Inject pf_note and pf_op server-side from the
request before responding.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:33:32 -04:00
8492557621 Stabilize Forecast viewer lifecycle
Reuse a single Perspective worker across version switches and delete
the previous table instead of terminating the worker — terminate was
returning a rejecting promise the sync try/catch missed, and each new
worker leaked WASM memory. applyLayout no longer leaks a view per call;
it reads schema directly from the table. An init id guards against
concurrent runs (StrictMode, rapid version switches) clobbering each
other, and a catch on "already exists" recovers via open_table+delete
when a stale table from a previous run is still hosted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:11:32 -04:00
e279a510d8 Lift source/version selection to a functional StatusBar
App.jsx owns sources/sourceId/versions/versionId and persists them
across reloads. StatusBar renders the dropdowns plus a status badge —
Source-only on Setup, Source · Version · status on Baseline/Forecast.
The duplicate in-view selector bars in Forecast and Baseline are gone;
Baseline keeps its version actions (New/Close/Reopen/Delete) inline.
Setup reports source-list mutations up via refreshSources so the bar
stays in sync after register/deregister.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 20:54:55 -04:00
d70d813604 Align dark mode with Perspective's Pro Dark theme
App chrome now uses Pro Dark's neutral grays (#242526 background,
#2a2c2f panels, #4c505b borders) so the surrounding UI sits cleanly
against the viewer instead of clashing with its warmer tone. Status
accents are desaturated to match. Forecast view sets theme="Pro Dark"
or "Pro Light" on the perspective-viewer in sync with the toggle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 20:32:48 -04:00
1a3209cbc2 Document Perspective architecture options
Captures current /data path (with bug history that forced single-batch
encoding), and four candidate redesigns: optimize the existing encoder,
DuckDB-WASM with Parquet, server-side DuckDB virtual server, and the
hybrid read-from-WASM/write-via-deltas variant. Each option weighed
against the forecasting write path, not just initial load. Intended as
a decision record so context survives a lost conversation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 20:21:19 -04:00
6b69b00645 Undo deletes rows in place instead of reloading the forecast
Perspective table is now created with index: 'pf_id'. Delete endpoints
return the pf_ids they removed; the client calls table.remove(pf_ids)
in undoEntry. Avoids the full /data refetch that dominated undo time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 20:21:13 -04:00
bf85f11b5f Expose pf_note/pf_op in forecast data; fix tables list duplicates
/data now joins pf.log to surface note text and operation type as
pf_note/pf_op so users can pivot/bridge by assumption. Joining at
fetch time avoids storing notes per row and keeps edits live.

/api/tables joined pg_class by name only with namespace filtered in
a separate LEFT JOIN, which cross-producted table names that exist in
multiple schemas. Restructured so namespace participates in the join.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:51:45 -04:00
a9ca58a845 Fix forecast data load and add byte-progress UI
pg now returns bigint/numeric as JS numbers so Arrow infers Int/Float64
instead of Dictionary<Utf8>. /data accumulates rows and emits a single
record batch to avoid dictionary REPLACEMENT messages that crash
Perspective's WASM reader. Forecast view streams the response body and
shows received/total bytes while loading. Drops stale public/ static
middleware that was shadowing the React build at /.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:51:39 -04:00
a6e6efd36e Switch /data endpoint to Arrow IPC stream with pg cursor batching
Server streams rows from a pg cursor in 10k-row batches, building Arrow
record batches incrementally and piping them as chunked HTTP response —
Node.js heap stays bounded regardless of dataset size.

Client fetches as arrayBuffer() and loads directly into Perspective worker
(native Arrow path, no JSON deserialization). X-Row-Count header drives
a non-blocking banner for datasets >= 500k rows. validCols now derived
from col_meta rather than from row keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 22:57:47 -04:00
11f5b02fc4 Spec: add OR filter groups, raw_where escape hatch, and Arrow IPC streaming for large datasets
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 22:50:02 -04:00
4a4cb80189 Add light/dark mode with theme toggle 2026-04-25 23:29:25 -04:00
bd5ea1c60e Restore functional changes lost in dark theme revert
Re-applies fetchCurrentTotals dimension-only filter (prevents Perspective errors
from split_by/expression columns in the filter), toolbar three-group reorganization
(Layout | Expand | Data with dividers), always-visible Save as…, msg in toolbar,
resizable panel, change log modal with undo and inline note editing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 21:32:00 -04:00
8f009e468e Fix cleanLayout stripping expression columns (e.g. Year) on restore
Expression columns (bucket, computed) are defined in cfg.expressions and
are valid pivot axes, but weren't in validCols (raw table columns), so
they were filtered out of group_by/split_by on every layout restore.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 21:24:01 -04:00
742d4b4cc4 Revert "Dark theme for Forecast view controls and operation panel"
This reverts commit cda3943515.
2026-04-25 21:23:43 -04:00
cda3943515 Dark theme for Forecast view controls and operation panel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 21:20:40 -04:00
6449fff573 Add change history modal with undo and note editing
- GET /api/versions/:id/log — log entries with row counts via JOIN
- DELETE /api/log/:logid — undo in a transaction (delete fc rows + log entry)
- PATCH /api/log/:logid — update note text
- History button opens a modal: op badge, slice, editable note, row count, Undo per entry
- Undo triggers full Perspective table reload via initViewer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:36:18 -04:00
3bdd7d0028 Fix Forecast pivot row click, buildWhere, and add resizable panel
- Fix perspective-click handler to use event filter triples instead of
  __ROW_PATH__ — Perspective encodes row position as [col,'==',val] in
  detail.config.filter
- buildWhere now skips unrecognised slice keys (e.g. pf_iter) instead of
  throwing, so only dimension columns reach the WHERE clause
- Add draggable resize handle on the operation panel (160–480px)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 20:21:13 -04:00
af52845523 Unify baseline/reference into one form; fix timeline for both types
- Baseline.jsx: merge Reference section into Add Segment form with baseline/reference toggle; segment rows now clickable to expand stored WHERE clause + timeline; date filter inputs use type="date" for date-role columns
- Timeline.jsx: add type prop ('baseline'|'reference'); reference band uses purple; single-band height shrinks to 52px; canvas uses requestAnimationFrame to fix offsetWidth=0 on mount
- operations.js: reference route now accepts where_clause like baseline (drops date_from/date_to)
- sql_generator.js: reference SQL template uses {{filter_clause}} instead of hardcoded BETWEEN

Note: existing sources need Generate SQL re-run to pick up the new reference template.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:45:03 -04:00
dc090fe394 Scaffold React/Vite/Tailwind UI with 3-step Setup → Baseline → Forecast flow
- ui/: React + Vite + Tailwind app (Setup, Baseline, Forecast views, collapsible sidebar, status bar, canvas timeline)
- server.js: serve built UI from public/app/
- package.json: add build script (cd ui && npm run build)
- routes/sources.js: default new col_meta role to 'dimension' instead of 'ignore'
- .gitignore: exclude public/app/ build output
- pf_spec.md: update tech stack, nav, frontend section, and project status to reflect current implementation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:28:45 -04:00
dd993e989c Add UX mockup and update spec with navigation direction
- HTML mockup with collapsible side nav, 3-step flow (Setup/Baseline/Forecast)
- Canvas-based timeline preview in baseline segment form
- Table peek modal, status bar, help popovers
- Spec updated: 3-step mental model, AG Grid replacement note

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 14:56:29 -04:00
9084a87ea5 Update spec: Perspective pivot, current project status
- Replace AG Grid pivot references with Perspective throughout
- Document pivot interaction flow, default layout, slice extraction,
  incremental row streaming
- Add Project Status section: what's working, known UX issues,
  branch status

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 07:24:20 -04:00
4c71049bf0 Fix Perspective viewer: pass table name in restore, simplify listener
- restore() must include table: tableName so viewer knows which table
- Replace cloneNode listener reset with named _pspClickHandler pattern
- Always inject table name into saved layouts on restore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 23:14:22 -04:00
5171b9770c Fix: restore AG Grid script tag removed by mistake
Sources/Versions/Log grids still use AG Grid — only the Forecast
pivot was replaced with Perspective.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 23:13:14 -04:00
368127e098 Replace AG Grid pivot with Perspective viewer in Forecast view
- Swap AG Grid CSS/JS for Perspective CDN imports (4.4.0)
- Replace #pivot-grid div with <perspective-viewer> web component
- Add loadPerspective() singleton, initPerspectiveViewer(data)
- Build default layout from col_meta (dims → group_by, date → split_by)
- Extract slice from perspective-click event.detail.config.filter
  (only == filters on role=dimension columns feed the operation panel)
- table.update() appends operation result rows without full reload
- Save/reset layout buttons per version in localStorage
- Remove expand/collapse buttons (Perspective handles natively)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 23:11:56 -04:00
1df37a5ff1 Refactor sources UI, rename pf_ system cols, replace filter builder with raw SQL
- Sources page: left column with stacked DB tables + registered sources panels,
  right column as full-height column mapping workbench
- Add compact table search, column search, table preview button, delete source button
- Rename fc_table system columns to pf_ prefix (pf_id, pf_iter, pf_logid,
  pf_created_at) to avoid collisions with source table columns like 'id'
- Remove 'filter' col_meta role — any non-ignore column usable in baseline filters
- Replace structured filter row builder with free-form SQL WHERE clause textarea
  and clickable column chips for insertion; fully flexible AND/OR logic
- Baseline segment cards now display raw WHERE clause text + offset

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 00:47:57 -04:00
d49aac70e4 Replace offset year/month spinners with type picklist + value input
Offset Type: [Year|Month|Week|Day]  Offset Value: [n]
Cleaner UX, handles all interval types uniformly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:05:25 -04:00
d4962f4376 Fix baseline workbench layout — stack form above segments
Switches from side-by-side panels to stacked layout so the filter
builder has full width and input fields are no longer cramped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 13:45:25 -04:00
5550a57f97 Add baseline workbench — multi-segment additive baseline with filter builder
- New Baseline nav view replaces the simple Load Baseline modal
- Baseline loads are now additive; each segment is independently undoable
- Filter builder: any date/filter-role column, full operator set
- Timeline preview shows source → projected period bars for date BETWEEN filters
- Clear Baseline action deletes all baseline rows and log entries
- DELETE /api/versions/:id/baseline route
- buildFilterClause() added to sql_generator
- filter role added to col_meta editor
- Reminder: re-run generate-sql for each source after this change

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 13:27:36 -04:00
7e9ea456b6 Fix baseline request body in spec — unified filters array
Removes the redundant date_from/date_to/date_col fields from the request
body. Period selection is now expressed as a filter condition in the
filters array like any other condition. SQL pattern updated to match
(single {{filter_clause}} token instead of date_range + filter split).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 13:15:17 -04:00
f0c51096ff Update spec: generalize baseline segment filters, timeline preview
Period selection (date range, season, etc.) is now expressed as a
filter condition like any other — no separate date range section.
Preview uses a timeline/number-line bar instead of month chips.
Documents the unified filter builder approach.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 13:12:52 -04:00
e9f37e09f2 Add pf_spec.md — application specification
Covers architecture, data model, API routes, SQL patterns, and UI design.
Includes baseline workbench design with multi-segment additive loads,
filter role for col_meta, and date offset for projecting actuals into
the forecast period.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 13:05:39 -04:00
6d8b052eb6 Add date offset to baseline — project actuals into forecast period
The baseline operation now accepts a date_offset interval (e.g. "1 year",
"6 months") and applies it to every date when inserting rows, shifting
historical actuals into the target forecast period.

SQL: {date_col} + '{{date_offset}}'::interval)::date at insert time.
Route: defaults to '0 days' if omitted so existing calls are unaffected.
UI: year/month spinners with a live before→after month chip preview so
the projected landing period is visible before submitting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 12:48:28 -04:00
ddd16bc7a0 Show params column in log grid
Exposes the stored params (e.g. date_from/date_to for baseline/reference)
so the date range used in each operation is visible in the log.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 12:28:03 -04:00
10441a4761 Show baseline/reference form in a modal with live month preview
Replaces the small inline form with a centred modal dialog. When both
dates are selected, a live chip list shows every month covered (up to
36 months) so it is immediately clear what periods will be loaded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 12:24:20 -04:00
cfee3e96b9 Return inserted rows from change operations for incremental grid updates
Instead of re-fetching all forecast data after scale/recode/clone/reference,
the routes now return the inserted rows directly. The frontend uses ag-Grid's
applyTransaction to add only the new rows, eliminating the full reload round-trip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 12:04:28 -04:00
08dc415bfd Initial commit — pivot forecast application
Node.js/Express + PostgreSQL forecasting app with AG Grid Enterprise pivot UI.
Supports baseline, scale, recode, clone operations on configurable source tables.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 07:59:05 -04:00