Skip to content

Commit 4b42030

Browse files
mpstatonclaude
andcommitted
issue(context-v): Org Workbench is dead on prod because every remote dials localhost, and the UI overhaul gets its numbers
Two issues raised from one prod screenshot of Org Workbench on the reach-edu workspace — a surface that renders its chrome and then shows nothing at all. The first is a one-line source defect with a wide blast radius. Sixteen remotes declare `const WS_URL = 'ws://localhost:3001/ws'` and never read `PUBLIC_WS_URL`, so on augment.didi.sh they open a data socket against the visitor's own laptop. Org Workbench's Dockerfile plumbs the variable through as ARG and ENV and Railway sets it, which makes every rung of the config chain look green while the value lands somewhere no source line consults. It cannot work in production for two independent reasons: wrong host, and `ws://` from an `https://` origin is blocked as mixed content. This takes down the whole Augment-from-DB flow, not one surface — org-workbench, search-and-add and search-results all carry it. It works locally only because the operator's laptop really is running workspace-service on :3001, which masks the defect perfectly. The second measures what No-Component-Library-UI-Improvised-Not-Component-Based jotted a month ago and left at Open · Jotted. The token half of the design system shipped to 19 of 20 apps; the component half is two components with three consumers. `design-drift.mjs` reports 99 fail / 0 warn across 16 apps, with 53 hardcoded hex values and ten distinct raw z-index values competing for a stacking contract that does not exist. The load-bearing finding is that org-workbench itself has zero drift failures and is still the surface that reads as improvised — the linter checks values, not form, so a remote can pass every rule we own and still invent its own visual dialect. Closing all 99 would not fix the screenshot. Also included: gitignore docker-compose.override.yml, so a local host-port remap can coexist with a sibling stack without dirtying the tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HuzSPdPK4QEhs7eztHgoC
1 parent fd7f0ea commit 4b42030

3 files changed

Lines changed: 347 additions & 0 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,6 @@ history.txt
6363
# Corpus scope is committed in .graphifyignore, not here.
6464
graphify-out/
6565
tools/
66+
67+
# Local-only compose overrides (host port remaps, etc.)
68+
docker-compose.override.yml
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
---
2+
title: "Every remote hardcodes the workspace WebSocket to localhost — Org Workbench loads no data on augment.didi.sh"
3+
lede: "Sixteen remotes dial `ws://localhost:3001/ws` with no env read. On prod that points at the visitor's own laptop, so the socket shows `closed` and the roster never fills."
4+
date_created: 2026-08-21
5+
date_modified: 2026-08-21
6+
authors:
7+
- Michael Staton
8+
augmented_with:
9+
- Claude Code on Claude Opus 5
10+
semantic_version: 0.0.0.1
11+
tags:
12+
- Issue
13+
- Augment-It
14+
- Org-Workbench
15+
- Deployment
16+
- Microfrontends
17+
- WebSocket
18+
- Reach-Edu
19+
- Module-Federation
20+
status: Open · Diagnosed · Root cause pinned to a one-line source defect
21+
site_uuid: c85ae72b-182a-4d7a-b8b7-ac47e8ffb3de
22+
hex_code: 3jweyu
23+
date_authored_initial_draft: 2026-08-21
24+
date_authored_current_draft: 2026-08-21
25+
publish: true
26+
---
27+
28+
# Every remote hardcodes the workspace WS to localhost
29+
30+
## Why Care?
31+
32+
On `https://augment.didi.sh`, the **Org Workbench** surface for the
33+
**reach-edu** workspace renders its chrome — title, `SurrealDB · Organizations`
34+
badge, `client: reach-edu`, search box, `+ New organization` — and then shows
35+
**nothing**. No coverage roster, no organizations, no people. A red **`closed`**
36+
pill sits in the top-right corner. The same surface, same workspace, same commit
37+
works perfectly on `localhost:3100`.
38+
39+
That "works local, dead on prod" split is the tell, and it is not a data
40+
problem, a SurrealDB problem, or a tenancy problem. The remote is asking the
41+
*visitor's own laptop* for its data.
42+
43+
## The root cause — one line, no env read
44+
45+
`apps/org-workbench/src/App.svelte:20`:
46+
47+
```ts
48+
const WS_URL = 'ws://localhost:3001/ws';
49+
```
50+
51+
That is the whole bug. Compare the pattern the shell, `chat`, and
52+
`corpora-curator` all use correctly — for example `shell/src/App.svelte:41-43`:
53+
54+
```ts
55+
const WS_URL =
56+
((import.meta as { env?: Record<string, string> }).env?.PUBLIC_WS_URL as string | undefined) ||
57+
'ws://localhost:3001/ws';
58+
```
59+
60+
rsbuild inlines `PUBLIC_`-prefixed vars into `import.meta.env` at build time.
61+
Org Workbench never performs that read, so no build-time value can reach it.
62+
63+
**The deployment looks correctly configured, which is what makes this
64+
expensive to spot.** `apps/org-workbench/Dockerfile:23-28` faithfully declares
65+
and exports the variable:
66+
67+
```dockerfile
68+
ARG PUBLIC_WS_URL
69+
ENV PUBLIC_WS_URL=$PUBLIC_WS_URL
70+
```
71+
72+
and Railway sets it per [[../../DEPLOYMENT]]. Every rung of the config chain is
73+
green. The value simply lands in a build environment that no source line ever
74+
consults, and is dropped on the floor.
75+
76+
## Two independent reasons it can never work in production
77+
78+
1. **Wrong host.** `localhost:3001` in a browser on `augment.didi.sh` resolves
79+
to the *viewer's* machine, not Railway's `workspace-service`. It works on
80+
the operator's laptop for the accidental reason that the laptop really is
81+
running `workspace-service` on `:3001` — the local stack masks the defect
82+
perfectly.
83+
2. **Mixed content.** Even if a viewer *did* run the backend locally, an
84+
insecure `ws://` connection is blocked outright by every modern browser when
85+
the page origin is `https://`. Prod needs `wss://ws.augment.didi.sh/ws`.
86+
87+
## Blast radius — this is a family defect, not one surface
88+
89+
Sixteen remotes carry the identical hardcoded constant:
90+
91+
| App | Line | Deployed to prod? |
92+
|---|---|---|
93+
| `org-workbench` | `App.svelte:20` | **yes** — broken, this report |
94+
| `search-and-add` | `App.svelte:22` | **yes** — same break |
95+
| `search-results` | `App.svelte:16` | **yes** — same break |
96+
| `corpora-curator` | `App.svelte:9` | yes — *works anyway*, see below |
97+
| `chat` | `App.svelte:20` | yes — correct, reads env with localhost fallback |
98+
| `record-collector`, `records-surface`, `pack-runner`, `sort-filter-lens`, `person-db-resolver`, `record-db-resolver`, `affiliation-rating-resolver`, `enhanced-records-list`, `prompt-template-manager`, `request-reviewer`, `response-reviewer` | various | no — latent, will break on the day they deploy |
99+
100+
**The entire Augment-from-DB flow is down on prod**, not just Org Workbench —
101+
`org-workbench`, `search-and-add`, and `search-results` are the three services
102+
that flow comprises, and all three share the bug.
103+
104+
Two nuances worth recording:
105+
106+
- **`corpora-curator` works by luck of file layout.** Its `App.svelte:9` has the
107+
same dead hardcoded constant, but its *real* client lives in
108+
`src/curation.svelte.ts:21-23`, which does read `PUBLIC_WS_URL`. The unused
109+
constant in `App.svelte` is a live trap for the next person who wires a socket
110+
there.
111+
- **`chat` is the reference implementation.** `apps/chat/src/App.svelte:16-20`
112+
gets it exactly right, comment included.
113+
114+
This is a *different* axis from [[Move-Remaining-Remotes-To-Remote-Hosting-Prod-Falls-Back-To-Localhost]].
115+
That issue is about where the shell fetches each remote's **`remoteEntry.js`
116+
asset**. This one is about where an already-loaded remote opens its **data
117+
socket**. Org Workbench proves they are independent: its asset *is* properly
118+
hosted on Railway and loads fine — then it dials localhost for data.
119+
120+
## Why the symptom reads as "no data" rather than "error"
121+
122+
`apps/org-workbench/src/App.svelte:38` models the socket as:
123+
124+
```ts
125+
let status = $state<'connecting' | 'open' | 'closed' | 'error' | 'auth_required'>('connecting');
126+
```
127+
128+
The `closed` badge in the corner is that state, faithfully rendered. But the
129+
main pane does not branch on it — it keeps showing the neutral instructional
130+
copy, *"Pick an organization from the coverage roster on the left (fewest corpus
131+
items first), or search above…"*, inviting the operator to use a roster that can
132+
never populate. The UI tells the truth in a 60px pill and lies in the 1200px
133+
region next to it. See [[Live-Not-Live-Indicator-Tooling-And-Cross-Service-Error-Surfacing]]
134+
and [[No-User-Visibility-Into-State-Needs-A-State-Inspector]].
135+
136+
## The fix
137+
138+
1. **Replace the constant in all sixteen apps** with the env-reading form. This
139+
is mechanical and identical everywhere; `chat` is the template to copy.
140+
2. **Delete the dead constant** in `corpora-curator/src/App.svelte:9` so it
141+
cannot be picked up by accident.
142+
3. **Set `PUBLIC_WS_URL=wss://ws.augment.didi.sh/ws`** on the `org-workbench`,
143+
`search-and-add`, and `search-results` Railway services, then **rebuild**
144+
`PUBLIC_*` is baked at build time, so `railway redeploy --service <name>
145+
--from-source` is required. A restart will not do it.
146+
4. **Make the empty state honest** — when `status` is `closed` / `error`, the
147+
main pane should say the connection failed, not invite a roster pick.
148+
5. **Guard it so it cannot regress.** Two cheap options: extend
149+
`scripts/design-drift.mjs` (or add a sibling lint) with a rule banning
150+
literal `ws://localhost` outside a fallback expression, or assert on it in
151+
the test harness named in [[No-Test-Coverage-TDD-Deferred-Despite-Agentic-Fit]].
152+
Without a guard this returns the next time a remote is scaffolded by copy-paste.
153+
154+
## Suggested verification
155+
156+
Per the browser-drive discipline in `CLAUDE.md`, the click-path is: load
157+
`https://augment.didi.sh`, sign in, switch workspace to **Reach Edu**, open the
158+
**Org Workbench** flow, and assert the connection pill reads `open` and the
159+
coverage roster renders ≥1 organization. That drive currently fails at the pill
160+
and is the regression test for this fix.
161+
162+
## Related
163+
164+
- [[Move-Remaining-Remotes-To-Remote-Hosting-Prod-Falls-Back-To-Localhost]] — sibling deployment defect, different axis
165+
- [[Search-And-Add-Invokes-Never-Reach-The-Workspace]] — same flow; worth re-checking whether its prod symptom is actually *this*
166+
- [[Domain-Type-Is-Ambient-State-So-A-Failed-Workspace-Load-Hides-Every-Corpus]] — the same failure-hidden-behind-a-neutral-empty-state shape
167+
- [[A-Failed-Deploy-Is-Silent-Nothing-Watches-Production-After-Merge]] — why this survived undetected on prod
168+
- [[Live-Not-Live-Indicator-Tooling-And-Cross-Service-Error-Surfacing]]
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
---
2+
title: "Tokens landed, components didn't — the UI needs an overhaul, and the drift linter can't see the problem"
3+
lede: "19 of 20 apps consume the theme package; shared-ui ships exactly two components. Every remote hand-rolls its own buttons, pills, and empty states against shared colours."
4+
date_created: 2026-08-21
5+
date_modified: 2026-08-21
6+
authors:
7+
- Michael Staton
8+
augmented_with:
9+
- Claude Code on Claude Opus 5
10+
semantic_version: 0.0.0.1
11+
tags:
12+
- Issue
13+
- Augment-It
14+
- Design-System
15+
- Component-Library
16+
- Usability
17+
- Microfrontends
18+
- Org-Workbench
19+
- Theme-System
20+
status: Open · Diagnosed · Scoped from a live prod screenshot + drift audit
21+
site_uuid: e862574a-3e21-4ee3-b3f7-bf99ce83f28f
22+
hex_code: 03hrzx
23+
date_authored_initial_draft: 2026-08-21
24+
date_authored_current_draft: 2026-08-21
25+
publish: true
26+
---
27+
28+
# Tokens landed, components didn't
29+
30+
## Why Care?
31+
32+
[[No-Component-Library-UI-Improvised-Not-Component-Based]] was jotted on
33+
2026-07-24 and still reads `Open · Jotted`. This issue is not a restatement of
34+
it — it is the **measurement** that closes the diagnosis, taken a month later
35+
against the live prod surface, plus the finding that our automated design
36+
guardrail is structurally blind to the actual defect.
37+
38+
The short version: **the token half of the design system shipped and the
39+
component half did not.** We now have the worst configuration of the two —
40+
enough shared infrastructure to believe the problem is handled, not enough to
41+
make any two surfaces look related.
42+
43+
## The measurement
44+
45+
| Signal | Value |
46+
|---|---|
47+
| Apps depending on `@augment-it/theme` | **19 of 20** |
48+
| Components exported by `packages/shared-ui/src` | **2**`ConfidencePill.svelte`, `ToggleHeader__PromptOrPackage--Icons.svelte` |
49+
| Apps importing anything from `shared-ui` | **3** (`pack-runner`, `prompt-template-manager`, `response-reviewer`) + `shell` |
50+
| `node scripts/design-drift.mjs` | **99 fail · 0 warn**, across **16 apps** |
51+
| Contrast pairs | **30/30 pass** |
52+
53+
Drift failures by rule:
54+
55+
| Count | Rule |
56+
|---|---|
57+
| 53 | `F8` hardcoded hex colour outside `packages/theme` |
58+
| 17 | `F6` no `DESIGN.md` at member root |
59+
| 22 | `F4` raw `z-index` (values 1, 2, 5, 10, 15, 20, 50, 90, 100, 200) |
60+
| 6 | `F8` hardcoded `box-shadow` outside `packages/theme` |
61+
| 1 | `P2` tier-2 token `--font-mono` missing in light vibrant |
62+
63+
The `z-index` spread is its own small horror: ten distinct raw values competing
64+
across remotes with no shared stacking contract. That is a layering bug waiting
65+
for the first overlay that needs to sit above a `200`.
66+
67+
## The finding that matters most: the linter is blind to this
68+
69+
**`org-workbench` produces zero drift failures.** It is token-clean — no
70+
hardcoded hex, no raw z-index, nothing. And it is the exact surface in the
71+
screenshot that reads as improvised.
72+
73+
That is the whole problem in one data point. `design-drift.mjs` checks
74+
*values* — is this colour a token, is this z-index a token, is there a
75+
`DESIGN.md`. It cannot check *form*: whether a button is the same shape,
76+
height, radius, and weight as the button on the surface next to it. A remote
77+
can pass every rule we have and still invent its own visual dialect, because
78+
nothing in the toolchain has an opinion about components.
79+
80+
So the 99 failures are real and worth fixing, but closing all 99 would **not**
81+
fix the screenshot. We would have 16 apps hand-rolling divergent components out
82+
of perfectly compliant tokens.
83+
84+
## What the prod screenshot actually shows
85+
86+
From Org Workbench on `augment.didi.sh`, reach-edu workspace:
87+
88+
- **Three button dialects in one 900px row**`+ New organization` (flat, dark,
89+
square-ish), `📋 Relevance brief` (lighter fill, different radius, emoji
90+
glyph), `◀ orgs` (third fill, third radius, arrow glyph).
91+
- **A `closed` status pill** floating unanchored in the top-right, overlapping
92+
the header's baseline rather than sitting in a defined status slot.
93+
- **A dead 900px void** below the intro copy — no empty state, no skeleton, no
94+
error surface. (The *reason* it is empty is
95+
[[Every-Remote-Hardcodes-The-Workspace-WS-To-Localhost-So-Prod-Loads-No-Data]];
96+
the fact that emptiness renders as an unstyled void is this issue.)
97+
- **A crowded, mixed-metaphor header** — monospace `augment-it · shell`,
98+
underlined `FLOW`, a numbered pill, chat/queue/Developers/account/Dark/Reach
99+
Edu controls in at least four different shapes and three different border
100+
treatments.
101+
- **Chat rail content vertically centred** in a tall column, so the prompt
102+
hint floats mid-void with no visual anchor.
103+
104+
Notably the *colours* are fine — dark ground, purple accent, readable text,
105+
30/30 contrast pairs passing. It is the **shapes, spacing, and states** that
106+
have no shared grammar. Which is precisely what tokens-without-components
107+
predicts.
108+
109+
## Why it went this way
110+
111+
The honest account is in [[No-Component-Library-UI-Improvised-Not-Component-Based]]
112+
and holds up: remotes were built fast, independently, each solving its own UI
113+
in isolation, and Module Federation made that independence frictionless. The
114+
theme package was the cheap win — a CSS import and a dependency line — so it
115+
propagated to 19 apps. A component library is the expensive win, because it
116+
requires agreeing on an API and then *migrating* sixteen call sites. It stalled
117+
at two components.
118+
119+
This is also the tail of the same pressure recorded in
120+
[[Refactoring-for-API-Speed]] and [[No-Test-Coverage-TDD-Deferred-Despite-Agentic-Fit]]:
121+
infrastructure that is one import away lands; infrastructure that requires
122+
coordinated migration does not.
123+
124+
## What an overhaul should actually do
125+
126+
Sequenced so each step is shippable on its own:
127+
128+
1. **Name the primitives.** From an audit of what the 16 remotes already
129+
hand-roll, the recurring set is roughly: `Button` (primary/secondary/ghost),
130+
`Input`, `Pill` / `Badge` (incl. connection status), `Card`, `EmptyState`,
131+
`ErrorState`, `Skeleton`, `Toolbar`. Ratify that list before writing any of
132+
it.
133+
2. **Fix the stacking contract first** — it is the cheapest high-leverage fix.
134+
Define `--z-*` tokens covering the ten values in use and convert all 22
135+
raw `z-index` sites.
136+
3. **Build the primitives in `packages/shared-ui`**, matching the two existing
137+
components' conventions so `ConfidencePill` does not become an orphan
138+
dialect.
139+
4. **Migrate surface by surface, most-visible first** — Org Workbench, then
140+
`search-and-add` / `search-results` (the rest of the Augment-from-DB flow),
141+
then corpora-curator and chat. Each migration is one PR and one changelog
142+
entry.
143+
5. **Standardise the connection-status slot** as part of step 4 — every remote
144+
has the same `'connecting' | 'open' | 'closed' | 'error' | 'auth_required'`
145+
state and each renders it differently, or (per the sibling issue) not
146+
meaningfully at all.
147+
6. **Retire the 53 hardcoded hex values** as a by-product of migration rather
148+
than as a separate sweep — most of them live in components that are about to
149+
be replaced.
150+
7. **Teach the linter about form.** Add a rule that flags a remote defining its
151+
own `button` / `input` / pill styling when a `shared-ui` primitive exists.
152+
Without this, step 4 decays exactly the way the theme rollout did.
153+
8. **`DESIGN.md` per member** — 17 apps lack one. Cheap, and the
154+
`maintain-design-md` skill already specifies the shape. Do it last; it
155+
documents the outcome rather than driving it.
156+
157+
## Open questions for the operator
158+
159+
- **Is this an overhaul or a rebuild?** The steps above are incremental and
160+
preserve every surface. A genuine visual redesign — new layout language, new
161+
header, new information density — is a different and larger piece of work.
162+
The screenshot's header crowding hints you may want the latter.
163+
- **Does the shell header get redesigned separately?** It is the one surface
164+
every flow inherits, and [[Header-Polish-Flow-Label-Chat-Toggle-Placement-Shell-Suffix]]
165+
already has scope on it.
166+
- **Should this supersede [[No-Component-Library-UI-Improvised-Not-Component-Based]]**,
167+
or sit under it as the measured follow-up? Recommend the latter — that issue
168+
holds the origin story, this one holds the numbers and the plan.
169+
170+
## Related
171+
172+
- [[No-Component-Library-UI-Improvised-Not-Component-Based]] — the 2026-07-24 admission this measures
173+
- [[Every-Remote-Hardcodes-The-Workspace-WS-To-Localhost-So-Prod-Loads-No-Data]] — why the screenshot's main pane is empty
174+
- [[Org-Workbench-Narrow-Layout-Roster-Doesnt-Collapse-Card-Contents-Spill]] — a layout symptom of the same absence
175+
- [[Header-Polish-Flow-Label-Chat-Toggle-Placement-Shell-Suffix]]
176+
- [[Live-Not-Live-Indicator-Tooling-And-Cross-Service-Error-Surfacing]] — the status-slot half

0 commit comments

Comments
 (0)