Document the nav, the admin gating, and what left the toolbar
Three sections were describing an app that no longer exists: the project layout still called Sidebar a 3-step nav and StatusBar a source and version picker, and the toolbar section still listed Fit widths. Adds "Choosing a forecast" (the sidebar list, why it is not nested under the Forecast step, why the source is absent and inferred, and the forecast/step naming collision that is still undecided) and "Setup and Baseline are admin-only to write" (which routes carry requireAdmin, which deliberately do not and why, the fieldset approach, and the note that a 403 used to blank the app because an error body reached .map during render). The toolbar section gains why Fit widths was removed rather than fixed, and how the Calculating indicator is driven -- including that perspective-config-update is the obvious hook and reports the end of the work rather than the start. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3426f3525b
commit
7d09ab2245
122
CLAUDE.md
122
CLAUDE.md
@ -51,8 +51,9 @@ ui/src/
|
||||
LayoutMenu.jsx The Layout ▾ control — Published / Mine, with the write actions
|
||||
OperationPanel.jsx The adjustment workbench — ledger + scale/recode/clone forms
|
||||
BridgeView.jsx Baseline → current waterfall by tag (exports buildSteps/layoutSteps)
|
||||
Sidebar.jsx 3-step collapsible nav
|
||||
StatusBar.jsx Source · version · write target · row counts · theme
|
||||
Sidebar.jsx Forecast list + the 3 steps; collapsible
|
||||
StatusBar.jsx Forecast name · row count · calculating · theme; source on Setup only
|
||||
ReadOnlyNotice.jsx The banner over a page a non-admin may read but not change
|
||||
Timeline.jsx Date-range preview bar for baseline segments
|
||||
```
|
||||
|
||||
@ -429,6 +430,38 @@ and `last_used` still come back from the route; nothing reads them.
|
||||
|
||||
---
|
||||
|
||||
## Choosing a forecast
|
||||
|
||||
The forecasts are listed in the **sidebar**, above the three steps, each row
|
||||
carrying its status and a star for the default. They were two dropdowns in the
|
||||
status bar, which is otherwise read-only — so the one pair of controls in it did
|
||||
not read as controls at all.
|
||||
|
||||
**Not nested under the `Forecast` step**, which is where the idea started:
|
||||
Baseline works on the same version, so hanging the list off one step would
|
||||
either misrepresent the scope or duplicate it under two. It reads *what am I
|
||||
working on*, then *what step am I on*.
|
||||
|
||||
**The source is not in the nav.** `gs.osm_skinny` means nothing to someone
|
||||
editing a forecast. The sidebar lists every source's versions and sets
|
||||
`sourceId` from the one picked, since `pf.version.source_id` comes with it — so
|
||||
choosing a forecast settles the source, and the picker is only offered on Setup,
|
||||
the one page where the raw table name is the right label.
|
||||
|
||||
`versions` stays scoped to the selected source, because Baseline and Forecast
|
||||
are; the sidebar gets its own `allVersions` instead, so widening it does not
|
||||
change what `versions` means everywhere else. It is a client-side fan-out over
|
||||
`/sources/:id/versions` rather than a `GET /versions` — one request, at one
|
||||
registered source.
|
||||
|
||||
The status bar keeps what follows *from* the selection: the forecast's name and
|
||||
its row count. The table written to is in the hover panel, still click-to-copy,
|
||||
because pasting it into psql is the reason it was ever on screen.
|
||||
|
||||
**Known collision:** "forecast" is both the scenario and the third step. The
|
||||
scenario should own the word — it is what people name and say aloud — which
|
||||
leaves the step wanting a different one (`Adjust`). Undecided.
|
||||
|
||||
## Where a browser lands
|
||||
|
||||
`App.jsx` restores `pf_sourceId` / `pf_versionId` from `localStorage` and
|
||||
@ -447,7 +480,7 @@ forecasting work around.
|
||||
else lands, so it follows the `pf.log` rule rather than being the version's own
|
||||
setting like its name. The checkbox on the Baseline page's **This forecast**
|
||||
panel is greyed out for other accounts rather than offering a click that
|
||||
answers 403. The `StatusBar` picker marks the default `(default)`.
|
||||
answers 403. The sidebar's forecast list marks the default with a star.
|
||||
|
||||
**A stored selection still wins.** The default is the fallback, not an
|
||||
override — someone who has been working in another version stays there. The
|
||||
@ -569,17 +602,48 @@ layout, since `restore()` is all-or-nothing.
|
||||
|
||||
## The Forecast toolbar is two zones
|
||||
|
||||
Left, on its own tinted ground: **view** — `Layout ▾`, the `Rows` and `Columns`
|
||||
depth buttons, `Fit widths`. Nothing there reaches the server or changes the
|
||||
forecast; it is all `ViewConfig` and `plugin_config` against rows already loaded.
|
||||
Left, on its own tinted ground: **view** — `Layout ▾` and the `Rows` and
|
||||
`Columns` depth buttons. Nothing there reaches the server or changes the
|
||||
forecast; it is all `ViewConfig` against rows already loaded.
|
||||
|
||||
Right: **forecast** — `Refresh data`, `Change log`, `Bridge`, `Operations`. These
|
||||
re-run the load, open something over the pivot, or write to the version.
|
||||
Right, in a bordered panel of its own: **forecast** — `Refresh data`,
|
||||
`Change log`, `Bridge`, `Operations`. These re-run the load, open something over
|
||||
the pivot, or write to the version.
|
||||
|
||||
They were one undifferentiated row separated by identical hairlines, which made
|
||||
`Fit` and `Bridge` read as peers when one is a column width and the other is the
|
||||
version's history. The split is the point; the tint is just what makes it
|
||||
visible.
|
||||
version's history. Three weights do the separating now, and they were all once
|
||||
the same: `.pf-section-label` names a group (`Layout`, `Rows`, `Columns`) and is
|
||||
heavier and darker than the controls under it, `.pf-group-rule` separates groups
|
||||
*within* a zone, and the zone borders separate the zones. A label that matched
|
||||
its buttons, and a group rule that matched a zone border, is what made the row
|
||||
read as one undifferentiated thing.
|
||||
|
||||
**`Fit widths` is gone**, button and `fitColumns` both, for three independent
|
||||
reasons. Auto-size measures the leaf header over the rows *currently in the
|
||||
viewport*, so anything below the fold never counted; `draw({invalid_columns})`
|
||||
passes an option `DrawOptions` does not have, and `draw()` is throttled, so
|
||||
clicking during a pending draw cleared the widths without re-measuring and the
|
||||
columns fell to their 52/60px floors; and the `persistLayout` after it saved
|
||||
nothing, since `saveColumnSizes()` returns only overrides, which `resetAutoSize()`
|
||||
had just emptied — and this fork discards `column_size_override` whenever
|
||||
`split_by` is non-empty, which here is always. Recoverable from git if it is ever
|
||||
worth fixing rather than dropping.
|
||||
|
||||
**A `Calculating…` indicator** sits in the app's status bar while the pivot
|
||||
rebuilds. Perspective has its own beaker, but it is rendered only while the
|
||||
settings panel is open, so it vanishes exactly when nothing else tells you to
|
||||
wait. `Forecast.jsx` raises a counter — a counter, because rebuilds overlap —
|
||||
in `applyDepth`, `applySplitDepth` and `applyLayout`, lowers it after
|
||||
`viewer.flush()`, and broadcasts `pf-pivot-busy` on the window for `StatusBar`
|
||||
to render, the same way `pf-data-changed` already travels between them.
|
||||
|
||||
Deliberately **not** driven from `perspective-config-update`, which is the
|
||||
obvious hook and is the wrong one: measured on a depth change over a large
|
||||
grain, the event arrives *after* the rebuild — seconds late — and the flush that
|
||||
follows resolves in 6ms. It reports the end of the work. The cost is that
|
||||
sorting or filtering from the viewer's own chrome shows nothing, because the app
|
||||
is never told those began.
|
||||
|
||||
---
|
||||
|
||||
@ -714,6 +778,44 @@ effects call the API immediately.
|
||||
`pf_user: 'admin'`, which any client could have set to anything. The audit log
|
||||
now names the account that made the change.
|
||||
|
||||
## Setup and Baseline are admin-only to write
|
||||
|
||||
`requireAdmin` sat in `lib/auth.js`, exported, and applied to **nothing** — so
|
||||
every non-admin account (26 of the 29) could call every Setup and Baseline
|
||||
endpoint directly. It is now on the writes those two pages lead to:
|
||||
|
||||
- **sources.js** — `POST /sources`, `PUT /sources/:id/cols`, `POST
|
||||
/sources/:id/generate-sql`, `POST /sources/:id/dim/:group/refresh`,
|
||||
`DELETE /sources/:id`
|
||||
- **versions.js** — `POST /sources/:id/versions`, `PUT /versions/:id`,
|
||||
`POST /versions/:id/{close,reopen}`, `DELETE /versions/:id`
|
||||
- **operations.js** — the loads: `POST /versions/:id/{baseline,reference}`,
|
||||
`PUT /versions/:id/baseline/:logid`, `DELETE /versions/:id/baseline`
|
||||
- **plans.js** — every write
|
||||
|
||||
**Not gated, and each for a reason.** No read is, including the table browser —
|
||||
the pages stay visible, and gating what a page needs in order to *render* turns
|
||||
it into a 403 (see below). The forecast operations are not, because scale,
|
||||
recode, clone and undo carry their own owner and territory rules. Nor is
|
||||
`PATCH /versions/:id/tags`: its permission is ownership, and a rep may rename
|
||||
the initiatives they named themselves.
|
||||
|
||||
**The pages stay visible and go read-only.** One `<fieldset disabled>` per page
|
||||
rather than a flag threaded through every control — Baseline alone has
|
||||
forty-five, and the one that got missed would be the one that mattered — plus a
|
||||
`ReadOnlyNotice` banner saying why. Setup wraps a real box because that fieldset
|
||||
*is* its flex row; Baseline uses `display: contents` so the page's layout is
|
||||
untouched, and `disabled` applies either way, being a DOM property. Baseline's
|
||||
Tags panel sits outside the fieldset, for the ownership reason above. **None of
|
||||
this is the permission** — it only stops an account being offered a button that
|
||||
would answer 403.
|
||||
|
||||
**A 403 used to blank the whole app.** Setup fed responses straight into state
|
||||
and rendered them with `.map`; an error body is `{error}`, and throwing during
|
||||
render unmounts React. Its list fetches now coerce to an array, so an unexpected
|
||||
response costs an empty panel rather than the session. That fragility predated
|
||||
the gating — the gating only made it reachable.
|
||||
|
||||
## Source columns come from pg_catalog
|
||||
|
||||
`information_schema.columns` omits **materialized views** — they are not in the
|
||||
|
||||
Loading…
Reference in New Issue
Block a user