Skip to content

fix(dash0): the live demo's deep link, editing your own demo check, and the untranslated dashboard - #344

Merged
fclairamb merged 33 commits into
mainfrom
batch/2026-09-07
Sep 8, 2026
Merged

fclairamb merged 33 commits into
mainfrom
batch/2026-09-07

Conversation

@fclairamb

Copy link
Copy Markdown
Owner

Two specs from specs/todos/, plus the i18n leftovers they exposed.

The live demo's deep link (spec 2026-09-07-02-demo-deep-link-and-own-check-edit)

?demo=true was honoured only on an org-scoped login page, so the link that reads
naturally — solidping.io/dash0/login?demo=true — dropped the flag and landed on an
ordinary login form. It now works from all four entry points (/orgs/$org/login,
/login, /, /orgs/<any>) through one shared parseDemoFlag, and it outranks an
existing session: a visitor already signed into their own org re-enters the demo, and one
already in the demo is taken there without minting a second session.

Editing a check you created in the demo also works again. Every save issued a "replace the
notification bindings" PUT carrying exactly what was already stored; that route is closed
to the demo by design, so the no-op write was refused and took the whole edit down with
it — the rename went through, but the visitor saw a red refusal toast and stayed stuck on
the form. The write now fires only when the selection actually differs.

The audit on this one caught a real bug in the first pass: the escalation picker's
"No escalation (silent)" option POSTs a zero-step policy when the org owns none, so it
was not "just the allowlisted PATCH body" as the implementation claimed. That branch is
now withheld from demo sessions, and a blank-page regression on demo-disabled instances
was fixed alongside it. The server allowlist is unchanged —
TestEveryNonGETRouteIsClosedToADemoSession still passes as-is, which is the proof.

Untranslated strings and the demo refusal (spec 2026-09-07-02-untranslated-strings-...)

Five components rendered English inside otherwise-French pages. All are now wired to
en/fr/de/es. The demo write refusal is localized by error code rather than by
the server's English title, announced once (one toast.info, not a blue toast plus a red
banner plus a red toast depending on the page), and reworded so it is true for both the
route-level and the ownership refusal. The check edit route no longer lets a demo visitor
fill in a form the server will refuse — it shows the seeded-check note and a Clone button.

A new whole-tree locale key-parity test is the guard that stops the next component slipping
through; it immediately found and fixed pre-existing gaps in four other namespaces.

The leftovers that test then exposed

The specs' string tables were written from screenshots, so they missed strings in the same
components. Followed up: the check form's last two labels, the whole publications panel,
and the status-page incident editor — which was almost entirely English. The publication
state badge printed its raw enum value in every language; state and severity labels now
come from one lib/publication-labels.ts helper shared by both screens, so a picker and the
badge beside it cannot disagree, with a humanizing fallback so a value from a newer server
renders readably instead of leaking a translation key.

Known gap, deliberately not fixed here

QueryErrorView's resource prop is passed as a raw English noun by all six routes that
use it, interpolating English into an otherwise-translated sentence. Fixing one call site
would be inconsistent; it wants a single pass across all of them.

QA

  • make build-backend lint-back — clean, 0 lint issues
  • make build-dash0 + bun run lint — 42 errors, exactly the pre-existing baseline;
    every touched file verified individually against its pre-change version
  • bun run test:unit — 1485 passing
  • demo-account.spec.ts — 18/18 against a side-car test server, including a rebuilt
    pre-fix negative control that fails exactly as reported and passes once restored

🤖 Generated with Claude Code

fclairamb and others added 30 commits September 7, 2026 08:36
…tence

/no-org's create-org preview and submission derived the slug from the
localized display sentence ("L'organisation de Florent"), so every locale's
proposal shared the same boilerplate prefix and the 20-char cap truncated the
one part that identified the person. Add suggestedSlugBase() to derive the
slug from the first name instead, and a new slugBase request hint (server:
CreateOrgRequest.SlugBase, consulted by GenerateUnique only when slug is
omitted, never answering 422/409) so the server keeps owning collision
handling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ion handling

Proves the new CreateOrgRequest.SlugBase field for spec 2026-09-07-01: it wins
over Name when present, is normalized/suffixed like any other candidate,
falls through silently when unusable, is entirely ignored once an explicit
Slug is supplied, and does not soften the strict slug contract (still 422
invalid / 409 taken) on the same request path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extends the existing "/no-org proposes an organization" block for spec
2026-09-07-01: the slug preview and the Advanced-seeded field must read
exactly the lowercased first name (not merely contain it), Create must land
on that exact slug, and the org's display name must still be the possessive
sentence. Switches the fixture name to a unique-per-run "Zoe<stamp36>" so the
exact-match assertions stay correct on a persistent dev database.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
build-backend, lint-back, full backend test suite (go test ./... -short),
build-dash0, dash0 eslint (no new errors), dash0 vitest unit tests, and the
extended create-org.spec.ts E2E block (run against a side-car server, all 3
tests green) all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…isting session

?demo=true is now parsed on /, /login and the /orgs/$org layout, not only on
the org login page, so solidping.io/dash0/login?demo=true is a link that works.
One coercion (lib/demo.ts parseDemoFlag) serves all four surfaces.

The flag also outranks a session the visitor already holds: the
redirect-if-authenticated effect stands down, and enterDemo short-circuits to
the demo org when the current principal is already the demo user instead of
minting a second session.

The check edit page stops issuing a no-op PUT .../channels on every save —
correct for every user, and the reason a demo visitor's own edit was answered
403 DEMO_READ_ONLY and stranded on the form. Secondary writes (channels,
dependency sync) now degrade the way the create page does, and the pickers a
demo session cannot use are replaced by the read-only note.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…wned check

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
QA (side-car test server on :4010, SP_RUNMODE=test + postgres):
- make build-dash0: green
- bun run lint: 42 errors / 445 warnings — unchanged pre-existing baseline,
  zero new errors in any touched file (verified file-by-file against HEAD)
- bun run test:unit: 86 files / 1390 tests green (31 in the two new/extended
  lib suites)
- make build-docs: green
- e2e/demo-account.spec.ts: 15/15 green, with a NEGATIVE CONTROL — rebuilt
  with the pre-fix edit page, the new rename test fails exactly as reported
  (stranded on the edit form), and passes again once restored
- regression sweep: login, login-2fa, checks, check-detail, check-dependencies,
  check-edit-period-persistence, check-form-progressive-disclosure,
  device-authorization, mcp-oauth-consent — all green
- backend untouched; TestEveryNonGETRouteIsClosedToADemoSession still green

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…dge extension

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Re-affirmed after the wiki note (wiki/ is excluded from CI by design, see
.github/workflows/ci.yml — no code, build or test input changed since the
previous gate run).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the three gaps the audit found on top of the existing
implementation.

1. §C.2 violation. The escalation picker's "No escalation (silent)"
   option is not a plain selection: with no zero-step policy in the org
   it POSTs one to /orgs/:org/escalation-policies, which is outside the
   demo allowlist — and the demo org's single seeded policy has a step,
   so for a demo visitor that option could only ever be refused, with
   the error swallowed and the selection snapping back. EscalationSelect
   now takes canCreatePolicy; the shortcut is withheld when the session
   may not create a policy and there is none to reuse. The server
   allowlist is untouched.

2. Regression introduced by part B: the login page's
   redirect-if-authenticated effect stood down on the demo flag alone,
   while the auto-login effect also requires the instance to HAVE a
   demo. On a self-hosted install with the demo off, an authenticated
   visitor following .../login?demo=true sat on the render guard's blank
   page forever. demoAutoLoginOwnsRedirect keeps the two in agreement,
   including while the public-config document is still in flight.

3. CHANGELOG: the docs deep link is now absolute — the file renders on
   GitHub, where a root-relative path resolves against github.com.

Tests: canOfferSilentEscalationShortcut and demoAutoLoginOwnsRedirect in
lib/demo.test.ts; in demo-account.spec.ts the demo edit flow asserts the
silent option is absent while the picker still opens, counts zero POSTs
to escalation-policies across the whole flow, and a new negative control
proves an ordinary session is still offered the shortcut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
check-form.tsx mixed ~25 hard-coded English labels into an otherwise
translated form (title/subtitle, Identity & target, Enabled, Type,
Notifications, Advanced, Cancel, the numeric default placeholders, the
inline timeout/interval validation messages, and the two "Failed to
...check" fallbacks). Move them into checks.json's form.* keys for all
four locales so a French/German/Spanish visitor no longer sees English
mid-form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s and the status-update form

Four more untranslated surfaces flagged by the 2026-09-07 demo screenshots:
the incident detail page's Status updates card (title, Add update button,
description, dialog footer, status-page placeholder), its Notifications
card (title, description, empty state, table headers), the publications
panel (zero useTranslation calls before this), and the standalone
status-update-form.tsx (16 hard-coded labels, now reusing the existing
statusUpdates:kinds.* keys for its Kind picker). Also translates the
leftover "No badge" option on the status-page incident publication editor.

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

Three problems with DEMO_READ_ONLY (spec 2026-09-07-02): the message was
never translated (api/client.ts surfaced the server's English error.title
verbatim as ApiError.message), it wore three different costumes (a blue
info toast, a red destructive Alert on the check form, a red toast.error
on the status-page section dialog), and its wording ("editing your own
checks is allowed") contradicted the ownership refusal a visitor actually
hits on a seeded check.

- api/client.ts now builds ApiError's message from i18n.t("org:demo.writeRefused")
  when the code is DEMO_READ_ONLY, instead of the server's English title
  (which stays in the JSON body verbatim for curl/CLI users). The
  deduplicated toast uses the same localized string.
- auth.DemoWriteMessage is reworded to something true for BOTH the
  route-level and the ownership refusal: "Only checks you create here can
  be changed — sign up free to change anything else."
- The status-page "add section" dialog's catch block no longer
  toast.error()s on top of the client's toast for DEMO_READ_ONLY.
- check-form.tsx skips setError (and its destructive Alert) for
  DEMO_READ_ONLY, mirroring how checks.new.tsx already swallows it for
  its secondary channel/dependency writes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The edit ROUTE had no demo gating: the detail page already hides the edit
affordance's explanation behind org:demo.seededCheck and offers Clone
instead, but checks.$checkUid.edit.tsx rendered a fully enabled CheckForm
for anyone — reachable via the checks-list row menu or a bookmarked URL —
so a demo visitor filled out a form the server would refuse on save
(spec 2026-09-07-02 §B.4).

checks.$checkUid.edit.tsx now checks canDemoEditCheck(...) for a demo
session and, when the visitor does not own the check, renders the same
seededCheck alert plus a Clone button (reusing the detail page's
useCloneCheck flow) instead of the form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Generalizes lib/operator-notifications-locales.test.ts (which covered one
feature) into src/locales/locale-parity.test.ts: for every namespace file
under locales/en, assert fr/de/es carry exactly the same key set. This is
the guard spec 2026-09-07-02 asked for — the untranslated check-form and
incident-page strings this spec fixes slipped through because nothing
caught a component shipping t() calls whose keys only existed in en.

Running it cold surfaced pre-existing gaps unrelated to this spec's
components — nav.json (promote, sessions), org.json (settings.default
Escalation*), escalation.json (list.deleteUsage, list.silentBadge), and
integrations.json/checks.json (the Freebox integration and check-type
copy, plus the notification test-send strings) were missing from fr/de/es
entirely. Filled all of them in rather than narrowing the test's scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ection note

Two new demo-account.spec.ts cases for spec 2026-09-07-02 §B.4/§B.5: a demo
session opening /checks/<seeded>/edit sees the read-only note and a Clone
button instead of a form (and check-name-input never renders), and the
status page detail replaces its add-section affordance with the read-only
note, with no "Add Section" control left in the DOM.

Verified against a side-car test server on :4010 (SP_RUNMODE=test,
solidping-postgres container) — all 18 demo-account.spec.ts cases pass.

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

Full scoped gate, all green on the first run (no fixups needed):
- make build-backend lint-back test: backend builds, golangci-lint clean,
  full go test ./... passes (including server/test/integration).
- make build-dash0: builds and type-checks clean.
- bun run lint: exit 1 on the pre-existing ~42 react-hooks errors (baseline,
  unrelated to this spec's files) — zero new errors in any file this spec
  touched, confirmed by diffing each touched file's lint output against its
  pre-change version.
- bun run test:unit: 1469/1469 passing, including the new client.test.ts
  localization/toast-dedup cases and the new locale-parity.test.ts.
- Playwright web/dash0/e2e/demo-account.spec.ts: 18/18 passing against a
  side-car test server on :4010 (SP_RUNMODE=test, solidping-postgres
  container), now shut down.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous i18n pass worked from a spec whose string table listed the
strings visible in five screenshots. These were in the same two components
but not in that table, so they stayed English in an otherwise-French page.

check-form: the Notifications card description and the Timeout label.

incident-publications-panel: the severity options, "auto-published", the
Unpublish control and its confirmation dialog, and the four publish and
unpublish toasts.

The severity badge on an already-published incident printed the raw enum
value, so translating only the picker would have left the two disagreeing —
"Majeur" in the dropdown, "major" on the badge right below it. Both now
render from one SEVERITIES list through severityLabel().

The publication STATE badge still prints its raw value ("resolved",
"monitoring"); it is a different enum, owned by statusUpdates:kinds, and
deserves its own pass with a fallback for values the client does not know.

locale-parity.test.ts covers the new keys in all four locales — verified by
removing one and watching it fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The badge printed publication.state raw — "investigating", "monitoring" —
in English, in both places a publication is shown: the incident page's
publications panel and the status-page incident detail. The timeline on
that page badged update.kind the same way.

State and kind are one vocabulary, and statusUpdates:kinds already ships it
in four locales, so the labels are reused rather than copied into a second
namespace that would drift from the first.

lib/publication-labels.ts holds both this and the severity helper the last
commit left inline in the panel, so the two screens that render these badges
render them identically. Unknown values humanize ("partial_outage" ->
"Partial Outage") instead of leaking an i18n key into a badge: t() returns
the key on a miss, and the backend can add a state before the frontend
catches up.

Still English on the status-page incident detail, untouched here: "Title",
"Severity", the Minor/Major/Critical options, "Save changes", "edited",
"auto-published", the autopilot alert, "No updates yet." and the "Incident
updated" toast. That route needs its own pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last file in this corner that was still almost entirely English: the
autopilot alert, both card headers and their descriptions, the Title /
Severity / Kind / Message labels, the severity and kind pickers, the
"auto-published" and "edited" badges, the Affected / no-components /
no-updates timeline copy, the "· automated" marker, and the three toasts.

Where a string already existed elsewhere it is reused rather than copied:
Title, Kind and Save changes come from statusUpdates:form.*, the severity
and kind labels from the publication-labels helper. The severity picker is
now built from the same SEVERITIES list as the panel on the incident page,
so the two screens offer the same options in the same order.

Still English, deliberately: the QueryErrorView resource name ("incident
publication"). Every route in the app passes that prop as a raw English
noun, so it interpolates English into an otherwise translated sentence
everywhere — fixing it here alone would be inconsistent. It wants one pass
across all six call sites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fclairamb and others added 3 commits September 8, 2026 02:36
A sweep across every component and route that still rendered English inside an
otherwise-translated page. ~500 strings, all four locales.

The bulk is the check form: the database, mail, game, web, ClickHouse, infra,
misc, messaging and network type panels were never internationalized at all.
Also done: the account device and OAuth consent pages, the status-page and
integration forms, the notification detail page, the crash fallback, the
sidebar, the label pickers and the dashboard KPI tiles.

Three defects were structural rather than a missed string:

- formatRelativeTime returned "just now" / "3d ago" in every language, and
  existed in THREE copies. The two local copies in account.sessions and
  account.tokens are gone; the shared helper takes a t.
- sourceLabel produced the values in the incident Notifications table's Source
  column ("Escalation step (cycle 2)"). Its headers were translated last
  round; the values were not.
- 129 strings were written as t("key", "English default") with the key present
  in NO locale file. They read as translated and pass review, and i18next
  quietly renders the English. Key parity cannot see them: the key is missing
  from en/fr/de/es equally, so parity holds. The whole private-locations
  feature (83 strings) shipped this way.

locales/translated-defaults.test.ts closes that blind spot — every t() call
carrying an inline default must resolve to a real en key. It has its own
guard-the-guard assertion, because a source walk that finds nothing would
pass vacuously.

Enum labels that are rendered rather than stored now go through shared helpers
(publication-labels, notifications, time-ago) so a picker and the badge beside
it cannot drift apart, with a humanizing fallback: the backend can add a value
before the frontend catches up, and a raw token is wrong-but-readable where a
leaked i18n key is neither.

Deliberately left in English: protocol and product names, example values, the
seeded test credential in the login placeholder, and QueryErrorView's resource
prop, which all six call sites pass as a raw English noun and which wants one
coordinated pass rather than six scattered ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fclairamb
fclairamb enabled auto-merge (squash) September 8, 2026 01:13
@fclairamb
fclairamb merged commit a4ed4ff into main Sep 8, 2026
13 checks passed
@fclairamb
fclairamb deleted the batch/2026-09-07 branch September 8, 2026 01:48
@fclairamb fclairamb mentioned this pull request Sep 8, 2026
fclairamb added a commit that referenced this pull request Sep 8, 2026
release-please renders one line per squashed PR, and #344 carried five
distinct user-visible changes behind a single title. Folds in the prose
written alongside each of them, and consolidates the three overlapping
i18n entries into one — the widest already contained the other two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 8, 2026
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