Skip to content

Write the UAT onboarding that does not exist yet - #2476

Merged
erikdarlingdata merged 6 commits into
devfrom
docs/uat-onboarding
Aug 21, 2026
Merged

erikdarlingdata merged 6 commits into
devfrom
docs/uat-onboarding

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

External testers start shortly and there is no path for them. The root README's Darling quick start is four steps ending at "Open the viewer and point it at the store to browse the fleet" — which is the whole instruction for the WPF viewer. The web dashboard appears once, as a clause inside a prose paragraph, with no procedure at all. MCP has a Lite-shaped Setup section that names the wrong port and the wrong verb for Darling.

Darling/README.md is not the answer, and it should not be made into the answer. Its 1,178 lines are a genuinely good reference: they tell you what a setting does, what a role can reach, why standbys are gated one way here and another way there. An onboarding answers a different question — I have nothing, what do I type — and rewriting the reference to answer it would cost the thing it is already good at.

So this adds the missing artifact and links into the existing one rather than duplicating it.

The layout, and why

One file: docs/uat-onboarding.md. It matches docs/postgres-first-target-runbook.md, which already established the shape here — an ordered procedure with a proof point at every step, deferring to the README for field-level detail.

One file rather than one per surface, because the three surfaces share most of a prerequisite: the install location rule, the two .NET runtimes, the JSONC rule, --encrypt-password, the two-minute first start, --configure-firewall. Split three ways, that shared half gets written three times and goes stale in two of them. Independent followability is kept structurally instead: Part 1 is explicitly the shared one, and Parts 2, 3 and 4 assume only Part 1 and never each other. The one fact that genuinely spans all three — which settings the file owns and which the store owns — is stated once in Part 0 so the later parts point at it rather than each re-deriving it.

Every step ends with the log line, HTTP response, or on-screen state that proves it worked. Those are quoted from the source that emits them, not paraphrased.

README.md step 4 is replaced. It was the weakest sentence in the document. It becomes the two steps it was hiding — start the viewer, which needs no configuration at all on the service host because it finds the same darling.json one directory up; and export a folder for a seat on another machine — plus a fifth step for the two off-by-default surfaces. Both READMEs link into the new procedure.

The correctness fix in the first commit

Verifying the gotchas against source turned up a doc drift worth its own commit, because it is the kind that changes what someone chooses to expose.

Darling/README.md and darling.sample.json still say the web dashboard's loopback stays tokenless while the endpoint is LAN-exposed. DecideWebAuth exempts loopback from the CIDR check only; in network mode a request from the box itself still needs a session cookie or a valid ?token=. That changed when the Custom Views composer added view create/update/delete and /api/compose/run and the surface stopped being read-only. The same fact makes "hosts no write paths" wrong two paragraphs later — and the old text was using that wrong half to justify the wrong conclusion about loopback, so the two are one correction stated twice.

A dashboard with no network block genuinely is tokenless (the middleware is registered inside if (networkMode)), so the claim is wrong only for the exposed case, which is the case the sentence was about. The fix also names the response an operator will actually see on the other arm: an in-CIDR request with no credential gets the login form as a 200, not a 401.

What was verified against source

Everything the doc asserts was read out of the code, not out of the docs. In particular: the store/file authority split for mcp/web (ResolveEndpointToggle, and the network block held for the process lifetime); the firewall rule taking its port from the control plane, and the narrowed-but-not-closed window on an upgrade of a moved-port box; --encrypt-password writing its prompt to stderr; DPAPI LocalMachine scope on every blob; the read-only probe, its fail-safe arms, and the remote seat defaulting to role: "viewer" while the local seat defaults to connectAs: "admin"; the ~2 minutes first start and the log sequence that ends it.

What a UAT user needs that the product does not do yet

Part 6 states these plainly rather than letting testers find them one support round at a time: no TLS on either HTTP endpoint; no verb that reprints an existing MCP or web token; install-darling.ps1 not checking for the .NET runtimes, so a missing one surfaces as a raw .NET host error; no global read-only indicator in the viewer; rejected 401/403/400 requests leaving nothing in the service log.

erikdarlingdata and others added 3 commits August 21, 2026 21:09
Three places still describe the web dashboard as it behaved before the
Custom Views composer landed, and all three are the ones an operator reads
while deciding what to expose.

DecideWebAuth exempts loopback from the CIDR check ONLY. In network mode a
request from the box itself still has to carry a session cookie or a valid
?token= -- that changed when view create/update/delete and /api/compose/run
made the surface writable, because a tokenless local process (a scheduled
task, SSRF'd code, another user's session) could otherwise read the whole
store and mutate views with no credential. The auth middleware is registered
inside `if (networkMode)`, so a dashboard with no network block really is
tokenless; the claim is only wrong for the exposed case, which is the case
the sentence was about.

"Hosts no write paths" is wrong for the same reason: POST/PUT/DELETE
/api/views and POST /api/compose/run exist, and GET /api/session reports
can_edit for any authenticated seat. That write IS the reason loopback
authenticates, so the two corrections are one fact stated twice -- and the
old text used the wrong half of it to justify the wrong conclusion.

Also names the two responses a reader will actually see, because "an
out-of-CIDR request is refused with 403" left the other arm unstated: an
in-CIDR request with no credential gets the login form back as a 200, not
a 401, and someone debugging by status code needs to know which is which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
External UAT testers start shortly, and the only onboarding that exists is
the root README's four-step Darling quick start, whose last step is "Open
the viewer and point it at the store" with nothing about how. The web
dashboard appears once, as a clause in a prose paragraph, with no procedure.
Darling/README.md is 1,178 excellent lines of REFERENCE -- it answers "what
does this setting do", which is a different question from "I have nothing,
what do I type", and turning it into the second would cost it the first.

So this adds the missing thing rather than growing the reference: one
ordered procedure, docs/uat-onboarding.md, matching the shape
postgres-first-target-runbook.md already established here -- a proof point
at every step, quoting the log line, HTTP response or on-screen state that
tells someone it worked without asking anyone.

One file, not three. The three surfaces share most of a prerequisite: the
install, the two .NET runtimes, the JSONC rule, --encrypt-password, the
two-minute first start, the firewall verb. Split three ways that shared half
gets written three times and goes stale in two of them. Each surface still
stands alone -- Parts 2, 3 and 4 assume only Part 1 and never each other --
and the one fact that spans all three (which settings the file owns and which
the store owns) is stated once, in Part 0, where both later parts can point
at it instead of re-deriving it.

Everything asserted was read out of the source rather than out of the docs,
which is how the #1649 loopback drift fixed in the previous commit surfaced.
The seven gotchas that cost real time get their own section with links back
to where they bite, and Part 6 names the gaps -- no TLS on either endpoint,
no verb that reprints a token, no runtime pre-check in the installer, no
global read-only indicator -- because an admitted gap is cheaper than the
support round that discovers it.

README.md's step 4 becomes the two steps it was hiding (start the viewer,
which needs no configuration on the service host; export a folder for a
remote seat), plus a fifth for the two off-by-default surfaces, and both
READMEs link into the new procedure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Restart-Service and --configure-firewall both need elevation, and the two
LAN sections showed them in a bare code fence after prose that had moved on
to talking about the config block. Every other elevated step in the document
says so at the point of typing; these two did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread docs/uat-onboarding.md
Comment on lines +491 to +494
What it *can* write is one thing: **Custom Views** — the saved dashboards and notebooks in
`config.custom_views`, created and edited through the built-in composer, plus `POST /api/compose/run` to
preview one. That is the `viewer` role's single narrow write grant, and it is available to **any authenticated
seat**, not only a local one. It is also the reason the loopback token exemption went away (see 3.5).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"That is the viewer role's single narrow write grant" is not accurate at the role level. DarlingManagedRoles.cs:494-496 and Darling/tools/provision-roles.sql:133-135 also grant viewer INSERT, UPDATE, DELETE on config.database_state_expected (the WPF Viewer's per-database state-override editor, #1986). It's true that grant isn't reachable through any web-dashboard HTTP route (no matching route in DarlingWebEndpoints.cs), so the claim about what the web dashboard can write still holds — but "the role's single narrow write grant" overstates it and would mislead someone auditing what the viewer role can actually do. Suggest scoping this to "the web dashboard's only write path" rather than describing it as the role's only grant.

Comment thread Darling/README.md Outdated
```

**What a web token can reach.** The web dashboard is **read-only over the collected store** — it connects as the least-privilege `viewer` role and hosts no write paths and no live-server queries (no `analyze_server`, no plan re-execution). A token-holder can view everything collected, change nothing, and reach no monitored server — which is why loopback stays tokenless here while MCP's does not.
**What a web token can reach.** The web dashboard is **read-only over the collected store** — it connects as the least-privilege `viewer` role and hosts no live-server queries (no `analyze_server`, no plan re-execution). Its one write path is the Custom Views composer, which is that role's one write grant: INSERT/UPDATE/DELETE on `config.custom_views` and nothing else. So a token-holder can read everything collected and author saved views, and reach no monitored server — but that write is why an exposed dashboard authenticates loopback too, exactly as MCP does.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"that role's one write grant: INSERT/UPDATE/DELETE on config.custom_views and nothing else" is factually wrong — the viewer role also has INSERT/UPDATE/DELETE on config.database_state_expected (DarlingManagedRoles.cs:494-496, Darling/tools/provision-roles.sql:133-135, the WPF Viewer's per-database state-override editor, #1986). That second grant isn't exposed by any web-dashboard HTTP route, so "the web dashboard's one write path" is accurate, but "that role's one write grant ... and nothing else" isn't. Same issue repeated in docs/uat-onboarding.md ("the viewer role's single narrow write grant"). Worth tightening the wording so it describes the dashboard's write surface, not the role's total grants.

The seat section listed "a `— read-only seat` window-title suffix" among
the ways to tell which seat you have. No such suffix exists on the main
window. What exists is one DIALOG title -- DatabaseStateOverridesWindow
appends "(read-only seat — changes cannot be saved)" -- which a reader
looking at the main title bar will not find.

Sending a UAT tester to check a title bar that says nothing is worse than
telling them there is nowhere central to look, so it now says the latter
and points at Part 6, where the missing global indicator is already listed
as a known gap. That gap is filed as #2479 item 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

claude Bot commented Aug 21, 2026

Copy link
Copy Markdown

Reviewed. This is a docs-only PR (no T-SQL/C# changes), so the style guidelines in CONTRIBUTING.md don't directly apply; I focused on factual accuracy of the technical claims against the current source, broken links/anchors, and Lite/Darling parity.

Verification approach: cross-checked ~20 specific claims (auth behavior in DarlingWebEndpoints.cs/DarlingMcpHostService.cs, role grants in DarlingManagedRoles.cs/provision-roles.sql, CLI verb behavior in DarlingCliCommands.cs/install-darling.ps1/uninstall-darling.ps1, the viewer's read-only probe and schema-version-mismatch message, and every internal/cross-file markdown anchor) against the actual code on this branch.

Result: everything checked out except one recurring inaccuracy, flagged inline in two places (docs/uat-onboarding.md and Darling/README.md): the doc states the Custom Views write is "the viewer role's single/one write grant." The viewer postgres role actually has a second write grant — INSERT/UPDATE/DELETE on config.database_state_expected (the WPF Viewer's per-database state-override editor, #1986) — in DarlingManagedRoles.cs and provision-roles.sql. The security-relevant claim (what the web dashboard's HTTP routes can write) is still correct, since that second grant isn't exposed over HTTP, but "the role's one write grant ... and nothing else" overstates it and would mislead anyone using this doc to reason about the viewer role's total blast radius.

Everything else — the DecideWebAuth loopback/CIDR/token behavior, the 403/200 status codes, the MCP bearer/CIDR asymmetry, the 101-tools/82-web-routes/19-exclusions math, the read-only-seat probe and its fail-safe behavior, config_command enqueue semantics, all the installer/CLI verb claims (--encrypt-password stderr behavior, --test-connection exit codes, --configure-network/--configure-firewall behavior including the #2436 firewall-sweep fix, --export-viewer-config ACL and non-zero exit, the unseeded-store refusal on --enable-web/--enable-mcp), the schema-version-mismatch message text, and every internal/relative markdown anchor across the three edited files — verified accurate against source.

No Lite/Darling parity concern: this PR only touches Darling's onboarding path, which is the one that actually lacked a procedure (Lite's existing 3-step quick start already covers its single-surface flow).

erikdarlingdata and others added 2 commits August 21, 2026 21:18
Review caught both new sentences claiming config.custom_views is the viewer
role's only write grant. It is not: DarlingManagedRoles.cs and
provision-roles.sql also grant INSERT/UPDATE/DELETE on
config.database_state_expected, for the Viewer's per-database state-override
editor (#1986).

The claim I wanted was about the WEB HOST's reachable surface, which really
is custom views alone -- no route in DarlingWebEndpoints and no reference in
wwwroot touches database_state_expected. Saying that as a fact about the ROLE
overstated the least-privilege boundary in the one direction a reader would
act on, which is the whole reason the previous commit exists.

Worth stating rather than just narrowing, because the second grant is
currently spent by nobody: GetDatabaseStateExpectationsAsync gates its writes
on IsReadOnly, and IsReadOnly is the has_table_privilege probe on
config_mute_rules -- which the viewer role fails by design. So a viewer-role
seat holds the grant and cannot reach the editor that uses it, on either
surface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gotcha 11 said a screen of Warning statuses "is not an incident" because
status is "collection freshness, not health". That is wrong, and wrong in
the worst available direction for a monitoring tool: it tells a new user
their first reflex on seeing amber should be to disregard it.

ServerHealthBands.ClassifyBand returns Warning for three unrelated causes.
A metric at warning severity -- CPU, blocking, deadlocks, memory, threads
-- returns Warning straight out of the overallMetricSeverity switch, and
that IS a real condition on the server. Awaiting-first-collection returns
Warning before that switch is reached. Stale collection returns it after,
through the hasCollectorErrors arm the viewer re-uses to carry staleness.

The advice survives in narrowed form, because the original observation was
not baseless: on a fresh install every server is awaiting first collection
at once, and at fleet scale staleness is routine. So an opening screen of
amber usually is not an incident. What it must not become is a rule for
every screen after that one.

It now names the three causes and points at the tooltip #2429 added, which
is built from the same metric rows shown underneath the status word, so it
cannot disagree with them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@erikdarlingdata
erikdarlingdata merged commit 1392bbd into dev Aug 21, 2026
6 checks passed
erikdarlingdata added a commit that referenced this pull request Aug 21, 2026
#2476's "What it will not do" listed the wait-type picker, tempdb, the
perfmon picker, Running Jobs and the configuration grids as missing. All
five ship on this branch, and a UAT tester reading a list of absences
that are no longer absent is worse served than by no list at all.

Two counts were also wrong and are corrected from the XAML: the viewer
has nineteen top-level per-server tabs, not fourteen, and 65 TabItems
counting the nested ones.

What remains missing is now divided by WHY, because the three groups have
different answers. Reads the service serves that no page shows (the eight
PostgreSQL ones, blocked by the fleet payload carrying no target-engine
discriminator; the nested-array scoped config; store-wide metrics).
Viewer data with no read endpoint at all, which no web work reaches.
And the desktop things a web imitation would be worse than — plan
viewer, heatmap, block chain, deadlock graph, and the grids' own
filter/export/repro/drill-down affordances.

The exclusion bullets above it are untouched: no plan analysis, no
analyze_server, no config_command enqueue, no alert tuning. Those are
the viewer role's grants and the web host having no write path, and
nothing here changes them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant