Commit Graph

5 Commits

Author SHA1 Message Date
033176eb43 Set row depth through ViewConfig instead of restoring it after every rebuild
ViewConfig carries group_by_depth -- alongside the split_by_depth this fork
added -- so row depth can be set declaratively:

    await viewer.restore({ group_by_depth: d })

The config is what the viewer rebuilds its view from, so the depth survives
every rebuild by construction, and viewer.save() carries it into the
persisted and named layouts for free.

It had been imperative: getView() then view.set_depth(), which puts the
depth on an object the viewer discards whenever it re-renders. Everything
that grew around that existed only to guess when a rebuild had happened and
put the depth back -- a shim patching window.IntersectionObserver and
window.ResizeObserver, focus/visibilitychange/pageshow listeners, a retry
loop for getView() throwing "No table set" while getTable() resolved, a flag
tracking whether the viewer had "gone away", and tracing to debug all of it.

That guesswork caused three separate visible faults in a single session: the
tree fully expanding on refocus, snapping on any reflow, and snapping when
Perspective's own settings sidebar was opened. Each fix was a finer
heuristic about which browser event meant what, which is the shape of
fighting a framework rather than using it.

312 lines out, 35 in. applySplitDepth no longer re-applies the row depth
after truncating split_by, because the rebuilt view brings it along.
expand_depth stays readable on load: it is the legacy key, from when the
depth had to be stored beside the config rather than in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 03:05:36 -04:00
7b90b0c07d Re-apply row depth from the observer callback, not from window focus
The WASM engine has no notion of focus or tab visibility. It re-renders
because IntersectionObserver or ResizeObserver told it the element is
visible again at some size, and that re-render discards the view -- taking
set_depth() and per-node expansion with it, since neither lives in
ViewConfig. Listening for window 'focus' was only a guess at when that
happened, which is why the re-apply kept landing while the viewer was still
detached from its table.

Perspective's viewer captures the constructors at module-evaluation time
(`var it=window.ResizeObserver; var st=window.IntersectionObserver`), so
replacing them on window before that import puts us in front of every
callback it gets. observerShim.js does exactly that and nothing else: the
original callback runs first and unchanged, and we re-emit as a CustomEvent
only when an entry's target is or contains a perspective-viewer. That has
to be the first import in main.jsx or there is nothing left to intercept.

focus/visibilitychange/pageshow stay as a backstop for the case where the
shim could not be installed. ResizeObserver fires on every frame of a panel
drag, so observer events trail by 150ms and act once the burst settles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 22:36:29 -04:00
c2e6fc8e77 Put the app behind a login
The server had no authentication: every /api route was open, CORS
allowed any origin, and the identity written to the audit log came from
the request body — the UI sent a hardcoded pf_user: 'admin', which any
client could have set to anything it liked.

Accounts live in pf.app_user with scrypt hashes from node's own crypto,
so there is no native build step and the parameters travel with each
hash. Sessions are express-session over connect-pg-simple in pf.session:
a restart no longer signs everyone out, and a session can be revoked by
deleting its row, which is how disable-user cuts off access immediately
rather than at cookie expiry.

Everything under /api except login/logout/me now requires a session, and
the React app is mounted only once there is one — its load effects call
the API on mount, so a logged-out mount would just fire a burst of 401s.
A session that expires while the app is open lands back on the login
screen: auth.jsx wraps fetch once rather than teaching every call site
to check.

Identity is now read from the session for pf_user, created_by and
closed_by, and the body values are ignored.

Hardened for an internet-facing deployment: trust proxy so req.ip and
secure-cookie detection are right behind TLS termination, httpOnly +
SameSite=Lax + Secure cookies, ten login failures per IP per fifteen
minutes, one error message for unknown, wrong and disabled alike, and a
fresh session id on success. CORS is off entirely unless CORS_ORIGIN
names an origin — a wildcard alongside a session cookie would be CSRF by
construction. The server refuses to boot without SESSION_SECRET rather
than falling back to a guessable default.

pf.sh grows add-user, passwd, list-users, disable-user and enable-user;
passwords are read on stdin and hashed before they reach psql, so no
plaintext in argv or shell history. install.sh generates the secret,
applies 02_auth.sql, and creates the first account.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 22:20:25 -04:00
4a4cb80189 Add light/dark mode with theme toggle 2026-04-25 23:29:25 -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