/schema: fix UI issues and adopt play.html-style auth form#108724
Conversation
User-reported fixes for the `/schema` web UI: - Load the schema when pressing Enter in any connection field, not only when clicking `Load`. - Theme-aware scrollbars: set `color-scheme` per theme and style the custom scroll areas (viewport, node column lists, sidebar) with the theme colours. - Readable node headers: show just the table name (the qualified name stays on hover and in the sidebar), and stop a long engine label (e.g. `ReplicatedReplacingMergeTree`) from squeezing the name out of the header. - Consistent grouping: every database section now holds all of its tables — dependency chains laid out as graphs on top, independent tables in a grid below — instead of hoisting independent tables into one global bucket. - Add `user-select: none` on nodes to prevent parasitic text selection while dragging tables. Make the authentication form behave like `play.html`: - Credential Management API: remember entered credentials via `PasswordCredential` and `navigator.credentials.store` so the browser can offer to save and autofill them (guarded for browsers without support). - Read `url`, `user`, and `password` from the query string, and strip the password back out of the address bar via `history.replaceState`. - Live credential validation: paint the user/password fields green on a successful `SELECT version()` probe and red on failure; reset on input and re-check on paste. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address AI review: accepting `?url` from the query string together with the same-origin auto-load path let `/schema?url=https://attacker.example` fire `loadAll` against an arbitrary origin on page load. If the browser's password manager had filled credentials for the trusted page origin, `chQuery` would forward them as `user`/`password` to the attacker-chosen server (which controls CORS). Gate auto-load on the target being same-origin via `isSameOriginTarget`; cross-origin targets now require an explicit `Load` click. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the security finding in 4c4380e. The auto-load on page load is now gated on the target being same-origin ( Added focused harness coverage for this path (a cross-origin |
Address AI review (Request changes) on PR #108724: the live credential validation could mark the current `url` / `user` / `password` fields `ok` or `fail` based on a superseded probe. A paste schedules a `SELECT version()` probe. `checkCredentials` only suppressed an older probe once a *newer* probe started (`checkCredentialsNum`); `resetCredentialsStatus` (on `input`) just cleared the classes without invalidating the pending request. So a probe that returned after the user edited the credentials, or after `loadAll` ran with different values, repainted the now-stale result onto the current fields. Snapshot the `url` / `user` / `password` the probe is testing and require them (and the probe sequence number) to still be current before calling `setCredentialStatus`. `chQuery` reads those fields synchronously when it builds the request, so the snapshot is exactly what the probe queried. Verified with a DOM-stub harness reproducing the race: a stale ok/fail probe arriving after a field edit no longer paints, while a matching probe and the newest of two concurrent probes still paint as before.
|
Addressed the AI review (Request changes) credential-probe race in
Also merged The two red checks are pre-existing flakes unrelated to this HTML-only change:
|
…All` Follow-up to the credential-probe race fix: the live credential status and the Credential Management store must describe the values that were actually probed. Two paths still leaked a stale outcome: - Only `#user` / `#password` edits cleared the status, so changing `#url` after a green probe left the credential fields green for an unvalidated target. The `input` listener now also covers `#url`. - `loadAll` called `setCredentialStatus` / `storeCredentials` after its awaited request without re-checking the fields. It now snapshots the credentials it used (via the shared `credentialSnapshot`) and only paints or stores while the fields still hold those exact values; the failure path is guarded the same way. The snapshot logic is factored into `credentialSnapshot`, shared by `checkCredentials` and `loadAll`. `chQuery` reads the fields synchronously when it builds the request, so the snapshot is exactly what was queried. Verified with a DOM-stub harness exercising the extracted functions: a URL edit clears a green probe; a stale `loadAll` (target changed mid-load) neither paints nor stores; a matching `loadAll` paints and stores; and the existing stale-probe / newer-probe-supersedes invariants still hold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review (Request changes) follow-up on the credential-status invariant in The remaining two stale-status paths are now closed:
This makes every credential-status update and credential store conditional on the current fields matching the values that were actually probed or loaded — the verdict's minimum required action. Verified with a DOM-stub harness over the extracted functions: a URL edit clears a green probe; a Also merged The one red check, |
The `paste` listener installed on the `url`/`user`/`password` fields
scheduled `checkCredentials` unconditionally, and `checkCredentials`
calls `chQuery`, which reads `$('url').value` as the host and forwards
`user`/`password` to it. So on a trusted `/schema` page whose credentials
were filled in by the browser's password manager, pasting a cross-origin
URL such as `https://attacker.example` into the `url` field issued a
`SELECT version()` POST with those credentials to the attacker-chosen
server before any explicit `Load` — reopening the cross-origin credential
leak through the paste path and bypassing the `isSameOriginTarget` gate
added for page-load auto-load.
Gate the paste probe with `isSameOriginTarget($('url').value)`, mirroring
the page-load auto-load invariant: a cross-origin target is now contacted
only after an explicit `Load` (or `Enter`) click, while same-origin
targets keep live credential validation on paste.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review (Block) cross-origin credential-leak finding on the URL paste path in The The paste probe is now gated on The one red check, |
The AI review flagged a remaining cross-origin credential leak in `loadAll`: `chQuery` re-read the live `url`/`user`/`password` fields on every call, so a URL pasted into the form between two of the batch's awaited queries — `columnsQ`, `dictsQ`, `refreshesQ`, or the `loadViewsLoad` query — would POST the password-manager-filled credentials to the newly pasted, possibly cross-origin, server without an explicit `Load`. The earlier `isSameOriginTarget` gates closed the page-load and paste paths, but an already-running same-origin load batch was still steerable mid-flight. `loadAll` now captures the connection once via `connectionSnapshot` and passes it to every `chQuery` in the batch, so the whole load targets the server the user committed to; a mid-load field change can no longer redirect later queries — or their credentials — to a different origin. `loadViewsLoad` reuses that captured connection (and captures its own when invoked interactively from the lookback selector). `chQuery` falls back to the live fields only for the single-shot credential probe. The `isCurrent` predicate still gates the credential-status paint/store so a stale result cannot repaint the form. Verified with a DOM-stub harness over the page's own `loadAll`/`chQuery`: a same-origin load whose `url` is changed to a cross-origin target after the first query issues zero requests to the new origin (the pre-fix code leaked the three remaining batch queries there), and no stale credential status is painted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review (Block) in-flight cross-origin credential-leak finding on the The earlier commits gated the page-load auto-load (
This is the verdict's minimum required action: every query in one Verified with a DOM-stub harness over the page's own |
`connectionSnapshot` bound each load batch's requests — and its credential status — to one captured connection, but `loadAll` still applied its results unconditionally. A same-origin auto-load could start, the user could edit `url` / credentials and press `Load`, and if the newer load rendered first the older batch's awaited queries could still return and overwrite `state.tables`, the status text, the `query_views_log` heat-map, and the rendered graph with data from the superseded connection — leaving a schema that no longer matched the current connection fields. Add a monotonic `loadGeneration`: each load captures the generation it started in and applies its results (`state`, status, heat-map, render) only while it is still the latest one, so a superseded batch quietly drops its stale result. `loadViewsLoad` reuses the caller's generation when invoked from `loadAll`, and reads the current one (without bumping it) for a standalone lookback reload, so a standalone heat-map reload never cancels an in-flight `loadAll` while a newer `loadAll` still supersedes it. Verified with a DOM-stub harness over the page's own `loadAll`/`loadViewsLoad`/`chQuery`: two overlapping loads where the older connection becomes stale before it applies — the older load updates neither `state` nor the graph after the newer load wins, a load superseded during its heat-map query does not overwrite the newer heat-map, and a single un-raced load still applies and renders normally. (As with the other `programs/server/*.html` web UIs, there is no committed CI runner for these DOM-stub checks, so the harness is run locally.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review (Request changes) follow-up on the async load race in The earlier The fix takes the verdict's monotonic-load-id option. A new Verified with a DOM-stub harness over the page's own The three red checks are pre-existing flakes unrelated to this single-file HTML change (
The fresh push re-runs CI. |
Cosmetic-only follow-up to the previous commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ction fields change `setCredentialStatus(true, version)` writes a `Connected to ClickHouse v...` message to the shared `#status` line, but `resetCredentialsStatus` only removed the `ok`/`fail` classes from the `user`/`password` fields. After a successful paste probe, editing `url`, `user`, or `password` cleared the field border color while the page still claimed to be connected to the previous server/version — the credential-status invariant that live status must describe the current fields was violated. `#status` is shared with the load status (`Loading tables...`, `Loaded N tables`, `Failed: ...`), so the message cannot be wiped unconditionally. Track whether the line currently holds a credential-probe message via `statusIsCredentialProbe`: `setStatus` clears the flag (any load status takes over the line), `setCredentialStatus` re-claims it after writing the `Connected` text, and `resetCredentialsStatus` clears the text only while the flag is set. Editing a field now drops a stale `Connected` message but leaves a load status untouched. Addresses the AI review (Request changes) finding on programs/server/schema.html:737. Verified with a DOM-stub harness over the extracted functions: a field edit after a successful probe clears the `Connected` text and the field colors; a `Loaded N tables`, `Loading...`, or `Failed:` status survives a field edit; a probe superseded by a load loses ownership so the load status also survives; a failed probe does not falsely claim ownership. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses the AI review (Request changes) follow-up on the async heat-map race at `programs/server/schema.html:1055`. `loadGeneration` (`eb1f10f4d3b`) guards a whole `loadAll` batch, but the `system.query_views_log` heat-map is also reloaded standalone whenever the lookback selector changes, and that path reused the current `loadGeneration` without a token of its own. Two heat-map reloads therefore shared a generation and both passed `isLatestLoad`, so a stale one could apply after the graph or lookback changed: - A standalone reload captures `quotedToKey` from the current `state.nodes`, then a same-generation `loadAll` rebuilds `state.nodes` and runs its own heat-map; when the older selector query returns, `isLatestLoad` still passed and it wrote keys from the superseded graph onto the new one. - Two quick lookback changes had the same stale-apply problem because neither call got a unique sequence — the older query returning last corrupted the newer heat-map. Add a monotonic `viewsLoadGeneration` bumped by every `loadViewsLoad` (both the `loadAll`-driven and standalone ones). A reload is the latest only while both its `loadGeneration` and its `viewsLoadGeneration` are still current, so a stale batch neither resets nor applies `state.loadByMv` / `loadByEdge` / `loadMax`. Since `state.nodes` only ever changes via a `loadAll` that also bumps the heat-map token, "the graph changed under me" is exactly "the token moved", so the token detects both races. The map reset is moved behind the guard so a superseded reload leaves the current heat-map intact instead of blanking it (the `query_views_log`-unavailable and disabled-heat-map paths still clear it, gated the same way). Verified with a DOM-stub harness over the page's own extracted `loadViewsLoad`: a standalone reload superseded by a same-generation `loadAll` graph swap no longer leaks the old-graph key (only the new node's heat-map remains), and two quick lookback changes keep the newest result while the older query is dropped. The same harness against the pre-fix code does leak the stale key and corrupt the newer heat-map, so the guard is meaningful. As with the other `programs/server/*.html` web UIs there is no committed CI runner for these DOM-stub checks, so the harness is run locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review (Request changes) follow-up on the async heat-map race (
The fix adds a monotonic Verified with a DOM-stub harness over the page's own extracted The one red on the previous head, |
|
@alexey-milovidov the Root cause: Deterministic repro (any build):
#109019 bounds You're right that this PR (HTML-only) cannot cause it — the fix belongs in #109019, not here. CIDB: STID 3243-1df9 = 1 hit / 1 PR (this one) / 0 master in 60d. |
|
The new The flagged test is
It is already being fixed by an open PR that allowlists exactly these lines in |
|
The
Neither a distributed-cache client TTL test nor a refreshable-MV attach test can be affected by a change to |
|
Re-merged `master` ( The only failing check was which cover all three |
…probes and loads The credential-status paint (the green/red on the `user`/`password` fields and the `Connected to ...` line) was keyed only by field equality: `checkCredentials` used a probe-only `checkCredentialsNum`, and `loadAll` gated its paint/store on `conn.isCurrent()` alone. Two operations against the *same* `url`/`user`/`password` both satisfy field equality, so an older callback could repaint the status after a newer one had already won. Reproduced (DOM-stub harness over the page's own functions): start two `loadAll` batches for the same connection, let the newer one succeed, then let the older one fail — the page keeps `Loaded 3 tables.` while the stale callback flips the credential fields to `fail`. The same happens with a paste-triggered probe racing a newer load. Replace `checkCredentialsNum` with a single monotonic `credentialStatusGeneration` bumped at the start of every operation that can repaint the credential status — the `checkCredentials` probe and each `loadAll` batch. Each captures the token and applies its paint/store only while it is still the latest (and the fields still hold the probed values via `conn.isCurrent()`), so the latest-started probe-or-load is the sole authority over the credential status. A superseded operation, even one on an identical connection, no longer repaints a stale `ok`/`fail`. Verified with a DOM-stub harness reproducing the flagged load-vs-load and probe-vs-load races (both now suppress the stale paint) while the pre-existing invariants still hold: a single un-raced load still paints and reports, a field edit before a probe returns still suppresses the paint, and a newer probe still supersedes an older one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review (Request changes) credential-status freshness finding ( The remaining stale-status path is now closed. The credential status (the green/red on the The fix takes the verdict's shared-token option: Verified with a DOM-stub harness over the page's own The two red checks are the known AST-fuzzer flake, unrelated to this single-file HTML change (
The fresh push re-runs CI. |
…ection The AI review (Request changes) flagged the last open path that violates the `play.html`-style explicit-`Load` contract this PR establishes (`programs/server/schema.html:1074`). `loadViewsLoad` reloads the `system.query_views_log` heat-map both at the end of every `loadAll` (with the batch's captured connection) and standalone whenever the lookback selector changes. The standalone path took `conn = conn || connectionSnapshot()`, so with `conn` omitted it read the *live* form fields. After a successful load a user could edit `url` to a cross-origin target and then merely change the lookback — without any explicit `Load` — and `chQuery(q, conn)` would POST `user` / `password` to that target. Even for a benign different server it overlays that server's `query_views_log` onto the graph still belonging to the previous load. Fix: record the connection that produced the current `state.nodes` in `loadedConnection` (set by `loadAll` the moment it commits the batch as the latest load), and bind a standalone `loadViewsLoad` to it instead of the live fields. A lookback change now re-queries only the server that produced the displayed graph; contacting a newly-edited target still requires an explicit `Load` (which runs `loadAll` and re-commits both `state.nodes` and `loadedConnection`). Fail closed — contact nothing — if no load has committed a connection yet. Verified with a DOM-stub harness over the page's own extracted `loadViewsLoad` / `connectionSnapshot` (the test the verdict asks for): after a same-origin load, editing `url` to an attacker origin and changing the lookback no longer contacts that origin — the reload queries the committed connection and keys the heat-map to the displayed graph; the `loadAll`-driven path (explicit `conn`) is unchanged; and a standalone reload with no committed connection contacts nothing. The same harness against the pre-fix code does POST to the attacker origin, so the check is meaningful. As with the other `programs/server/*.html` web UIs there is no committed CI runner for these DOM-stub checks, so the harness is run locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review (Request changes) blocker on the standalone heat-map reload (
The fix takes the verdict's "bind to the last successfully loaded connection" option. A new Verified with a DOM-stub harness over the page's own extracted The fresh push re-runs CI. |
The AI review (Request changes) flagged that `storeCredentials` persisted the entered credentials via `navigator.credentials.store` for **any** target, including a cross-origin `#url` (`programs/server/schema.html:718`). `PasswordCredential` is scoped to the page origin, not to the backend named in `#url` — its `name` field is a display label, not an isolation key. So an explicit cross-origin load stored those credentials under `/schema`'s own origin. On a later plain visit (no `?url=`), `#url` defaults back to `location.origin` and the page auto-loads it; the browser can autofill the remembered cross-origin credentials into the fields, so that same-origin auto-load would POST them to this page's own server instead of the cross-origin one they belong to. `play.html` can store unconditionally because it never auto-contacts a server on open; `/schema` does, so it must not persist a credential that its own auto-load could later misdirect. The fix gates `storeCredentials` on the existing `isSameOriginTarget` invariant that already guards the page-load auto-load and the paste probe, so only same-origin credentials are ever remembered. Verified with a DOM-stub harness over the page's own `storeCredentials` / `isSameOriginTarget`: a same-origin login is stored, a cross-origin login (both `https://other` and protocol-relative `//other`) is not, an unparseable `#url` fails closed, and empty-password / no-`PasswordCredential` cases store nothing; the same harness against the pre-fix code does store the cross-origin credentials, so the check is meaningful. As with the other `programs/server/*.html` web UIs there is no committed CI runner for these DOM-stub checks, so the harness is run locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review (Request changes) credential-scoping blocker (
The fix takes the verdict's first option: Verified with a DOM-stub harness over the page's own The fresh push re-runs CI. |
…in auto-load The AI review (Request changes) flagged that `/schema` only implements the `store()` half of the `play.html`-style remembered-auth flow (`programs/server/schema.html:726`). Unlike `play.html`, the connection inputs were bare `<input>`s outside a `<form>` and without `name=` attributes, and the page never requested the stored credential before the same-origin auto-load at the bottom of the script. So the first `loadAll` snapshotted whatever happened to be in `#user` / `#password` at script time; a remembered login could be missing when that automatic request fired, leaving `Failed: ...` onscreen even though a valid saved credential existed. This adopts `play.html`'s form semantics and adds the missing retrieval step: - Wrap the connection inputs in a `<form>` with `name="url"` / `name="user"` / `name="password"` (as in `play.html`) so the browser offers to save and later autofill the credentials, and handle the form `submit` (Enter in a field) by loading the schema instead of navigating away. - Before the same-origin auto-load, when the fields are empty, retrieve a saved credential via `navigator.credentials.get` and fill the fields, then start the load. Retrieval is gated on the same `isSameOriginTarget` invariant that guards the auto-load, so the credential stays bound to the origin it was stored for; a cross-origin `?url=` still neither retrieves a credential nor auto-loads. `mediation: 'silent'` never shows UI, so opening the page stays non-interactive, and failures are caught so the load still runs. `play.html` needs no retrieval step because it never auto-contacts a server on open. Verified with a DOM-stub harness over the page's own extracted `autoLoadOnOpen` / `isSameOriginTarget` (27 checks): a same-origin open with empty fields and a stored credential fills the fields and loads; with no stored credential it still loads with empty fields; URL-supplied credentials are never overridden; a cross-origin target neither retrieves a credential nor loads; `file:` open, missing `PasswordCredential`, and a throwing `get()` all degrade to the prior behavior. As with the other `programs/server/*.html` web UIs there is no committed CI runner for these DOM-stub checks, so the harness is run locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review's sole remaining blocker (the The
Verified with a DOM-stub harness over the page's own |
The last remaining AI-review blocker (`programs/server/schema.html`, `autoLoadOnOpen`) was a cross-origin credential leak via a check-then-act race. `autoLoadOnOpen` validated `isSameOriginTarget` and the empty user/password fields only *before* awaiting `navigator.credentials.get`. That retrieval is async, so while it was pending the user could paste a cross-origin URL into `#url` or start typing their own credentials; the callback then unconditionally filled the saved same-origin login and called `loadAll`, which reads the *live* fields via `connectionSnapshot`. A same-origin login remembered by the Credential Management API could therefore be POSTed as `user`/`password` to a user-chosen (possibly attacker) server on page open — the same leak `isSameOriginTarget` guards against on this path. Snapshot the URL up front and, after the retrieval, re-establish both invariants (URL unchanged, fields still empty). If either changed, abort the whole auto-load — the user is now driving and will load explicitly via the form (`Load` / Enter). The re-check is placed after the `try`/`catch` so it also guards the path where `navigator.credentials.get` throws. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the AI review's sole remaining blocker (the
The fix snapshots the URL up front and, after the retrieval, re-establishes both invariants (URL unchanged, fields still empty). If either changed, the whole auto-load is aborted — the user is now driving and will load explicitly via the form ( |
There was a problem hiding this comment.
The current guard only proves that the user did not edit the live fields while navigator.credentials.get was pending; it does not prove that the returned PasswordCredential belongs to the current backend URL. PasswordCredential is scoped to the page origin, while plain revisits initialize #url to location.origin at programs/server/schema.html:689. Concrete trace: explicitly load https://example.com/ch-proxy, let this PR save that login, then reopen plain /schema; get(...) can return the saved credential here and loadAll will POST it to https://example.com/?..., not to /ch-proxy. Please bind remembered-logins to the exact backend URL before auto-reusing them here (or only persist credentials for the exact default target).

Fixes a batch of user-reported usability issues in the
/schemaweb UI (the schema visualizer served by the HTTP handler, e.g. on play.clickhouse.com), and aligns its connection/authentication form withplay.html.UI fixes:
Loadbutton.color-schemeis set per theme and the custom scroll areas (viewport, node column lists, sidebar) are styled with the theme colours, so scrollbars no longer render light on the dark theme.ReplicatedReplacingMergeTreecan no longer squeeze the name out of the header.user-select: noneon nodes so dragging a table doesn't leave a text selection behind.Authentication form, matching
play.html:PasswordCredential/navigator.credentials.storeso the browser can offer to save and autofill them (guarded for browsers without support).url,user, andpasswordare read from the query string, and the password is stripped back out of the address bar viahistory.replaceState.SELECT version()probe and red on failure; status resets on input and re-checks on paste.Tested against the live play.clickhouse.com schema; the grouping and auth logic were additionally exercised by loading the page's own functions under a DOM stub.
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Several usability fixes for the
/schemaweb UI: load the schema on Enter, theme-aware scrollbars, readable table names, consistent per-database grouping of independent tables, no text selection while dragging, and aplay-style authentication form (Credential Management API, URL-supplied credentials with password stripping, and live credential validation).🤖 Generated with Claude Code