@monster-orange/den 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +1547 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +507 -4
  5. package/make/den.mk +19 -4
  6. package/make/journeys.mk +2 -1
  7. package/make/local_dev.mk +6 -13
  8. package/make/secrets-1password.mk +18 -0
  9. package/make/test.mk +1 -1
  10. package/package.json +9 -2
  11. package/schemas/den.plugin.v1.json +156 -0
  12. package/schemas/den.theme.v1.json +269 -0
  13. package/scripts/auth-configure.ts +16 -8
  14. package/scripts/heal-config.ts +0 -5
  15. package/scripts/setup-1password.ts +41 -19
  16. package/scripts/setup-secrets.ts +67 -101
  17. package/src/pillars/api/global-setup.ts +1 -1
  18. package/src/pillars/journeys/sandbox.ts +23 -18
  19. package/src/pillars/journeys/tools/browser-session.ts +2 -2
  20. package/src/scripts/setup-default-workspace.ts +2 -2
  21. package/src/shared/auth/headless-login.ts +1 -1
  22. package/src/shared/auth/sudo-vault.ts +93 -31
  23. package/src/shared/auth/token-providers/index.ts +7 -3
  24. package/src/shared/brand.ts +7 -0
  25. package/src/shared/config-error.ts +33 -3
  26. package/src/shared/constants.ts +3 -24
  27. package/src/shared/den-config.ts +141 -5
  28. package/src/shared/edge/config.ts +147 -21
  29. package/src/shared/edge/exec.ts +31 -0
  30. package/src/shared/edge/included.ts +169 -0
  31. package/src/shared/edge/render.ts +69 -14
  32. package/src/shared/edge/runtime.ts +128 -121
  33. package/src/shared/edge/status.ts +68 -8
  34. package/src/shared/edge/tls.ts +328 -0
  35. package/src/shared/edge/types.ts +25 -9
  36. package/src/shared/environment-manager/default-workspace.ts +16 -1
  37. package/src/shared/environment-manager/health.ts +73 -38
  38. package/src/shared/environment-manager/scope.ts +168 -0
  39. package/src/shared/environment-manager/service-runner.ts +789 -43
  40. package/src/shared/environment-manager/up-targets.ts +132 -0
  41. package/src/shared/health/rollup.ts +48 -0
  42. package/src/shared/open-url.ts +21 -2
  43. package/src/shared/org-sources.ts +78 -59
  44. package/src/shared/org.ts +53 -5
  45. package/src/shared/plugins/manifest.ts +406 -0
  46. package/src/shared/plugins/store.ts +358 -0
  47. package/src/shared/process/legacy-adopt.ts +285 -0
  48. package/src/shared/process/process-group.ts +202 -0
  49. package/src/shared/process/spawn-streaming.ts +7 -0
  50. package/src/shared/secrets/doppler.ts +1 -1
  51. package/src/shared/secrets/index.ts +279 -50
  52. package/src/shared/secrets/none.ts +56 -0
  53. package/src/shared/secrets/strategies.ts +202 -0
  54. package/src/shared/secrets/types.ts +1 -1
  55. package/src/shared/smoke/git-remote.ts +40 -0
  56. package/src/shared/stacks/manifest-commands.ts +1 -1
  57. package/src/shared/stacks/namespace.ts +178 -0
  58. package/src/shared/stacks/owners.ts +303 -0
  59. package/src/shared/stacks/render.ts +8 -3
  60. package/src/shared/stacks/runtime.ts +100 -16
  61. package/src/shared/stacks/status.ts +63 -12
  62. package/src/shared/strategies/registry.ts +89 -0
  63. package/src/shared/theme/contract.ts +538 -0
  64. package/src/shared/types/index.ts +12 -3
  65. package/src/ui/cli/create.ts +24 -7
  66. package/src/ui/cli/doctor-command.ts +131 -0
  67. package/src/ui/cli/edge-command.ts +56 -10
  68. package/src/ui/cli/flag-parser.ts +7 -2
  69. package/src/ui/cli/health-display.ts +25 -0
  70. package/src/ui/cli/index.ts +548 -60
  71. package/src/ui/cli/notice.ts +20 -0
  72. package/src/ui/cli/plugin-command.ts +849 -0
  73. package/src/ui/cli/stack-command.ts +63 -2
  74. package/src/ui/cli/test-runner.ts +11 -9
  75. package/src/ui/cli/theme-command.ts +136 -0
  76. package/src/ui/cli/web-command.ts +47 -23
  77. package/src/ui/mcp/api.ts +28 -2
  78. package/src/ui/mcp/scope.ts +22 -1
  79. package/src/ui/mcp/tools.ts +50 -7
  80. package/src/ui/server/appearance-prefs.ts +95 -0
  81. package/src/ui/server/daemon-reaper.ts +29 -1
  82. package/src/ui/server/engine-stamp.ts +83 -0
  83. package/src/ui/server/github-service.ts +23 -44
  84. package/src/ui/server/graph-page.ts +20 -0
  85. package/src/ui/server/graph-prefs.ts +44 -1
  86. package/src/ui/server/http-errors.ts +293 -0
  87. package/src/ui/server/http-helpers.ts +22 -3
  88. package/src/ui/server/index.ts +532 -51
  89. package/src/ui/server/public/appearance.js +531 -0
  90. package/src/ui/server/public/edge-page.js +248 -0
  91. package/src/ui/server/public/edge.html +251 -0
  92. package/src/ui/server/public/environments.html +65 -83
  93. package/src/ui/server/public/error-page.js +247 -0
  94. package/src/ui/server/public/error.html +260 -0
  95. package/src/ui/server/public/graph-model.js +332 -14
  96. package/src/ui/server/public/graph.css +141 -47
  97. package/src/ui/server/public/graph.js +223 -44
  98. package/src/ui/server/public/header.js +396 -600
  99. package/src/ui/server/public/health-fit.js +248 -0
  100. package/src/ui/server/public/health-rollup.js +128 -0
  101. package/src/ui/server/public/i18n/es.js +287 -0
  102. package/src/ui/server/public/i18n/ja.js +276 -0
  103. package/src/ui/server/public/index.html +139 -438
  104. package/src/ui/server/public/local-dev.html +3468 -4044
  105. package/src/ui/server/public/logs.html +71 -208
  106. package/src/ui/server/public/panel.js +14 -2
  107. package/src/ui/server/public/release.html +175 -389
  108. package/src/ui/server/public/services.html +596 -770
  109. package/src/ui/server/public/settings-page.js +759 -0
  110. package/src/ui/server/public/settings-registry.js +521 -0
  111. package/src/ui/server/public/settings.html +590 -0
  112. package/src/ui/server/public/shell.js +3869 -0
  113. package/src/ui/server/public/topbar.js +1413 -0
  114. package/src/ui/server/public/version-label.js +12 -0
  115. package/src/ui/server/public/workspace-picker.js +74 -34
  116. package/src/ui/server/readonly-routes.ts +12 -1
  117. package/src/ui/server/services-list.ts +94 -17
  118. package/src/ui/server/settings-distribution.ts +74 -0
  119. package/src/ui/server/shell-prefs.ts +192 -0
  120. package/src/ui/server/theme-routes.ts +292 -0
  121. package/src/ui/server/theme-runtime.ts +407 -0
  122. package/src/ui/server/ui-assets.ts +16 -1
  123. package/src/ui/server/ui-language.ts +50 -0
  124. package/src/ui/server/workspace-routes.ts +45 -3
  125. package/themes/den/den-theme.json +72 -0
  126. package/themes/den/favicon.ico +0 -0
  127. package/themes/den/mark.svg +6 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,1553 @@ All notable changes to Den are documented here. Each release lists the
4
4
  config and manifest changes a distribution must make to move to it under
5
5
  **Migrations**.
6
6
 
7
+ ## [0.3.0] - 2026-11-06
8
+
9
+ 0.3.0-dev.31 and later include all of 0.2.7 (below).
10
+
11
+ ### Added
12
+
13
+ - **R26a's 422 page: "Config is invalid"** (0.3.0-dev.40). A page
14
+ navigation with a config that fails validation gets the shell frame
15
+ with a table of each problem (file, key path, message), the command
16
+ that shows them in a terminal (`<cli> validate`), "Reload once fixed",
17
+ and links to Logs and Settings, in en, ja and es. The rail stays
18
+ usable: `/settings` and `/logs` load whatever the config holds, and so
19
+ do the routes the shell and those pages need (`/api/settings`,
20
+ `/api/themes`, `/api/scope`, `/api/workspaces/settings`,
21
+ `/api/sudo-password/status`, `/api/environments` (dev.41), besides `/api/server-info`,
22
+ `/api/server-log`, `/api/changelog` and `/api/web/restart`). Any other
23
+ page answers 422 with this page.
24
+ - Org config validation lists every problem, not the first: a
25
+ ConfigError carries `issues` (`{ file, key, message }`), its message is
26
+ one line per issue as each always read, and the 422 JSON body gains
27
+ `issues`.
28
+ - **`up --dry-run` says what up will do about a pre-dev.37 record or a
29
+ held port** (0.3.0-dev.40, a consumer's dev.39 nit; it said only "Would
30
+ up"): "would adopt pid N (port P), nothing started", "would drop the
31
+ record (nothing running), then start it", or "port P held outside the
32
+ repo: would not start" (exit 1, as the real up). It signals nothing and
33
+ writes no record.
34
+ - Everything in 0.2.8 (scoped MCP for agents, orgs that own their sources,
35
+ config validation that stops every command) is in 0.3.0.
36
+ - **The UI in Japanese and Spanish** (0.3.0-dev.39, R19; R10's and R21's
37
+ Language).
38
+ - Catalogs for en, ja and es. en is shell.js's `MESSAGES.en`, the
39
+ fallback; ja and es are `public/i18n/<lang>.js`, which the server
40
+ loads before shell.js for a page in that language (with
41
+ `<html lang>`), so the first paint is already in it. Every lookup is
42
+ `t(key, vars)`; a `key.one` (any CLDR category) is the plural form
43
+ `Intl.PluralRules` picks for `n`, and numbers go through
44
+ `Intl.NumberFormat`.
45
+ - The language is a user pref, `language:` in `config/local/user.yml`
46
+ (absent: English), read and written by `/api/settings` (`ui_languages`
47
+ lists the choices; anything else is a 400). The user menu has a
48
+ Language section, each language named in itself, Settings → Appearance
49
+ a Language row, and ⌘K a "Language: …" command; a pick reloads the
50
+ page in it.
51
+ - The whole shell is converted: rail, top bar and scope menus, status
52
+ chip and drawer, ⌘K palette (its Settings rows too), Runs chip and
53
+ dock, lock popover, user menu, What's new, the stale-server pill and
54
+ restart overlay, the engine and network banners, the error toasts and
55
+ the 404 and 500 pages. The pages' own content (Workspaces, Graph,
56
+ Services, Logs, Tests, Environments, Releases, Edge and the Settings
57
+ page's rows) is still English: they move to `t()` page by page.
58
+ - Never translated: names (services, workspaces, repos, themes,
59
+ languages), logs, command output, run labels, and "Copy details"
60
+ (it is for a report, so it stays English).
61
+ - Guards: `i18n.test.ts` fails when any catalog lacks a key en has, has
62
+ one en doesn't, drops a placeholder, or uses a plural form its
63
+ language doesn't; when the code looks up a key en lacks; and when a
64
+ node-side `EN_FALLBACK` drifts from en. `i18n-shell.browser.test.ts`
65
+ draws the shell in a pseudo-locale with every overlay open and fails
66
+ on any word that skipped `t()`.
67
+ - English shows exactly what it did, apart from the new Language
68
+ controls.
69
+
70
+ - **Errors people can see and act on** (0.3.0-dev.38, R26, Ward QA
71
+ 2026-11-05: "we don't handle 500 or 404s from the den server with
72
+ anything remotely obvious ... just text").
73
+ - A page request that fails gets the shell frame (rail, top bar, status
74
+ chip) around an error page (`error.html`, `error-page.js`). 404 "Page
75
+ not found" shows the path, "Did you mean …" from the command palette's
76
+ own matcher (`rankCommands`), links to Workspaces and Search (⌘K) and
77
+ the list of pages; a path under `/plugins/<id>/` says which plugin it
78
+ belongs to and that it isn't installed. 500 "Something went wrong"
79
+ shows the method and route template (never query values), the time,
80
+ a request id, Retry and Copy details, and no stack: the server log
81
+ has it under the same request id (`✗ GET /route failed (req
82
+ <id>): …`). Assets and non-browser requests keep a plain 404.
83
+ - Every `/api/*` error is JSON, one shape:
84
+ `{ ok: false, error, code, requestId }` (`code`: `not_found`,
85
+ `bad_request`, `internal`, `method_not_allowed`, `engine_stale`, …). It
86
+ holds at the response for every route, whatever the route sends, and a
87
+ route's own fields (`runCmd`, …) are kept. Every response carries
88
+ `X-Request-Id`; a failed request's log line carries the id.
89
+ - `denShell.api(method, url, body?)`: the one client path for a call. It
90
+ resolves with the data or throws a typed error (`DenApiError`: status,
91
+ code, requestId, network); one no page catches becomes a toast with
92
+ the message, the request id, Retry and Copy details
93
+ (`denShell.errorToast`). The shell's own calls, the scope and focus
94
+ menus, Services' workspace panel, the Workspaces page's add, create,
95
+ delete, rename, auto-start, mode and open-in-editor/terminal actions,
96
+ Releases' config save and deploy start, and the log search use it.
97
+ - When fetches fail at the network level (the server stopped or is
98
+ restarting), a banner says "Den's server isn't responding — retrying",
99
+ retries with backoff (1 s doubling to 15 s, or Retry now), and clears
100
+ itself when the server answers; meanwhile the status chip reads
101
+ "Status unknown", never a stale green.
102
+ - **A server older than the engine on disk says so** (0.3.0-dev.38, MD-2).
103
+ After a pin upgrades Den under a running web server, the server keeps the
104
+ old engine in memory while serving the new pages, so their new routes
105
+ 404. The server records the engine's version and build id (a hash of its
106
+ package.json) at startup and compares them with the engine on disk on
107
+ page requests (cached for 3 s). On a mismatch every page leads with a
108
+ banner, "Den was updated to <new>. This server is still running <old>.
109
+ Restart it: `<cli> web restart` (or your service restart).", an unknown
110
+ route answers that message (`/api`: code `engine_stale` with `loaded`
111
+ and `onDisk`) instead of a 404, and a failed MCP tool call names both
112
+ versions and the restart. `/api/server-info` gains `loaded` and `onDisk`
113
+ (`{ version, buildId }`) and `engineStale`; its `version` is now the
114
+ engine the server runs.
115
+ - **DOC-1: the README's screenshots, generated** (0.3.0-dev.40; `npm run docs:shots`,
116
+ `scripts/doc-shots.mjs`). Runs Den on a scratch copy of
117
+ `examples/distribution` with the `examples/demo` repos as its workspace (a
118
+ docker shim, a random localhost port, a throwaway HOME, and a server env
119
+ built from HOME, PATH and the DEN_* it needs, never the caller's), and
120
+ writes to `docs/images/<version>/` (package.json's version without the
121
+ prerelease suffix): `hero-dark.png` and `hero-light.png` (the Graph page,
122
+ 2x), `social-preview.png`, `workspaces.png`, `graph.png`,
123
+ `status-drawer.png`, `edge.png`, `runs-dock.png`, the `make up`
124
+ animation (`up.gif` with ffmpeg, else `up.webp` with img2webp, else
125
+ skipped with a message) and `alt.json`, the alt text. Size budgets: hero
126
+ 400 KB a theme, the rest 200 KB, the animation 5 MB; over budget, the run
127
+ fails. Nothing reaches the tree unless every shot passes.
128
+ - The capture guard (`scripts/doc-shots-guard.js`): before each shot, the
129
+ page's innerText, every title, alt, aria-label and placeholder, and SVG
130
+ text go through the org-name gate's own matcher, now shared
131
+ (`scripts/org-names.js`, used by `org-name-gate.mjs` too). A hit aborts
132
+ the run naming the shot and the term, never the text around it, and
133
+ writes no image; so does a path under the real HOME. Without
134
+ DEN_ORG_NAMES or DEN_ORG_NAMES_FILE the script refuses (exit 2);
135
+ `--unguarded` is for local layout work only.
136
+ - The images stay out of the npm package: `files` has `!docs/images/`,
137
+ and dist-smoke fails on any image in the tarball.
138
+ - **`npm run docs:links`** (`scripts/docs-links.js`) checks that every
139
+ relative link in README.md and docs/ reaches a file and, with a
140
+ `#fragment`, a heading.
141
+
142
+ - **`stack_namespace`: a workspace can choose its stacks' Compose
143
+ namespace** (0.3.0-dev.36, Ward 2026-11-05). By default nothing changes:
144
+ a stack runs as Compose project `<stack>-<workspace>` (`default`:
145
+ `<stack>-main`), and so do its containers, volumes and networks. A
146
+ workspace can opt out with `stack_namespace: <slug>`, which replaces that
147
+ suffix: several workspaces can then share one namespace, and a stack
148
+ keeps its data when its repo moves to another workspace.
149
+ - Where: `workspaces.<ws>.stack_namespace` in
150
+ `config/local/workspaces.yml`, or `stack_namespace` on a blueprint
151
+ workspace (`defaults.workspaces.<n>`, next to `org`). First match wins:
152
+ workspaces.yml, the blueprint, then the workspace's own name.
153
+ `workspace seed` carries the blueprint's value into the workspace it
154
+ seeds. workspaces.yml overriding a different blueprint value is a
155
+ warning naming both, as for `org`. The value is lowercase letters,
156
+ digits and dashes; anything else is a config error naming the key.
157
+ - One resolver: every stack path (`stack` commands, `/api/stacks`,
158
+ Den's in-process compose health checks, a stack service's container
159
+ lookup, `workspace deactivate`'s running-stacks note) turns a workspace
160
+ into its suffix through `resolveStackWorkspaces`.
161
+ - A shared namespace means a shared instance, keyed by checkout. Two
162
+ workspaces bringing up the same stack from the same checkout (repo
163
+ path and branch) share one project: `stack down` (and `down <repo>`)
164
+ from one while another active workspace in the namespace still holds
165
+ it detaches instead of stopping it; the last one stops it
166
+ (`stack down --force` stops it regardless). A workspace skipping the
167
+ stack as already healthy is recorded as an owner too. The owners are
168
+ kept in `owners.json` in the project's edge state directory, and
169
+ re-derived on every decision (active, same namespace, still holds the
170
+ stack), like Den's own service refcount. Bringing it up from a
171
+ different checkout or branch is refused (`ServiceConflictError`,
172
+ `identity_mismatch`), naming both checkouts and the namespace: Den
173
+ never swaps the containers under another workspace.
174
+ - Visible: `workspace list` gains a Stacks column when a workspace's
175
+ namespace is not its default; the Workspaces page header shows a
176
+ "stacks: <ns>" chip; `GET /api/workspaces` adds `stackNamespaces`, each
177
+ workspace's resolved `stack_namespace`, its `source` (`workspace`,
178
+ `blueprint` or `default`), its `default` and whether it `differs`.
179
+ - `DEN_STACK_WORKSPACE` still wins over all of it, for that one process:
180
+ a per-command override (it also bypasses the owner refcount), not a
181
+ migration tool. To move a workspace's stacks onto another namespace
182
+ for good, set `stack_namespace`.
183
+ - **The graph's edges flow when a card is selected, and stay on top**
184
+ (0.3.0-dev.35, R22b, Ward's QA of dev.34). Selecting a card only
185
+ thickened its edges, and the edge layer painted under the group frames,
186
+ repo frames and cards, so an opaque frame hid an edge or an arrowhead
187
+ that crossed it (dev.34's routed cross-group edges too). The edge layer
188
+ now paints above the frames and cards, still taking no pointer events
189
+ (cards under an edge stay clickable). A highlighted edge carries a
190
+ marching-dash overlay, in its kind's colour, moving from the dependent
191
+ to its dependency (data-f to data-t) at about one cycle a second: a
192
+ selected card's dependencies flow out of it and its dependents flow in.
193
+ Under reduced motion nothing moves: a highlighted edge is drawn thicker
194
+ in the accent colour.
195
+ - **Workspaces: Local Dev is an ops page** (0.3.0-dev.35, R24a–e, Ward
196
+ 2026-11-05: "the UX is a bit of a kitchen sink"). One job: run your
197
+ workspaces.
198
+ - R24a: the rail item, the page title and its header read
199
+ **Workspaces** (same icon and place). `/` and `/local-dev` still serve
200
+ it; the palette finds it as "Local Dev" too. The nav label key is
201
+ `n.workspaces`.
202
+ - R24b: the "⚙ Settings" footer under the workspace list is gone; "+ new
203
+ workspace" stays.
204
+ - R24c: the selected workspace's header adds **Open in Graph →**
205
+ (`/graph?ws=<name>`) and a ⋯ menu: Workspace settings
206
+ (`/settings?ws=<name>#workspace`, the old "Settings ↗" tab), Rename,
207
+ Delete.
208
+ - R24d: **one repo table** replaces the Graph, Applications and Config
209
+ tabs. Per repo: the health dot and rollup ("2/2 healthy", or the
210
+ failing check), the branch, the mode with "use local / use managed",
211
+ the last run's result and time (from the runs dock), Start, Stop,
212
+ Restart, Logs (in the dock) and ⋯ (terminal, editor, auto-start,
213
+ clone, validate, remove). A repo with several services expands to a
214
+ sub-row each (keyboard: the ▸ button, `aria-expanded`). Problems sort
215
+ first, then the startup order, with the fix inline: the status
216
+ drawer's text and Start/Logs buttons, from one source
217
+ (`topbar.js problemFix`). **Group by** None, Application (as `den
218
+ apps`, each application's Up/Restart/Down in its row) or Org, a user
219
+ pref (`graph.groupBy`). **Find** uses the Graph page's matcher. "+ Add
220
+ repo" sits under the table; "Validate a manifest" folds under it. The
221
+ empty state is unchanged. A real `<table>` with `th scope`; every
222
+ action button is named with its repo or service.
223
+ - R24e: on Workspaces the runs dock shows the selected workspace's runs
224
+ (a run belongs to every workspace that has its repo; one with no repo,
225
+ such as Sync, always shows), with an **All runs** toggle in the bar.
226
+ `denShell.runs.scope({ ws })`, and `runs.add(…, { ws })` tags a run.
227
+ - Not changed: the Graph page, Services, Settings and every API; no
228
+ consumer config needs a change.
229
+
230
+ - **The Graph page** (0.3.0-dev.32, R22, Ward 2026-11-05). Develop ›
231
+ Graph, after Local Dev, at `/graph`: the workspace graph alone, full
232
+ width under its page header, with the same toolbar, zoom, Maximize
233
+ (which now hides the page header too) and details panel as Local Dev's
234
+ Graph tab. It shows the workspace Local Dev would (`?ws=`, else the
235
+ view scope when it is one workspace, else the focus). Local Dev keeps
236
+ the workspace list, the workspace's actions and its tabs, Graph tab
237
+ included. Overview and Security stay out for now.
238
+ - **A page header on every page** (0.3.0-dev.32, R20). The shell draws the
239
+ prototype's `.ph` on each page that has no header of its own: the page's
240
+ title (`600 22px/1.2`, its one h1, which used to be visually hidden) and
241
+ a 13px muted line with the scope, `org / workspace / env`, kept in step
242
+ with the top bar (Local Dev and Graph name the workspace they show). Pages
243
+ keep their content below it. `denShell.pageHeader()` gives a page the
244
+ header's `act` (the primary action, at the end) and `tabs` (`.ptabs`,
245
+ below it), and `setScope` / `setSub` for the line. Local Dev's workspace
246
+ tabs are page tabs now: real tabs (`role="tab"`, `aria-selected`) at the
247
+ prototype's sizes (8px 12px, 14px, a 2px accent rule under the selected
248
+ one).
249
+ - **The stacks' routes on the Edge page and in `edge routes`**
250
+ (0.3.0-dev.32, from a consumer). The routers Den writes into
251
+ `edge/dynamic/stack-*.yml` when a stack comes up are neither Den's host
252
+ routes nor `edge include` files, so nothing listed them. They are listed
253
+ last, source "stack" with the file; `/api/edge` adds `stacks` and
254
+ `stackErrors` (a file that doesn't parse is named, never fatal).
255
+
256
+ - **The runs dock** (0.3.0-dev.30, R16). One output dock for every page, at
257
+ the foot of the content column, ported from the shell prototype: a bar
258
+ with "Runs", the run tabs (12px mono, a dot that pulses while running,
259
+ red when the run failed) and Show/Hide runs; open, a 170px output. The
260
+ Runs chip (R7) shows and hides it and counts the running tabs; an empty
261
+ dock links to the page that ran the last run. The bar carries Copy and
262
+ Clear for the selected run, a tab closes with its × (or Delete), ←/→ move
263
+ between tabs, and the dock's top edge drags to resize it. Pages and
264
+ plugins use one path, `denShell.runs` (`start`, `append`, `finish`, `set`,
265
+ `select`, `clear`, `remove`, `show`/`hide`/`toggle`, `list`, `onChange`);
266
+ a run started there gets "✓ done in {s}s" when it finishes. Since R16a
267
+ (below) the dock starts collapsed and open/closed is a user pref.
268
+ - **The Edge page** (0.3.0-dev.30, PL-SPEC §10). A built-in page in the
269
+ rail's Ship group, shown once `/api/edge` reports the edge configured
270
+ (`/api/edge?probe=1`, which asks no Docker). Read-only: the container and
271
+ its ports, the TLS strategy (`edge.tls`: none, mkcert or files) with
272
+ where it came from (set, mapped from the legacy keys, or implicit), CA
273
+ trust and the CA's directory, the cert and its names, the domains, and
274
+ every route: Den's own, then the routers in the included dynamic files
275
+ (`edge include`), each marked by its source file.
276
+ - **Room for Local Dev's graph** (0.3.0-dev.30, R23, Ward QA 2026-11-05).
277
+ The workspace list, the output and two toolbar rows left the graph at 40%
278
+ Fit. The workspace list now collapses to a 40px strip (« in its header;
279
+ » brings it back), remembered per user (`graph.workspaces`: `open` or
280
+ `collapsed`, on the Settings page under Graph). A Maximize button in the
281
+ graph's zoom group, or `F` with focus outside a field, hides the list,
282
+ the workspace header, the action row and the tabs; Esc restores. Fit
283
+ measures the whole area, so its zoom rises once maximized.
284
+ - **Included routers in `edge routes` and `/api/edge`** (0.3.0-dev.30,
285
+ PL-SPEC §10, from a consumer review). Routers defined in the distribution's
286
+ included dynamic files are served by Traefik but were never listed. Den
287
+ now reads their `http.routers` (read-only) and lists them after its own
288
+ routes with their source file; `/api/edge` adds `included`,
289
+ `includedErrors` (a file that doesn't parse is named, never fatal) and
290
+ `tls.from` / `tls.ca`. The rendered edge config is unchanged.
291
+
292
+ - **`<cli> relocate <repo> <path>`** (0.3.0-dev.26, from a consumer). A
293
+ checkout moved on disk: every workspace's dev-managed entries for the repo
294
+ (bare and `<repo>/<svc>`) now point at the new path, keeping each entry's
295
+ other settings. Managed entries (Den's own clones) are left alone, and focus
296
+ and the active set never change. It refuses a missing path, one whose
297
+ manifest name or GitHub remote names a different repo, and a repo no
298
+ workspace has. Since 0.3.0-dev.27 (from a consumer's dev.26 run) it also
299
+ refuses a folder where nothing names the repo (an empty or unrelated
300
+ folder): the manifest name, or the GitHub remote against the catalog's
301
+ `gh` (or the repo name when the catalog has none), must match.
302
+ `--force` accepts such a folder. The refusal's `--force` hint keeps
303
+ `--dry-run` and `-w` when given (0.3.0-dev.28). `--dry-run` shows the changes without writing; `-w` limits it
304
+ to one workspace.
305
+ - **`<cli> register <path> -w <workspace>`** (0.3.0-dev.26). `-w` writes that
306
+ workspace, active or not, and leaves focus alone. It used to fail with
307
+ "-w/--workspace is not valid with register". The no-TTY usage line names `-w` too
308
+ (0.3.0-dev.27).
309
+
310
+ - **The "What's new" pop-up shows the newest release only** (0.3.0-dev.16).
311
+ A "Show all versions (N earlier)" button in its footer lists every
312
+ earlier version in the same pop-up, and "Show latest only" collapses it
313
+ again. It always opens collapsed, at the top.
314
+ - **Themes (`den.theme/v1`).** The contract is `schemas/den.theme.v1.json`:
315
+ color tokens for dark and light, type, shape, a mark and self-hosted fonts.
316
+ `<cli> theme add <repo>[/<subpath>]@<tag|commit>` validates a theme and
317
+ vendors it into the distribution (`plugins/<id>/`, `plugins.lock.json`, shared with future plugin kinds). `theme add` never replaces a plugin dir it did not vendor (a local, unlocked plugin) or another kind's lock entry; `theme list` fails on a leftover 0.3.0-dev.0 `themes.lock.json`, naming it; `theme add` refuses a `file://` source (0.3.0-dev.7, below). A theme's and a font's `license` follow one rule: an SPDX expression, never UNLICENSED/NONE. `theme list` reports a theme that fails this Den's rules as "no longer valid" with the reasons, apart from "changed since it was added"; validate explains the license and CSS-value rules instead of printing "must NOT be valid". Local themes (the distribution's own, in `plugins/<id>/` with no lock entry) are listed as `local` and validated; `theme validate` with no argument checks every installed theme; `theme new <id>` starts a local theme from the built-in one;
318
+ `theme list` checks themes against the lock; `theme validate` and `theme
319
+ remove`. Contrast is checked per pair in both modes. A branch ref needs
320
+ `--track`. Den bundles its own theme, `den` 2.0.0. The public set and the
321
+ authoring guide: https://github.com/Monster-Orange/den-themes. See "Themes"
322
+ in `docs/distributions.md`.
323
+ - **The web UI is themed (0.3.0-dev.5).** Every page draws its colors from
324
+ the active theme, in dark or light: the engineer's choice (user.yml
325
+ `appearance: { theme, mode }`, from the header's new Mode and Theme menus),
326
+ else the org config's `brand.theme`, else `den`; an unknown or invalid id falls
327
+ back with one server-log warning. The server serves `/theme.css`,
328
+ `/theme/<id>/<file>` (only the files the manifest names) and `/api/themes`.
329
+ Switching writes user.yml only (no edge, services or restart). No page,
330
+ stylesheet or UI script hard-codes a color any more (a test holds that).
331
+ The header's version pill shows the running build, prerelease suffix
332
+ included (`v0.3.0-dev.5`, not the changelog's `v0.3.0`).
333
+ - **Plugins (`den.plugin/v1`, PL-1a and PL-2a, 0.3.0-dev.7).** The contract
334
+ is `schemas/den.plugin.v1.json`: `id`, `name`, `version`, `license` (the
335
+ theme schema's SPDX rule, by `$ref`), `den` (a version range, required),
336
+ `maintainer`, `readme`, `provides` (one entry per extension point: `theme`
337
+ with its `manifest`, `page` with its `entry` and `nav`, and the other kinds
338
+ by name) and `capabilities` (a closed vocabulary: `ui.nav`, `api.read`,
339
+ `api.actions:`, `secrets:`, `net:`, `exec:`, `send:`). A directory holding
340
+ only a `den-theme.json` is an implicit theme plugin, so every theme keeps
341
+ working. Every `plugins/<id>/` is a plugin: **local** when it has no lock
342
+ entry (the distribution's own, any kind; being present is its approval),
343
+ **added** when `plugin add` vendored it. A `plugins/<id>/` with neither
344
+ manifest is an error in `plugin list` and `plugin validate`, never
345
+ skipped, as are the id `den` and a directory name that is not an id.
346
+ - `<cli> plugin list`: every plugin with its version, kinds and `local`
347
+ or its source and ref; added ones are checked against the lock's hash
348
+ (as `theme list` does).
349
+ - `<cli> plugin validate [dir]`: every local and added plugin, or one dir
350
+ (a plugin, a distribution, or a repo of `themes/<id>/` and
351
+ `plugins/<id>/`). Admission rules: a `maintainer` (1); a browser- or
352
+ server-tier plugin's README names its tier and says what it must never
353
+ do (2; themes are exempt); an SPDX license and licensed fonts (3). A
354
+ local plugin that declares `api.actions`, `secrets`, `exec`, `net` or
355
+ `send` and has files git has not committed gets a warning (never an
356
+ error): committing it is its review.
357
+ - `<cli> plugin add <repo>[/<subpath>]@<tag|commit> [--track] [--yes]`,
358
+ `plugin update <id> @<ref>` and `plugin remove <id>`. The lock entry's
359
+ `kinds` and `approved_capabilities` come from the manifest; a plugin that
360
+ asks for capabilities needs `--yes`, and `update` asks again only when the
361
+ set grows. `remove` and `update` are for added plugins: a local one is
362
+ edited or deleted in git. A den-plugin.json plugin is vendored whole
363
+ (every file but dot-files); a theme still vendors only its named files,
364
+ so existing lock hashes still match.
365
+ - `add` and `update` refuse a `file://` source: "for your own plugin, put
366
+ it in plugins/<id>/ as a local plugin; to try a published one before
367
+ tagging, push a branch and use --track". This replaces 0.3.0-dev.2's
368
+ warning.
369
+ - `plugins.allow` in `den.yml` lists the `<host>/<owner>` pairs (or bare
370
+ hosts) `add` may fetch from. Absent: any host. A malformed value is an
371
+ error.
372
+ - `theme add | list | validate | remove` are `plugin` filtered to kind
373
+ theme, with the same output and exit codes; `theme new` is unchanged.
374
+ The web UI also finds a theme a `den-plugin.json` provides.
375
+ See "Plugins" in `docs/distributions.md`.
376
+ - **PL-SPEC draft-4 rulings (0.3.0-dev.8).**
377
+ - Rule 1 covers themes: a bare `den-theme.json`'s `author` is its
378
+ maintainer, and a theme plugin without one fails `plugin validate`.
379
+ `theme new` sets the author to the brand's name.
380
+ - The `den` range is checked (semver, prereleases included).
381
+ `plugin validate` and `theme validate` fail a plugin whose range
382
+ excludes the running Den, naming the range and the version. At load,
383
+ the web UI skips such a theme, whether the range is in its
384
+ `den-theme.json` or in the `den-plugin.json` that provides it, with one
385
+ server-log notice, and draws with the next choice (yours, then
386
+ `brand.theme`, then `den`). It never crashes. Den now depends on
387
+ `semver` (pinned).
388
+ - `plugin list` and `theme list` warn, never fail, about a lock entry
389
+ with a `file://` source (from 0.3.0-dev.2), and say how to fix it.
390
+ - **`plugin validate <id>` and `theme validate <id>` mean `plugins/<id>/`**
391
+ (0.3.0-dev.8). An id that names a dir in `plugins/` or a lock entry is
392
+ never read as a cwd-relative path. A bare name that isn't an id (such as
393
+ `x`) means `plugins/x/` when no such path exists. Anything else is a path,
394
+ as before. `theme validate den` checks the built-in theme. `theme
395
+ validate <id>` of a plugin that isn't a theme gives one ✗.
396
+ - **The theme reaches the type, the mark and links.** Every page draws text
397
+ in `var(--sans)` / `var(--mono)` / `var(--display)` (no page names a font
398
+ any more; a test holds that, like the color rule), so a theme's fonts
399
+ apply; `/theme.css` ends a theme's stack with Den's when it has no generic
400
+ family. The header shows the active theme's mark (SVG, `svg_dark` in dark
401
+ mode, or text in its `shape`) and redraws it on a live switch; favicon.ico
402
+ stays the favicon. Links in body content are underlined (subtly, in a tint
403
+ of their own color), so a red accent never makes a link read as an error.
404
+ - **Back to the org default.** The Theme menu starts with "Org default
405
+ (<name>)", which stores `appearance.theme: null`, so you keep following
406
+ `brand.theme`. Picking a theme by name stores its id, even the theme that
407
+ is the org default today (0.3.0-dev.11; dev.8 stored null for it): that
408
+ is a pin, selected as itself and labelled "<name> (pinned)".
409
+ `/api/themes` gives `org_default`. The popover's controls are named
410
+ (Mode, Theme; the button is "Appearance" with `aria-expanded` and
411
+ `aria-controls`); Escape closes it and returns focus to the button.
412
+
413
+ - **What fails `validate` never loads (0.3.0-dev.9).** The theme runtime's
414
+ verdict is now `plugin validate`'s for the whole plugin. That covers a bare
415
+ `den-theme.json`'s `author`, a `den-plugin.json`'s `maintainer`, README,
416
+ capabilities and `den` range, and the theme contract. A theme that fails
417
+ is left out of the Theme menu (even when it is your stored choice: you then
418
+ follow the org default) and is skipped at load with one server-log notice.
419
+ The fallback is the same as for the range: yours, then `brand.theme`, then
420
+ `den`. `/api/themes` still lists it, with `ok: false` and the problem.
421
+ - **A prerelease Den explains a range without `-0` (0.3.0-dev.9).** When
422
+ the running Den is a prerelease and only that keeps the range from
423
+ matching, the refusal adds "prerelease builds need a -0 range, e.g.
424
+ >=0.3.0-0", built from the range's own lower bound.
425
+ - **The left rail: grouped, pinnable, collapsible** (0.3.0-dev.17, R11,
426
+ R11a, R12, R13, R14). Pages sit under Develop (Local Dev, Services, Logs),
427
+ Verify (Tests) and Ship (Environments, Releases), with Settings at the
428
+ foot. Groups and items come from one registry in `shell.js`;
429
+ a distribution adds its own with `denShell.registerGroup({id, label,
430
+ order, tag})` and `denShell.registerNav({group, id, label, href, icon,
431
+ order, badge})`, and its labels with `denShell.addMessages(lang, {...})`.
432
+ - **Pinned**: ☆ on a Services card or in the graph's details pins a repo
433
+ or service to the top of the rail with its health dot ("Pinned shop");
434
+ × or Delete on the pinned row unpins it.
435
+ - **Badges**: Services shows how many services are down, live from the
436
+ health row; Logs how many log streams are open.
437
+ - **Collapse** (« at the foot) narrows the rail to its icons, 216 to 56px;
438
+ the headings and labels stay for screen readers. Under 720px the rail is
439
+ a drawer behind ☰.
440
+ - Pins and collapse are stored in user.yml `shell` (`GET`/`POST
441
+ /api/settings` `shell: {pins, collapsed}`); setting them changes nothing
442
+ else (the theme-switch isolation test covers it).
443
+ - **The shell matches the prototype's sizes** (0.3.0-dev.17, R1, R2, R3,
444
+ R9, R10): one 48px top bar with the brand, the workspace pill and Env as
445
+ key + value buttons, the health row as a chip, "What's new" and the ◐ ▾
446
+ menu (now with "All settings"); the rail's type, spacing and current-page
447
+ bar. `scripts/shell-fidelity.mjs` measures the real pages against the
448
+ prototype at 1440 and 1024 (1px, 0.5px for type) and lists any allowed
449
+ difference with its reason.
450
+
451
+ - **The scope breadcrumb is three menus** (0.3.0-dev.18, R4): `/ ORG
452
+ <org> ▾ / WORKSPACE <ws> ▾ / ENV <env> ▾`, each the prototype's `.ib`
453
+ button with a `.pop` menu (✓ on the current entry). The menus are ARIA
454
+ menus: ↓ or Enter opens one, ↑/↓, Home and End move, Enter picks,
455
+ Escape (or Tab) closes it and gives focus back to its button, and a
456
+ click outside closes it. They replace the workspace pill and its menu,
457
+ the env select, the interim View select and Services' own workspace
458
+ select. Below 860px the keys, separators and the org hide; below 720px
459
+ the scope does (the narrow layout, R11b, comes later).
460
+ - **Org:** one org is a static label with no ▾, as before (ORG-SPEC
461
+ C4). `denShell.setOrgs([{ id, title, workspaces? }], current,
462
+ onSelect?)` turns two or more into an Org ▾ menu; a pick calls
463
+ `onSelect(id)` and fires `den:org` on window. Nothing feeds it a list
464
+ yet: ORG-1b plugs `/api/orgs` in.
465
+ - **Workspace:** the view scope. "All active (N)" (with two or more
466
+ active) and each active workspace, the focused one tagged `focus`.
467
+ Picking one stores the view (user.yml `view`, `POST /api/scope`) and
468
+ reloads the page; it never moves the focus. A separate "Set <ws> as
469
+ focus" item (when the workspace in view isn't the focus) does Local
470
+ Dev's focus action, and "Manage workspaces…" opens Local Dev.
471
+ - **Env:** the same envs, value and browser storage as the select it
472
+ replaces; `#env-sel` stays (hidden) as the value pages read and the
473
+ `change` event they listen for.
474
+ - **A view scope: all active workspaces, or one** (0.3.0-dev.18). The
475
+ health row covered every active workspace while Services, Tests, Logs
476
+ and `status` showed only the focused one, so the row above a page listed
477
+ repos the page didn't. One source now says which repos are in view
478
+ (`src/shared/environment-manager/scope.ts`), and the health row,
479
+ `/api/services` (every page's list) and `<cli> status` all read it.
480
+ - "All active" is the default: it is what the health row already showed,
481
+ and with one active workspace it is the same view. A scope that is no
482
+ longer active falls back to all, never to an empty page.
483
+ - **The view is not the focus.** The focus stays the default target for
484
+ commands; the view only filters what pages show. Switching it writes
485
+ user.yml `view` and nothing else: no edge route, container, daemon or
486
+ workspaces.yml change, and the server doesn't restart (the
487
+ theme-switch isolation test covers it). `GET`/`POST /api/scope`.
488
+ - **A shared service is listed once, with its owners**: a repo that two
489
+ workspaces in view define shows one card (`shop · ws-a, ws-b`) and one
490
+ health dot, its deps and auto-start from the first (focus first).
491
+ `/api/services` rows carry `inWorkspaces`, `/api/health` rows carry
492
+ `workspaces`.
493
+ - **`<cli> status`** shows the same view and says so ("Scope: all active
494
+ (a, b, c) · focus: a"). Use `--scope <workspace>` for one (`--all`
495
+ already means `--verbose` there); `-w <workspace>` still checks that
496
+ one workspace.
497
+ - Local Dev edits one workspace at a time: it opens on the view's
498
+ workspace when that is one, else the focus.
499
+ - **A ready path for distribution scripts** (0.3.0-dev.18). A script that
500
+ loads before shell.js pushes functions onto `window.denShellQueue` (an
501
+ array); each runs with `denShell` once the shell exists, and a later push
502
+ runs at once. `den-shell:ready` fires on window when the shell is up.
503
+ - **`den:pins`** (0.3.0-dev.18): any pin change fires it on window
504
+ (`detail.pins`), and every pin toggle on the page repaints from it.
505
+ - **`org.title_source`** in `/api/<cli>/config` (0.3.0-dev.18; added beside
506
+ the existing fields, ORG-SPEC C3): `org.title`, `orgs`, or `id`.
507
+ - **A Settings page** (0.3.0-dev.19, R21): `/settings`, in the shell,
508
+ with the prototype's page header, sections, rows, segmented buttons,
509
+ swatches and navigation list, applied to every setting Den has.
510
+ - **Categories** on the left (a nav; the current one `aria-current`),
511
+ their rows on the right: Appearance, Editor & terminal, Navigation,
512
+ Graph, Workspace, Command line, Distribution, then any a distribution
513
+ or plugin registers (tagged with its `from`).
514
+ - **Search** (`/` focuses it) filters rows by label, description and
515
+ keywords; a match stays under its category's heading, empty
516
+ categories hide, "No settings match" shows when nothing does, and the
517
+ count is announced. The URL keeps `#<category>` and `?q=`, so a
518
+ setting can be linked to; reload and back/forward work.
519
+ - **Every row shows its scope**: User (config/local/user.yml),
520
+ Workspace (that workspace in config/local/workspaces.yml) or
521
+ Distribution (read-only). The Workspace rows follow the Workspace you
522
+ view; with All active, the section asks which workspace.
523
+ - **Writes go only to per-user, gitignored state**, as before:
524
+ `POST /api/settings` (user.yml) and `POST /api/workspaces/settings`
525
+ (workspaces.yml), with the same keys. Nothing writes except an
526
+ explicit change: a control writes on its change (the per-workspace
527
+ editor on Save, as before) and says "✓ Saved". Loading, searching,
528
+ moving between categories and changing the hash or query write
529
+ nothing. The page never writes the distribution's config.
530
+ - **Distribution rows are read-only**: the product name, GitHub org, org
531
+ labels, web port, environments, edge domain and the org default theme,
532
+ each with "Set by the distribution in config/<file>.yml". Your own
533
+ override of the same thing (your theme) is a separate User row.
534
+ `GET /api/settings` gains `distribution` (read from the org config
535
+ only), `user_web_port`, `local_dev` and `cli_tabs` (read-only).
536
+ - **Today's settings moved in, unchanged in storage**: Default IDE,
537
+ Per-language IDE and Default terminal (Local Dev's ⚙ panel); mode and
538
+ theme (◐ ▾ stays as a shortcut, and both drive the same prefs and
539
+ repaint each other); the graph prefs; the per-workspace editor (Local
540
+ Dev's workspace Settings tab). `log_window_lines`, `local_dev`,
541
+ `cli_tabs` and your `web.port` show read-only with the command that
542
+ sets them.
543
+ - **Navigation** (new): hide or show each rail page and move it within
544
+ its group; reorder and unpin pins; collapse the rail. Stored in
545
+ user.yml `shell` as `hidden` and `order` (page ids), beside `pins` and
546
+ `collapsed`. A view preference only: no edge route, container, daemon,
547
+ pid or restart changes (the theme-switch isolation test covers it).
548
+ - **The old entry points land on the matching category**: Local Dev's
549
+ ⚙ (`#editor`), its workspace Settings tab (`#workspace`, that
550
+ workspace), `/local-dev?settings=1` (redirected to `#editor`), the
551
+ rail's Settings item and "All settings" in ◐ ▾ (`#appearance`). The
552
+ old panel is gone. Arriving at a category (a link, the hash,
553
+ back/forward) marks it current and puts focus on its heading.
554
+ - **The SETTINGS registry** (0.3.0-dev.19, R21), beside NAV and GROUPS on
555
+ `window.denShell` (and through `window.denShellQueue`):
556
+ `registerSettingsCategory({ id, label, order, description?, from? })`
557
+ and `registerSetting({ id, category, label, description, keywords,
558
+ scope: user|workspace|distribution, control, read(ctx), write?(value,
559
+ ctx), keys? })`; `settings()`, `settingsCategories()`; `den:settings`
560
+ fires on any change. A distribution row with a `write` is refused.
561
+ Controls: `seg`, `radio`, `select`, `number`, `toggle`, `text`,
562
+ `readonly` and `custom` (`render(el, ctx, { value, save })`). Den's own
563
+ rows are in `public/settings-registry.js`; a test fails if a key
564
+ `POST /api/settings` accepts has no row.
565
+ - **`denShell.setNavPrefs({ hidden?, order? })`, `setPins(ids)`,
566
+ `setCollapsed(bool)` and `orderedNav()`** (0.3.0-dev.19): the rail's
567
+ Navigation prefs, saved to `/api/settings` `shell`.
568
+ - **The top bar's search, status, runs and lock** (0.3.0-dev.20, R5, R6,
569
+ R7, R8, R10, R15, R17, R18). It ships together with the Settings page
570
+ (dev.19, never released on its own): the palette finds every setting,
571
+ and the user menu's "All settings" opens it. The bar now reads ☰, brand, scope, search,
572
+ then the Status, Runs and lock chips, "What's new" and the ◐ ▾ menu, at
573
+ the prototype's sizes (`scripts/shell-fidelity.mjs` measures it). The
574
+ search field gives way first, so the scope's parts never run into each
575
+ other; below 960px the scope's keys hide (860px in the prototype, whose
576
+ chips are narrower), below 860px the chips keep only their dots and
577
+ icons, and below 720px the scope hides and the search field is an icon.
578
+ - **Search and the ⌘K palette.** "Search pages, services, actions…"
579
+ (⌘K or Ctrl+K anywhere, or a click) opens a palette: pages, apps,
580
+ services with their health dot, Restart and Logs for each service,
581
+ Appearance (System, Dark, Light) and every installed theme. It ranks a
582
+ label's prefix, then a word in it, then anywhere, then keywords and
583
+ the kind, then the letters in order; every word must match. ↑/↓ move,
584
+ Enter runs, Escape closes and gives focus back. It is a modal dialog
585
+ with a combobox and a listbox (`aria-activedescendant`).
586
+ - **`denShell.registerCommand({id, label, keywords, kind, run})`**
587
+ (also from `window.denShellQueue`) adds a palette row; `kind` is
588
+ `page`, `app`, `service`, `action` or `setting`. Every row of the
589
+ SETTINGS registry (Den's, a distribution's or a plugin's) is a
590
+ `setting` row on every page: "terminal" finds Default terminal, and
591
+ Enter opens `/settings?q=<label>#<category>`.
592
+ `unregisterCommand(id)` and `commands()` too. `denShell.openMenu(btn,
593
+ items)` opens a `.pop` menu any script can use.
594
+ - **The status chip** replaces the health dot row and its "+N" chip:
595
+ "All N healthy", or "ok/N healthy · k down" with a bad border, from the
596
+ same rollup as the rail's Services badge. It shows "Checking…" until
597
+ the first cycle is in and paints once: no partial red on a cold load.
598
+ A pending check is pending, never down.
599
+ - **The status drawer** (the chip opens it): a right-hand modal panel,
600
+ focus on ✕, Escape back to the chip. "Needs attention" has one card
601
+ per down service, with what was checked, the error and when, and
602
+ Start, Logs and Pin; "Everything" lists each application ok/N, each
603
+ service with its time in ms. What the row's tooltips said is in each
604
+ row's title.
605
+ - **The Runs chip** counts the runs this page started (service and
606
+ workspace commands, syncs, test runs, deploys; not log tails), with a
607
+ pulse while one runs. Until the output dock (dev.21) it shows the
608
+ page's own output pane (marked `data-den-output`), else links to the
609
+ page of the last run.
610
+ - **The lock chip** (🔒 Locked / 🔓 Unlocked) holds the admin-password
611
+ form that was Local Dev's sudo banner, with Save to 1Password, in a
612
+ popover: "Den asks once per session. Agents can't unlock." It is the
613
+ one unlock on every page; the banner is gone. Local Dev still asks
614
+ 1Password for a stored password on load; other pages ask only when
615
+ the popover opens. Start and Restart from the drawer or the palette
616
+ follow the same lock rule, then run on Local Dev (which runs them on
617
+ arrival when you are on another page).
618
+ - **The user menu** (◐ ▾) is the prototype's `.pop`: Appearance, Theme
619
+ and "All settings" (`/settings#appearance`), and a hidden slot for
620
+ Language. Its ◐ ▾ glyphs are hidden from screen readers; its name is
621
+ "Appearance". Opening any of these overlays writes
622
+ nothing; switching appearance or theme from the menu or the palette
623
+ writes user.yml `appearance` only (the theme-switch isolation test
624
+ covers it).
625
+ - `header.js` exports `requireUnlock`, `isUnlocked`, `onLockChange`,
626
+ `setServiceRunner` and `takePendingAction` for pages.
627
+ - **One h1 per page**: the brand title in the bar is a span now, not an
628
+ h1, so a page's own heading is its one. A page with none (Services,
629
+ Logs, …) gets a visually hidden h1 named from its title.
630
+ - **Raster theme marks** (0.3.0-dev.22): a mark can be a PNG or a WebP
631
+ (`png` / `png_dark`, `webp` / `webp_dark`), for a mark that exists only
632
+ as a raster. One image kind per mark (svg, png or webp): two fail
633
+ validate, naming both. `theme validate` and `theme add` check each file's
634
+ first bytes against its extension, a 64 KB cap and at least 44×44 px
635
+ (twice the bar's 22px mark), reading the PNG or WebP header itself.
636
+ `/theme/<id>/<file>` serves them as `image/png` and `image/webp`, still
637
+ only the files the manifest names. The top bar draws one as an image at
638
+ 22px, clipped to the mark's `shape`, the dark file in dark mode. SVG and
639
+ text marks are unchanged.
640
+ - **The browser tab shows the theme's icon** (0.3.0-dev.22): the theme's
641
+ new optional `favicon` (`.png`, `.svg` or `.ico`, magic-byte checked, at
642
+ most 64 KB), else its mark's file (the dark variant in dark mode), else
643
+ Den's own icon. The shell sets it on every page (no page links
644
+ `/favicon.ico` any more) and changes it with the theme and the mode,
645
+ System following the OS included. Its URL,
646
+ `/theme-favicon/<id>?mode=<mode>&v=<version>-<hash>`, changes with each,
647
+ so a switch is never answered from cache. The built-in `den` theme names
648
+ Den's icon as its `favicon`; `theme new` leaves it out, so a new theme's
649
+ tab shows its own mark.
650
+ - **Secrets backends are registered strategies, and `none` is one**
651
+ (0.3.0-dev.23). Each built-in (`none`, `env`, `doppler`, `1password`)
652
+ registers through one registry (`src/shared/strategies/registry.ts`, kind
653
+ `secrets`) as `{ id, kind, detect(), capabilities }`: a factory, a label,
654
+ its CLI, which ref parts it needs, and optional `setup` and env
655
+ injection. `detect()` is a PATH check that never prompts or prints; a
656
+ backend whose CLI is missing reports "not available: The Doppler CLI
657
+ (doppler) is not on PATH" instead of failing a spawn. `secrets.provider:
658
+ none` resolves nothing and never runs a CLI. `den create` now scaffolds
659
+ `secrets: { provider: none }`, with the choices in a comment, so a new
660
+ distribution depends on no store. See "Secrets" in
661
+ `docs/distributions.md`.
662
+ - **The edge's TLS is a registered strategy: `edge.tls`** (0.3.0-dev.25,
663
+ TLS-1a). `none`, `mkcert` and `files` register through the strategy
664
+ registry as kind `tls`, as the secrets backends do. `tls: none` serves
665
+ http only (no https entrypoint, no cert work, mkcert never run);
666
+ `{ strategy: mkcert }` issues the edge's wildcard cert from a local CA,
667
+ its own or `ca:` (an existing CA, never minted or uninstalled);
668
+ `{ strategy: files, include, mounts }` serves the certs your included
669
+ config holds (`ca:` optional, for health checks). An unknown strategy,
670
+ or a key a strategy does not take, is a validation error naming the
671
+ built-ins. mkcert's `detect()` is a PATH check: with it missing,
672
+ `validate`, `doctor`, `edge up` and `edge trust` say "mkcert is not
673
+ installed" instead of a spawn error. `edge trust` / `untrust` exist under
674
+ mkcert, and `edge trust` also under `files` with a `ca` (it installs that
675
+ existing CA, as `caroot` did); elsewhere they exit 1 ("edge trust needs
676
+ edge.tls.strategy: mkcert (or files with a ca)").
677
+ `den create` scaffolds `edge.tls: none`, with the mkcert and files forms
678
+ in a comment. `/api/edge` adds `tls: { strategy, implicit }`. See "The
679
+ edge and TLS" in `docs/distributions.md`.
680
+ - **`<cli> doctor`** (0.3.0-dev.25): each strategy kind (secrets, tls),
681
+ the strategy selected, whether it was implicit, and its `detect()` with
682
+ the reason. Read-only: a PATH check, never a backend CLI, a prompt or a
683
+ secret value. Exit 1 when a strategy that will be used cannot work.
684
+
685
+ ### Changed
686
+
687
+ - **`down` works under an invalid config** (0.3.0-dev.40): it is
688
+ config-free, so stopping is never stranded. With a config that fails
689
+ validation it says so ("Stopping anyway, without the config's org
690
+ data") and stops from the daemon records and compose projects; `down -a`
691
+ too. Every other command still stops first.
692
+ - **Only up and restart detach by default** (0.3.0-dev.40, DOC-1). Every
693
+ string command but down and setup ran detached, so `preflight` run from
694
+ the Services page never finished. A command other than up and restart
695
+ now runs to completion unless it says `detachable: true`.
696
+ - **English plurals** (0.3.0-dev.40): en has the `.one` forms (`org.n.one`
697
+ "{n} workspace", and the status, badge, drawer and changelog counts), so
698
+ the org menu no longer says "1 workspaces". The catalog test fails when a
699
+ count another catalog makes singular has no en `.one`.
700
+ - **The graph's top-level groups wrap to the stage** (0.3.0-dev.34, a
701
+ consumer's dogfood finding: 31 services in 10 org groups). Top-down,
702
+ every org (or application) group sat in one row, so the whole graph
703
+ fitted only at 15–24%, where cards are hard to read. Each top-level
704
+ group is now laid out on its own by the same layered layout (a group's
705
+ inside is unchanged), and the groups wrap into rows (top-down) or
706
+ columns (left-right), choosing the wrap that fits the stage at the
707
+ highest zoom; the edges between groups are routed through the gaps
708
+ between rows. A 31-card, 10-org graph fits at 45% at 1440×900 (was
709
+ 22%) and 33% at 1024×768 (was 14%).
710
+ - **The automatic fit shows the whole graph** (0.3.0-dev.34). A graph
711
+ that needed less than 25% used to open on its top at 60%; the first
712
+ paint now fits it as Fit does (down to 10%).
713
+
714
+ - **The Graph page follows the top bar's workspace scope** (0.3.0-dev.33,
715
+ a consumer's dogfood finding). With the top bar's Workspace on "All
716
+ active", `/graph` used to show (and name) the focused workspace only. It
717
+ now shows every active workspace in one graph, as the Services page
718
+ lists them: one card per service, a service several workspaces define
719
+ listed once with its owners (`demo-api · main, notes`). One workspace
720
+ picked shows that one, and `?ws=` still wins. The page header's line
721
+ names what is shown (`acme / All active / local`). A card's actions run
722
+ in the workspace that owns its repo (the first in view, focus first).
723
+ Local Dev keeps one workspace, the list's selection: its actions, its
724
+ Applications and Config tabs and its Graph tab are per workspace.
725
+ - **Stack routes on the Edge page and in `edge routes`: scheme and
726
+ upstream** (0.3.0-dev.33, a consumer's dogfood finding). A stack
727
+ router's URL is shown with its scheme like Den's own rows: `https://`
728
+ when the router has TLS or is on the `websecure` entrypoint, else
729
+ `http://`. UPSTREAM shows the service's `loadBalancer` server URL when
730
+ the router's file defines it, else the Traefik service's name. The
731
+ routers in `/api/edge` (`included`, `stacks`) carry `secure` and
732
+ `upstream`; included routers get the same.
733
+ - **Page tabs are WAI-ARIA tabs** (0.3.0-dev.33, a consumer's dogfood
734
+ finding: ArrowRight, Home and End left focus on Graph). Local Dev's
735
+ workspace tabs and any tabs `denShell.pageHeader().tabs(spec)` makes
736
+ share one helper, `denShell.wireTabs(tablist, opts)`: one tab stop (the
737
+ selected tab is `tabindex=0`, the rest `-1`), ←/→ move and wrap, Home and
738
+ End go to the ends, and a tab is selected as focus moves to it
739
+ (automatic activation). Each tab's `aria-controls` names its
740
+ `role="tabpanel"`, whose `aria-labelledby` names the tab, and the other
741
+ panels are hidden. Local Dev's "Settings ↗" link now sits beside the
742
+ tablist rather than in it. `pageHeader().tabs` is a function now: the
743
+ shell no longer adds a hidden, unlabelled, empty `.ptabs` tablist to
744
+ every page, only the one a page asks for.
745
+
746
+ - **POST /api/settings: null clears a pref** (0.3.0-dev.32, from a
747
+ consumer). A key set to `null` (`{"graph":{"workspaces":null}}`, or in
748
+ `shell` or `appearance`) is removed from config/local/user.yml instead of
749
+ failing with 400, and a block left empty goes too; a whole block set to
750
+ `null` (`{"graph":null}`) removes it. `graph:` now keeps only the keys
751
+ someone set (it used to write every default), so setting a pref and
752
+ clearing it again leaves user.yml byte-identical.
753
+ - **Edge page: the cert-names card under tls `files`** (0.3.0-dev.32, from a
754
+ consumer) reads "Names the included certs should cover": Den issues
755
+ nothing there. mkcert keeps "Certificate names (planned)".
756
+
757
+ - **The pages' output panes are gone; their runs are tabs in the runs
758
+ dock** (0.3.0-dev.30, R16). Local Dev's output panel (one tab per
759
+ system, log tails, the dismissed tabs per workspace), Services' output
760
+ pane, Tests' output pane and its service command strip (Stop / Detach is
761
+ now a button in the dock's bar while the command runs), and Releases'
762
+ deploy log (each deploy session is a tab: selecting one attaches its
763
+ log, its × closes the session; the session chips are gone) all write to
764
+ the dock. The output lines are the same text with the same line classes;
765
+ k6's progress lines still update in place, Local Dev still keeps its
766
+ last 1000 lines per tab. Auto-scroll now pauses on every page when you
767
+ scroll up (Local Dev's rule).
768
+ - **The runs dock starts collapsed, and stays as you leave it** (0.3.0-dev.30,
769
+ R16a, Ward QA 2026-11-05). A run starting never opens it: its tab
770
+ appears and the Runs chip pulses, and you open it (Show runs, the chip or
771
+ a tab). Only an action whose whole point is output opens it: Logs on a
772
+ service (Services) and a card's output in Local Dev's graph. Open or
773
+ closed and its height are user prefs (`shell.dock`, `shell.dockHeight`
774
+ in `/api/settings`, the Settings page's Navigation row "Open the runs
775
+ dock"), so they hold across pages, reloads and browsers; nothing is kept
776
+ per browser tab any more. Dragging its top edge stops at half the window.
777
+ - **Settings: the search and the category list stay put** (0.3.0-dev.30,
778
+ R21a, Ward QA 2026-11-05). The page header with its search box and the
779
+ category list on the left are sticky while the rows scroll; a category
780
+ click scrolls its section to just below the header.
781
+
782
+ - **Existing edge configs render exactly as before** (0.3.0-dev.25). The
783
+ legacy keys map to `edge.tls` (EXT-1 rule 6): `issue_cert: false` is
784
+ `files`, `caroot` is `mkcert` with `ca`, and an `edge:` block with
785
+ neither is `mkcert` with the edge's own CA. A test renders each shape
786
+ (and its explicit `edge.tls` form) and compares the Traefik dynamic and
787
+ compose files, cert names and paths, route URLs and the mkcert call
788
+ byte for byte with 0.3.0-dev.24's own output. Under `files` with no `ca`,
789
+ `edge trust` now refuses (it minted a CA that signs nothing the edge
790
+ serves); with a `ca` it installs that CA, as before.
791
+ - **No built-in secret locations** (0.3.0-dev.23). Den no longer assumes a
792
+ vault, item, field, key or account of its own: the default 1Password
793
+ vault, developer-keys item and sudo field, the default account domain, and
794
+ the flat `<PREFIX>_LOCAL_ADMIN_PWD` key that doppler and env used for the
795
+ saved sudo password are gone. The saved sudo password and the journeys
796
+ agent key come only from `secrets.refs.sudo` and
797
+ `secrets.refs.journeys_agent_key`; without them each says "not configured:
798
+ set secrets.provider and secrets.refs.<name> in config/<cli>.yml" and
799
+ runs no CLI (the interactive unlock still works). A `1password`
800
+ distribution with no refs gets one warning in the web server's startup
801
+ log and in `validate`. The 1Password developer-keys setup writes to
802
+ `secrets.developer_keys: { vault, item }` (else the vault and item of
803
+ `refs.sudo`). See Migrations.
804
+ - **`make up` / `make web` check secrets only when `secrets.required` is
805
+ set** (0.3.0-dev.23). Otherwise the gate passes silently, whatever the
806
+ provider, and never asks the backend. `make <cli>-setup-secrets` runs the
807
+ selected strategy's setup; with `none`, or no provider set, it says there
808
+ is nothing to set up and exits 0.
809
+ - **`/api/sudo-password/vault-status` is backend-neutral** (0.3.0-dev.23):
810
+ `{ backend, label, configured, backendAvailable, reason?, hasStored }`,
811
+ and the unlock form says "Save to <backend>". `load-from-vault` answers
812
+ `{ ok: false, reason: "not-configured", message }` with no `refs.sudo`.
813
+ - **A local-jwt signing key that is not found names its provider**
814
+ (0.3.0-dev.23): "... not found in env or the doppler secrets backend
815
+ (secrets.provider)". Its resolution is unchanged.
816
+
817
+ ### Deprecated
818
+
819
+ - **The implicit mkcert default (set `edge.tls`)** (0.3.0-dev.25). An
820
+ `edge:` block with no `tls`, `caroot` or `issue_cert` still means mkcert
821
+ with a local CA, and warns once per run (`validate`, `doctor`, `edge up`,
822
+ `stack up`): "edge.tls is not set; defaulting to mkcert with a local CA.
823
+ Set edge.tls explicitly." `edge.caroot` and `edge.issue_cert` still work,
824
+ and `validate` and `doctor` name the `edge.tls` line that says the same.
825
+ - **The implicit doppler default (set `secrets.provider`)** (0.3.0-dev.23).
826
+ An unset `secrets.provider` still means `doppler`, but only when something
827
+ actually resolves a secret; the first time it does, Den warns once:
828
+ "secrets.provider is not set; the implicit doppler default is deprecated
829
+ and will be removed in a later minor. Set it explicitly: doppler,
830
+ 1password, env or none." A distribution that resolves no secret never
831
+ runs `doppler` and never sees the warning.
832
+ - **`opAvailable` in `/api/sudo-password/vault-status`** (0.3.0-dev.23):
833
+ an alias of `backendAvailable`, removed in a later minor. Read
834
+ `backendAvailable` (and `backend`, `reason`).
835
+
836
+ ### Fixed
837
+
838
+ - **`web up` under an invalid config reports the server ready**
839
+ (0.3.0-dev.42, a consumer's dev.41 finding). It waited for
840
+ `/api/health`, which answers 422 by design then, and exited 1 with
841
+ "web did not become ready" while the server ran. Readiness now asks
842
+ `/api/server-info` (answered under any config; `/api/health` for a
843
+ server too old for it), and the 422 "Config is invalid" page counts as
844
+ the shell serving.
845
+ - **No page throws under an invalid config** (0.3.0-dev.41, a consumer's
846
+ dev.40 finding: "envs.map is not a function" on every page).
847
+ `/api/environments` is config-free (it already fell back to `local`),
848
+ the env picker and the Logs toolbar treat an error answer as an empty
849
+ list, and a test loads every page with an invalid config and expects
850
+ no uncaught error.
851
+ - **A refused `edge down` says "not stopping it"**, not "not replacing
852
+ it"; `edge` rejects an unknown flag (a mistyped `--take-over` is never
853
+ ignored) (0.3.0-dev.41).
854
+ - **A pre-dev.37 record whose port is held by another process** no
855
+ longer warns "nothing running is it" before saying what holds the port
856
+ (0.3.0-dev.41).
857
+ - **Safety: an edge another state dir owns is never replaced**
858
+ (0.3.0-dev.40, P1). The edge's container and Compose project are named
859
+ by `edge.org`, so a scratch root with its own `DEN_EDGE_DIR` but the real
860
+ `edge.org` recreated the real edge with its scratch certs and routes
861
+ (`stack up` brings the edge up first). The edge container now carries
862
+ `den.edge.root=<realpath of its state dir>` and `den.edge.org=<org>`, and
863
+ `edge up`, `stack up`/`restart` (their implicit edge up) and `edge down`
864
+ touch an existing `<org>-edge` only when its `den.edge.root` is their own
865
+ state dir. Otherwise they refuse before writing or starting anything,
866
+ exit 1: "edge <org>-edge belongs to <its dir>; not replacing it". An
867
+ unlabelled (pre-dev.40) edge is adopted only from the default state dir,
868
+ never under a `DEN_EDGE_DIR` that overrides it (the refusal names its
869
+ mounts' dir when known). Under such an override a refusal also warns:
870
+ "Give this root a distinct edge.org." `edge up --take-over` is the only
871
+ override, and nothing implies it. The routes (the dynamic files) are
872
+ byte-identical. See "The edge and TLS" in docs/distributions.md.
873
+ - **A recreated edge keeps running stacks routed** (0.3.0-dev.40, P1). A
874
+ new or restarted edge container starts on its own network only, so every
875
+ running stack's routes answered 502 until that stack's next `stack up`.
876
+ After `edge up` (and `stack up`/`restart`'s implicit edge up) the edge
877
+ now rejoins the routed networks of each stack that has its routes file
878
+ (`dynamic/stack-<project>.yml`) in this edge's state dir and whose
879
+ Compose project is running, with one line per network ("rejoined
880
+ <network> (<project>)"). Stacks that are down are skipped; a network the
881
+ edge is on already is left alone; a project with no routes file in this
882
+ state dir is never touched. `stack up` connects through the same helper.
883
+ - **`validate` reports an org config error** (0.3.0-dev.40, a consumer's
884
+ 0.2.8 nit): each problem with its file and key path, exit 1; the
885
+ manifests are still checked.
886
+ - **The scoped `manifest_validate` refusal names the whole path**
887
+ (0.3.0-dev.40, a consumer's 0.2.8 nit): it was cut at 80 characters,
888
+ mid-filename. Up to 160 it is whole; a longer one keeps its start and its
889
+ filename and elides the middle with "…".
890
+ - **A named `up <repo>/<svc>` whose port is held refuses before spawning**
891
+ (0.3.0-dev.40, a consumer's dev.39 finding): it started the server once,
892
+ which died on EADDRINUSE, before refusing. It now refuses as the
893
+ repo-level up does ("<repo>/<svc> not started"), with or without a
894
+ pre-dev.37 record. And a listener in the repo but in Den's own process
895
+ group is said as that ("in Den's own process group; not adopted"), not
896
+ as "outside <repoPath>".
897
+ - **Graph cards** (0.3.0-dev.40, DOC-1): a service card's red "down"
898
+ border goes when it recovers (only repo cards toggled it); before the
899
+ first health round cards say "checking…", as the Workspaces table does,
900
+ not "no health check"; the details panel's Opens link follows the edge's
901
+ TLS (`http://` and the http port for `tls: none`; it always said
902
+ `https://`).
903
+ - **Records from before 0.3.0-dev.37 reach adoption from the CLI**
904
+ (0.3.0-dev.39, a consumer's dev.38 finding). dev.38 decided right when
905
+ asked, but the CLI's paths never asked. Every up, down and healthy-skip
906
+ now reads a service's record through one accessor
907
+ (`settledDaemonRecord`) that settles a pre-dev.37 record first: adopted
908
+ (its group recorded) or dropped (said, nothing signalled).
909
+ - `up <repo>` on a healthy service attached to the old record and left
910
+ it without a group, so a later `down` could not stop it. It now adopts
911
+ the running process ("adopted pre-dev.37 process (pid …, port …)"),
912
+ starts nothing, and `down <repo>` stops it.
913
+ - `down <repo>/<svc>` of a service with an up of its own pointed at the
914
+ repo's down ("it is stopped by repox's down") and stopped nothing. Its
915
+ down is its own: the group its up recorded.
916
+ - A healthy service with no record whose port is held by a process in
917
+ its repo is adopted the same way; one held from outside the repo is
918
+ said ("port N is in use by pid … (cwd …), outside …: left alone"),
919
+ never claimed or signalled, and nothing is started on it (a named
920
+ target fails; a dependency warns).
921
+ - An up whose process exits non-zero during startup (a second server
922
+ that died on EADDRINUSE) keeps no record and no process group, so a
923
+ later `down` never signals a dead group while the real listener runs.
924
+ - The CLI prints what was adopted, dropped or found busy; before, those
925
+ lines only flashed in the spinner.
926
+ - `down -a` warns about records no active workspace reaches ("… is in no
927
+ active workspace: down -a left it alone") and leaves them alone, where
928
+ it skipped them silently.
929
+ - **`restart` of a service with only an `up` works** (0.3.0-dev.39, a
930
+ consumer's dev.38 finding). `restart repox/web` failed with `command
931
+ "restart" not found` and left the old group running. With no restart or
932
+ down command, a restart stops the recorded group, then runs the up
933
+ detached, as `down` then `up` would.
934
+ - **`down <repo>` and `down -a` stop a repo's per-service daemons**
935
+ (0.3.0-dev.38, a consumer's dev.37 finding). A repo with services of
936
+ their own (`up repox` started `repox/web` and `repox/api`, each with
937
+ its own process group) and no down of its own printed "down repox
938
+ (nothing to stop)" and left both running. A repo-level down with no
939
+ down command now stops every service record of the repo through the
940
+ per-record path (its refcount, its down command or its recorded group);
941
+ `down -a` does it for each repo of the workspace. The mirror of
942
+ dev.35's `up` multi-service fix.
943
+ - **Records from before 0.3.0-dev.37 are adopted or dropped, never
944
+ trusted blindly** (0.3.0-dev.38, a consumer's dev.37 finding). A record
945
+ with no process group made `up` print "✔ up repox/api" while nothing ran,
946
+ and `down` give up on it. One rule now serves `up` and `down`: the
947
+ listener on the service's known port (its `port`, the port in its
948
+ health URL, or `--port`/`-p` in its up command) whose cwd is inside the
949
+ repo is adopted (its process group is recorded: "adopted pre-dev.37
950
+ process (pid N, port P)"); else a recorded pid whose group shows every
951
+ token of the up command (leading `VAR=value` and an `npx`/`npm exec`
952
+ prefix stripped) is adopted; else the record is stale: warned, dropped,
953
+ nothing signalled. `up` then reports an adopted service already running
954
+ and starts a stale one; `down` stops an adopted one through the
955
+ TERM→KILL group path. A port held by a process outside the repo is
956
+ named and left alone.
957
+ - **A restart records only the up** (0.3.0-dev.38). A service with a down
958
+ and an up but no restart ran one detached `sh -c "<down> && <up>"`, so
959
+ its recorded process group held the down too. It is now the down step,
960
+ then a detached up step, and the group is the up's.
961
+
962
+ - **Graph: a selected card's flowing edges are visible** (0.3.0-dev.37,
963
+ Ward QA of dev.35). The marching dashes were drawn in the same colour on
964
+ a full-strength line, so they barely showed. A lit edge's line is now a
965
+ faint track in its kind's colour and the dashes run wider on top of it;
966
+ under reduced motion the lit edge stays solid and thicker, as before.
967
+ - **Test workers inherit no credentials** (0.3.0-dev.37). `tests/setup.ts`
968
+ strips credential-shaped variables (by name, family or token shape) from
969
+ the environment before any suite runs, so a failing assertion on a
970
+ spawn env can no longer print a developer's tokens.
971
+ - **`down` stops what a detached `up` started** (0.3.0-dev.37, a
972
+ consumer's dogfood finding). `<cli> down <repo>` for a service with no
973
+ down command and no container printed ✔ and stopped nothing: daemon
974
+ records kept no process. A detached `up` now records its process group
975
+ (`pgid`, and the leader's start time as `pgidStartedAt`) in the daemon
976
+ record, and `down` sends the group SIGTERM, waits up to 3s
977
+ (`DEN_DOWN_GRACE_MS`), then SIGKILL. Before each signal it checks the
978
+ pid still belongs to the recorded process (same start time), so a
979
+ reused pid is never signalled: that service had already exited, and
980
+ `down` says so and leaves the process alone. After a declared down
981
+ succeeds, a recorded group still running is stopped too. When `down`
982
+ cannot stop what it knows is running (a record written before dev.37,
983
+ with no group; a group that survives SIGKILL), it warns, and an explicit
984
+ `down <repo>` exits non-zero (a workspace-wide `down` warns and goes
985
+ on). With nothing recorded, the CLI says "nothing to stop" instead of ✔.
986
+ `down --dry-run` lists a service with a recorded group as one it stops.
987
+ - **A compose-stack repo's Start and Restart work on the Workspaces page**
988
+ (0.3.0-dev.37, a consumer's dev.35 finding). The repo row's buttons
989
+ were disabled, "has no up command", for a repo whose up is its compose
990
+ stack's. `/api/services` now gives each local service its effective
991
+ commands (a stack's synthesized up/down/restart included) and
992
+ `upTargets`: what `<cli> up <repo>` starts (`resolveUpTargets`), as the
993
+ table's addresses. The repo row's Start and Restart are enabled when it
994
+ is non-empty and run exactly those addresses.
995
+ - **The Workspaces table says "checking…" until the first health round
996
+ lands** (0.3.0-dev.37). Before it, repo and service health cells read
997
+ "unknown (not checked)" or "no health check".
998
+ - **Find shows only the sub-rows it matches** (0.3.0-dev.37). A Find that
999
+ matched one service of a repo opened every one of the repo's sub-rows;
1000
+ now only the matching ones show under their repo (a match on the repo
1001
+ itself still shows them all).
1002
+
1003
+ - **`<cli> up <repo>` no longer fails fast on a multi-service dependency**
1004
+ (0.3.0-dev.35; backported to 0.2.8). A dependency with no `up` of its
1005
+ own but with per-service ups (two services, each with its `up`) was run
1006
+ as the repo, `command "up" not found for service "<repo>"`, and the run
1007
+ aborted before the repo you asked for. `up <repo>` and `up -a` now make
1008
+ one decision (`resolveUpTargets`): a repo with its own `up` starts as
1009
+ before; one with none but per-service ups starts its services (all of
1010
+ them, or the ones its dependents name in `depends_on`); only one with
1011
+ no `up` at all is skipped, said as `-a` says it ("Skipping (no local up
1012
+ command): …"), and nothing waits on it. A dependency whose `up` fails
1013
+ still fails fast.
1014
+
1015
+ - **The graph fits on first paint** (0.3.0-dev.34, a consumer's dogfood
1016
+ finding). With "All active", `/graph` opened at 60% with 17 of 31 cards
1017
+ outside the stage at 1440×900 (23 at 1024×768) until Fit was clicked:
1018
+ the org frames land after the first draw, the graph was drawn again,
1019
+ and only the first draw was fitted (a render a newer one overtook could
1020
+ also fit the stale layout). The view now fits once the final layout is
1021
+ drawn, and again whenever the set of cards changes, unless you zoomed
1022
+ or panned since. Local Dev's per-workspace graph likewise.
1023
+ - **The Workspace picker on `/graph` replaces `?ws=`** (0.3.0-dev.34, a
1024
+ consumer's dogfood finding). On `/graph?ws=w2`, "All active" changed the
1025
+ pill but the URL kept `?ws=w2`, which wins, so the header and the graph
1026
+ stayed on w2, after a reload too. A choice there now drops `?ws=` (All
1027
+ active) or sets it (a workspace), replacing the history entry, and the
1028
+ pill shows the workspace a fresh `?ws=` names: the pill, URL, the
1029
+ header's line and the graph agree.
1030
+ - **The runs dock is a tablist only while it holds tabs** (0.3.0-dev.34).
1031
+ With no runs, `.den-runs-l` was a `role=tablist` holding a "No runs"
1032
+ span and no tab; the role and its label come back with the first run.
1033
+ - **`theme-switch-isolation` is deterministic under load** (0.3.0-dev.34;
1034
+ it failed 5/6 then 1/6 on a busy machine). Its file snapshot covered the
1035
+ throwaway HOME, where a container runner's CLI that the health check
1036
+ calls writes its own update hint on its own schedule; it now covers
1037
+ Den's files (the distribution and `DEN_HOME`). The server gets a port
1038
+ from the OS on localhost (not a random pick that could land on another
1039
+ session's server), and readiness waits for the server whose
1040
+ `/api/server-info` names this distribution's root.
1041
+
1042
+ - **Stale daemon records are dropped** (0.3.0-dev.33, a consumer's
1043
+ dogfood finding). A record whose repo had been relocated (no pid,
1044
+ `containers: []`) was kept forever because its address still resolved,
1045
+ and the Runs dock showed a permanent tab. The startup sweep and the
1046
+ health reaper now drop a record of this root's, or an unstamped one,
1047
+ whose `repoPath` is not on disk, or whose `repoPath` is no longer where
1048
+ the service resolves and none of its candidate containers (captured,
1049
+ or the dev-op's at its old path) is running; the relocated checkout's
1050
+ own container, which carries the repo's name, does not count.
1051
+ Another root's records are never touched, and a record that resolves
1052
+ to its own checkout with nothing to verify is still kept (it may be a
1053
+ live non-opctl process). Removals are logged with the sweep's and the
1054
+ reaper's lines.
1055
+ - **Graph group boxes fit their content** (0.3.0-dev.33, a consumer's
1056
+ dogfood finding). On `/graph` a repo box with one row of cards drew
1057
+ about twice the height it needs: top-down, ELK reads a compound node's
1058
+ minimum size in its own layout axes, so the repo box's minimum width
1059
+ (386px) became its height. The minimum is passed in that order now; the
1060
+ boxes are their padding plus their cards, and keep the header's width.
1061
+ - **Fit keeps the graph above the legend and the zoom group**
1062
+ (0.3.0-dev.33, a consumer's dogfood finding). After Fit, a legend on
1063
+ two lines covered the last group's bottom ~10px. Fit's height now leaves
1064
+ out the overlays' measured height (and a gap), so no card or group
1065
+ border sits under them.
1066
+
1067
+ - **CA trust on a fronted edge reads "not needed: fronted"** (0.3.0-dev.32,
1068
+ from a consumer). With `edge.publish: false` a proxy in front owns
1069
+ browser trust (BOOT-1), so the Edge page's CA trust tile is neutral
1070
+ instead of a red "not trusted", and `edge status` says the same.
1071
+ - **Fit follows the graph's size on maximize and restore** (0.3.0-dev.32,
1072
+ R23, from a consumer). Fit stopped at 35%, so a big graph at 1440×900 read
1073
+ 35% maximized or not, and didn't fit either way. Fit now fits the whole
1074
+ graph, by width and height, down to 10% (zooming out goes as far as
1075
+ that), and a fitted view refits when the graph is maximized or restored.
1076
+
1077
+ - **A seed without a terminal no longer resets a workspace that has
1078
+ entries** (SEED-0, 0.3.0-dev.29; also 0.2.7). `<cli> init` and
1079
+ `<cli> workspace seed` from a script or `make` replaced every entry of the
1080
+ default (or blueprint) workspaces with managed clones, dev-managed
1081
+ checkouts included, without asking. Without a TTY they now refuse (exit 1,
1082
+ nothing written) and name `--reset`; on a TTY they still ask, default No.
1083
+ `init --reset` and `workspace seed --reset` reset as before. A script that
1084
+ relied on the silent reset must pass `--reset`.
1085
+ - **`<cli> init` in the distribution root seeds through a symlinked path**
1086
+ (0.3.0-dev.29): it compared paths without resolving links and ran the
1087
+ manifest scaffolder instead.
1088
+
1089
+ - **`create`'s "Next:" hint names an absolute path** (0.3.0-dev.26) when the
1090
+ new directory is not under the cwd, instead of `cd ../../../..`.
1091
+
1092
+ - **A fresh distribution runs no gh for a placeholder org** (0.3.0-dev.25,
1093
+ from a consumer's dev.24 run): `den create` wrote `github_org:
1094
+ your-github-org`, and Local Dev then ran `gh repo list your-github-org`
1095
+ on every load. The scaffold now leaves `github_org` commented out, with
1096
+ an example. With `github_org` unset or still a placeholder
1097
+ (`your-github-org`, `your-org`, `<your-org>`, `example-org`) GitHub is
1098
+ "not configured": `/api/github/repos` is empty and runs no gh (no
1099
+ fallback to catalog owners or a git remote), and `register`'s GitHub
1100
+ list ignores a placeholder.
1101
+ - **The lock's not-configured note names your config file** (0.3.0-dev.25):
1102
+ with no `secrets.refs.sudo`, the admin-password popover says "Saving the
1103
+ password is off: set secrets.provider and secrets.refs.sudo in
1104
+ config/acme.yml.", the distribution's real file, from the new
1105
+ `configFile` field of `/api/sudo-password/vault-status`.
1106
+ - **`register` with no terminal exits cleanly** (0.3.0-dev.25): with stdin
1107
+ closed or piped, the interactive `register` crashed with
1108
+ ERR_USE_AFTER_CLOSE. It now exits 2 (a usage error, since 0.2.8's
1109
+ check; it exited 1 before): "register needs a terminal (stdin is not a
1110
+ TTY)", naming the non-interactive form, `register <path> [--name
1111
+ <repo>] [--gh <owner/repo>]`.
1112
+ - **`bin/den` sets no backend account** (0.3.0-dev.24, from a consumer's
1113
+ dev.23 run): the launcher exported `OP_ACCOUNT` with a placeholder
1114
+ domain for every command, whatever the secrets backend, so it reached
1115
+ the web server even under doppler. It now adds nothing; the 1password
1116
+ strategy sets it from `secrets.onepassword.account`, or the caller's own
1117
+ is kept.
1118
+ - **`backendAvailable` is true for a usable backend even with no sudo ref**
1119
+ (0.3.0-dev.24): dev.23 reported false whenever `secrets.refs.sudo` was
1120
+ unset, so the deprecated `opAvailable` alias changed meaning. With no
1121
+ ref it now reflects detect() (a PATH check, nothing spawned) for an
1122
+ explicitly chosen backend; `configured: false` says the ref is missing.
1123
+ An unset provider and `none` still report false and are never probed.
1124
+ - **Raster marks are scaled to fit, never cropped** (0.3.0-dev.23).
1125
+ - **Den adds no 1Password account to anything it starts** (0.3.0-dev.23).
1126
+ The web server daemon (`web up`, `web restart`) was always given
1127
+ `OP_ACCOUNT`, with a built-in domain when unset, and `den.mk` exported
1128
+ one to every recipe. Now only the `1password` strategy gives one, from
1129
+ `secrets.onepassword.account` or your own `OP_ACCOUNT`; no other backend
1130
+ gives any, and `den.mk` sets none.
1131
+ - **1Password make targets exist only when 1Password is selected**
1132
+ (0.3.0-dev.23). `make <cli>-setup-1password` and `make auth-configure`
1133
+ moved to `make/secrets-1password.mk`, which `den.mk` includes only when
1134
+ `secrets.provider` (or `<PREFIX>_SECRETS_PROVIDER`) is `1password`; every
1135
+ other distribution no longer has them.
1136
+ - **"Register a managed repo" never lists the engine's owner's repos**
1137
+ (0.3.0-dev.23). With no `github_org`, the org fell back to the git remote
1138
+ of the engine's own checkout, so a linked or git-installed engine listed
1139
+ its owner's repos for an unrelated project. The fallback is now the
1140
+ distribution's own checkout (only when it is the top of its own repo),
1141
+ else the "set github_org" error. The 1Password setup scripts likewise
1142
+ read `developer_keys.env_file` and write `admin-auth-<env>.json` under the
1143
+ distribution root, not the engine's.
1144
+ - **Settings keeps your pick marked while it scrolls** (0.3.0-dev.22, SA's
1145
+ dev.21 run): a click or Enter on a category no longer walks
1146
+ `aria-current` through every category the smooth scroll passes. Your
1147
+ pick holds until the scroll ends (`scrollend`, else once it is still,
1148
+ within about a second); then the scroll-spy takes over again.
1149
+ - **Below 480px the top bar shows only the mark** (0.3.0-dev.22, SA's
1150
+ dev.21 run): the product name is visually hidden there and stays the
1151
+ brand link's accessible name; from 480px it truncates as in dev.21.
1152
+ - **0.2.6 is in the 0.3 line** (0.3.0-dev.21): `type: container` health
1153
+ checks, the server identity check behind `web restart` and `web down`,
1154
+ the MCP server's identity, and `service install --plan`. See 0.2.6 below.
1155
+ - **Settings marks the category you picked** (0.3.0-dev.21, SA's dev.20
1156
+ run): `/settings#distribution`, or a click on the last short section,
1157
+ marked the one before it as current, because the page could not scroll
1158
+ far enough for the section to reach the top. At the end of the page,
1159
+ your pick wins if it is in view.
1160
+ - **"IDE for this workspace" saves on change** (0.3.0-dev.21), like every
1161
+ other row; its Save button is gone.
1162
+ - **The top bar fits at 400px** (0.3.0-dev.21): a long product name
1163
+ truncates and the stale-server pill shows only its ⚠ (named "Stale
1164
+ server: restart"), so the page no longer scrolls sideways.
1165
+ - **Status drawer rows are named by service, state and latency**
1166
+ (0.3.0-dev.21), e.g. "shop/db, healthy, 9 ms". What the check ran is
1167
+ the row's description and tooltip, not its name.
1168
+ - **The rail's Settings item opens Settings** (0.3.0-dev.19): in dev.18 it
1169
+ went to `/local-dev?settings=1`, which nothing read, so it landed on
1170
+ Local Dev. It now goes to `/settings`, and the old URL redirects there.
1171
+ Plain `/settings` opens at the top, on the title and the search box; a
1172
+ link that names a category (`/settings#editor`) scrolls to it and puts
1173
+ focus on its heading.
1174
+ - **The rail's × keeps focus in the rail** (0.3.0-dev.19, SA's dev.18
1175
+ run), as Delete does: the next pinned row, else the previous one, else
1176
+ the first item (Enter on "Unpin <id>" dropped focus to the top of the
1177
+ document). One function for both.
1178
+ - **The narrow rail is a modal drawer** (0.3.0-dev.19, SA's dev.18 run).
1179
+ Below 720px, ☰ moves focus into the rail (the current page), makes the
1180
+ top bar and the page `inert` behind a scrim, and keeps Tab inside;
1181
+ Escape or a click on the scrim closes it, sets `aria-expanded` back and
1182
+ returns focus to ☰ (Escape did nothing before).
1183
+ - **Names screen readers read** (0.3.0-dev.19, SA's dev.18 run): a rail
1184
+ group is named by its label alone (`aria-labelledby` points at the label,
1185
+ not the heading with its tag; Chrome read "XGlobex"); a tagged menu
1186
+ row reads "default, focus", not "defaultfocus"; the ✓ on a checked menu
1187
+ row is drawn with no alt text (`aria-checked` says it); ◐ ▾'s glyphs are
1188
+ hidden from readers. A scope button's `aria-controls` names its menu
1189
+ only while the menu is open.
1190
+ - **A page registered before its group no longer warns** (0.3.0-dev.19,
1191
+ a consumer's dev.18 run). Queuing a page before its group is a legitimate
1192
+ order; the warning now waits until the queue has drained and a tick has
1193
+ passed after `den-shell:ready`, and fires only for a group that is still
1194
+ missing (the same for a setting and its category).
1195
+ - **Opening a run's output no longer hides the lowest graph cards**
1196
+ (0.3.0-dev.19, a consumer's dev.18 run). Automatic fits (first draw, layout
1197
+ changes, and the re-fit as the output panel opens) now show the whole
1198
+ graph whenever it fits at 35% zoom, the Fit button's limit; only a graph
1199
+ too big for that starts at its top at 60%. The panel already had its
1200
+ own room below the graph: stopping the fit at 60% cut the lowest cards
1201
+ off at the graph's bottom edge, where the panel begins, so a click
1202
+ landed on the panel.
1203
+ - **The ORG label is the org's name, not its id** (0.3.0-dev.18). With no
1204
+ `org.title` in the org config, the label was the id (`edge.org`). It is
1205
+ now `org.title`, then the `orgs:` label of `github_org` (the name the
1206
+ Environments page shows), then the id.
1207
+ - **A pin toggle follows the rail** (0.3.0-dev.18). Unpinning from the rail
1208
+ (× or Delete) left the Services card's ☆ and the graph panel's reading
1209
+ "Unpin X" (`aria-pressed=true`) until a reload.
1210
+ - **Delete on a pinned row keeps focus in the rail** (0.3.0-dev.18): on the
1211
+ next pinned row, else the previous one, else the first item (it fell to
1212
+ the top of the document).
1213
+ - **A pinned row is named by its id** (0.3.0-dev.18), not "shop-web×":
1214
+ the × is its own control, out of the tab order, named "Unpin
1215
+ shop-web", its glyph hidden from screen readers; the row has
1216
+ `aria-keyshortcuts="Delete"`.
1217
+ - **One main landmark per page** (0.3.0-dev.18). Every page's content is in
1218
+ `<main id="den-content">` (the skip link's target); the four pages that
1219
+ had a `<main>` of their own around part of it, and the two that had
1220
+ none, now have exactly one.
1221
+ - **A distribution group is named once** (0.3.0-dev.18): its tag is hidden
1222
+ from screen readers and read as the group's description ("from globex"),
1223
+ not "Globex globex".
1224
+ - **Registration mistakes warn** (0.3.0-dev.18): a non-number `order`, or
1225
+ a `registerNav` into a group not registered yet, logs a `console.warn`
1226
+ naming the field. The page waits for its group instead of being lost.
1227
+ - **Local Dev's output panel no longer covers graph cards** (0.3.0-dev.18).
1228
+ When the panel opened (a run's tab), the graph's viewport shrank but the
1229
+ graph kept its fit, so the lower cards sat under the panel and couldn't
1230
+ be clicked. A viewport that changes size re-fits the graph unless you
1231
+ have panned or zoomed it yourself.
1232
+
1233
+ - **An app is never painted down before its checks have answered**
1234
+ (0.3.0-dev.17). One cold load in twenty painted an app red for ~100ms:
1235
+ only its one down service had answered, and as the only row it read as
1236
+ the entry service. A poll that overlapped the first one started from
1237
+ nothing and drew what had landed. Until every check of a repo has
1238
+ answered (a pending check has), the repo is unknown (neutral, "still
1239
+ checking"); a repo the last complete cycle described keeps its earlier
1240
+ answers meanwhile. The rail's Services badge and pinned dots read the same
1241
+ rollup, so they count only completed downs.
1242
+ - **Shell accessibility** (0.3.0-dev.17): a "Skip to content" link comes
1243
+ first on every page; the rail is one tab stop (the current page) with
1244
+ ↑/↓, Home and End moving within it; the Logs link's name is "Logs" or
1245
+ "Logs, 2 open", not "Logs0"; the Tests page's title is "<title> · Tests".
1246
+ - **"What's new" is a modal dialog, and reopens at the top** (0.3.0-dev.17).
1247
+ A reopen after scrolling kept the old position: the scroll was reset
1248
+ while the overlay was still hidden, where it doesn't stick; it is reset
1249
+ once the dialog shows. The box is `role="dialog"` with `aria-modal` and
1250
+ its heading as the label; it takes focus (the versions toggle, else Got
1251
+ it), keeps Tab inside, and gives focus back on close (to the version pill
1252
+ after a first-visit open). A browser test covers Escape, × and Got it,
1253
+ collapsed and expanded; CI installs Playwright's chromium for it.
1254
+ - **One bar on Logs** (0.3.0-dev.17). The cols/rows toggle moved into the
1255
+ Logs toolbar from a band of its own.
1256
+ - **One workspace control on Services** (0.3.0-dev.18). Services' own
1257
+ workspace select (beside the top bar's) is gone: the top bar's Workspace
1258
+ ▾ says what the page lists; its status moved to the Output bar.
1259
+ - The appearance control (◐) sits at the header's far right on every page
1260
+ and never leaves the screen: the nav shrinks and scrolls and the health
1261
+ dots give way first (the Tests page's top bar overflowed at 1440 and 1024).
1262
+ - Org colors from the config are no longer text: the graph's org frames and
1263
+ the panel's breadcrumb label in `--text` with the color as a dot (they read
1264
+ at 2.5:1 on a light ground), and the frame's ground is neutral. A test
1265
+ bans a config color as `color:`.
1266
+ - The active workspace row in the Local Dev sidebar is a calm neutral tint
1267
+ in both modes (it was a loud accent tint in light mode).
1268
+ - `plugin list` with broken dirs in `plugins/` (0.3.0-dev.8, reported by
1269
+ consumers on dev.7) prints exactly one row for every directory there, every
1270
+ time. Before, it crashed with no rows when `plugins.lock.json` couldn't be
1271
+ parsed, skipped a symlink, and hid a dir whose lock entry named another
1272
+ id. `plugin validate` reports each of these too. Nothing in a plugin dir
1273
+ makes either command throw.
1274
+ - A dir name that isn't an id (such as `plugins/x/`) now gives the real
1275
+ rule: "2–40 characters: lowercase letters, digits or dashes, starting with
1276
+ a letter". A missing manifest is reported as well (0.3.0-dev.8).
1277
+ - A dir with no manifest reads "local (plugins/scratch): no den-plugin.json
1278
+ or den-theme.json, so it isn't a plugin" in `plugin list`. "Not valid
1279
+ under this <cli>: …" is now kept for a recognized plugin that fails
1280
+ validation (0.3.0-dev.8).
1281
+ - `theme list` still shows themes only, and still exits 0 when other plugin
1282
+ dirs are broken. It now adds one line, "N plugin dir(s) have errors: run
1283
+ `<cli> plugin list`" (0.3.0-dev.8).
1284
+
1285
+ - `theme list`'s footer counts every broken plugin dir, whether or not it
1286
+ is a row above, and gets the plural right: "1 plugin dir has errors" /
1287
+ "4 plugin dirs have errors: run `<cli> plugin list`" (0.3.0-dev.9; it
1288
+ counted only the hidden ones, as "N plugin dir(s)").
1289
+ - `plugin validate` says the same as `plugin list` for a dir with no
1290
+ manifest: "no den-plugin.json or den-theme.json, so it isn't a plugin",
1291
+ from one constant. `plugin list`'s and `theme list`'s id column fits the
1292
+ longest id, so rows stay aligned with ids up to 40 characters
1293
+ (0.3.0-dev.9).
1294
+ - **The header's health row no longer cuts repos off** (0.3.0-dev.9, a
1295
+ regression from the one-line header earlier in 0.3.0). The row was clipped
1296
+ silently: whole repos were hidden, a name could be cut mid-word, and a
1297
+ hidden repo that was down didn't show. It now:
1298
+ - orders problems first: down, then degraded, then healthy, then unknown
1299
+ (0.3.0-dev.11; dev.9 put unknown before healthy), within and across
1300
+ application boxes;
1301
+ - shows only items that fit whole;
1302
+ - ends with a "+N" chip for the rest, named "N more repos" and saying
1303
+ "+N · 1 down" when a hidden repo has a problem. The chip opens the full
1304
+ list; Escape closes it and returns focus to the chip.
1305
+ - fits again on resize, when fonts load, and when the rest of the header
1306
+ squeezes it.
1307
+
1308
+ An interim fix until the slice-3 status chip. The new module is
1309
+ `public/health-fit.js`.
1310
+
1311
+ - **One repo health rule for the header, the chip, the graph and
1312
+ `<cli> status`** (0.3.0-dev.10). The header row and `status` read only
1313
+ the entry service, so a repo whose web answered but whose mailpit
1314
+ container was gone read healthy. It sorted with the healthy repos and
1315
+ never reached the chip's count, while the graph showed the down service.
1316
+ All four now use one function, `public/health-rollup.js`, typed for
1317
+ TypeScript in `src/shared/health/rollup.ts`. Only checked signals count:
1318
+ - **down**: the entry service (the manifest `entrypoint`, else the
1319
+ service named like the repo, else the only one) is down;
1320
+ - **degraded**: another checked service is down;
1321
+ - **ok**: at least one signal is checked and none fails;
1322
+ - **unknown**: nothing is checked. It is neutral, never green, and sorts
1323
+ last, after healthy (0.3.0-dev.11). A service not yet probed is
1324
+ unknown. `status` keeps the graph's order.
1325
+
1326
+ A repo of 26 services with one probe is judged by that probe.
1327
+ - `status` prints one row per repo with its rolled-up state and names
1328
+ what is down, e.g. "degraded" with "mailpit down". It says "N not
1329
+ checked" when services aren't probed. An entrypoint-less repo is
1330
+ rolled up too, and `--verbose` still lists every service.
1331
+ - The graph's repo card shows degraded in `--warn`.
1332
+ - In the row, same-state repos sort the focused workspace's first, then
1333
+ by name. `/api/health` rows carry `focused`.
1334
+ - **The health row no longer hides everything behind one big application
1335
+ box** (0.3.0-dev.10).
1336
+ - When the boxes don't all fit, the row shows repos one by one. It places
1337
+ every down or degraded repo that fits first, and a problem that is too
1338
+ wide never blocks a later one. Then it adds the rest, in order.
1339
+ - The chip shows the richest text that fits ("+9 · 1 down, 1 degraded",
1340
+ then "+9 · 1 down"). Its name and tooltip always carry the full count.
1341
+ - The row now takes the header's free space. Before, it shrank to its
1342
+ content, and the page's own spacer took half of what was left.
1343
+ - **The health row is never an empty box on a cold load** (0.3.0-dev.11).
1344
+ Before the first health result (the application and editor lookups and
1345
+ the stream come first) the row painted nothing, and since dev.10 it takes
1346
+ the header's free space, so it read as a wide empty box with no refresh
1347
+ button. It now shows a neutral "…" and the refresh button at once. A fit
1348
+ that runs before the row or its items have a width keeps everything
1349
+ boxed and runs again next frame (`fitPlan` in `public/health-fit.js`).
1350
+ It fits again on the first data and when fonts load. The brand, version
1351
+ badge and pills beside the row never shrink for it.
1352
+ - **The health row's fit, checked against a real 6+3 workspace**
1353
+ (0.3.0-dev.11). Measured names of 71 to 182px: all nine flat need about
1354
+ 1070px, so a 1002px row shows 7 and "+2", and a 1282px row shows both
1355
+ boxes whole. The fit was right; these numbers are now a test, with an
1356
+ 11-repo 7+3+1 case for the flat fallback.
1357
+ - **The "+N" chip names a hidden problem in its text** (0.3.0-dev.12).
1358
+ With only room for the chip, a hidden degraded repo read "+11" with an
1359
+ amber ring, and "1 degraded" was only in its accessible name. The ladder
1360
+ is now "+11 · 1 down, 1 degraded", then the worst one alone ("+11 · 1
1361
+ down"), then compact ("+11 · ⚠2", 0.3.0-dev.13), down and degraded named
1362
+ the same way. Bare "+N" only when no
1363
+ hidden repo has a problem. The fit keeps room for that text
1364
+ (`fitPlan`'s `chipWidthOf`).
1365
+ - **Escape from the chip's list returns focus to the chip, even after a
1366
+ re-fit** (0.3.0-dev.12). A health tick or a font load while the list was
1367
+ open laid the row out again with a new chip node, so focus fell to the
1368
+ page (the breadcrumb). The chip is now one node across re-fits, keeps its
1369
+ focus, and stays the list's anchor; when a re-fit drops the chip, focus
1370
+ moves to the row's refresh button.
1371
+ - **The health row settles once on a fresh load** (0.3.0-dev.13). It
1372
+ reshuffled three or four times between 0.6s and 3.6s as streamed results
1373
+ arrived. Now a first cycle (a page load, an environment switch) keeps the
1374
+ "…" placeholder until the whole cycle is in (or 8s pass with a slow probe
1375
+ still out), then fits once. Later data repaints are debounced by 300ms;
1376
+ resize and font re-fits stay immediate (`paintGate` in
1377
+ `public/health-fit.js`).
1378
+ - **The refresh button is never clipped** (0.3.0-dev.13). At 124px,
1379
+ "+11 · 1 degraded" pushed ⟳ out of the row. The chip's ladder ends in a
1380
+ compact form, "+11 · ⚠1" (every hidden problem counted), used before the
1381
+ problem would be dropped; its name and tooltip keep the full text. If
1382
+ even that doesn't fit, the chip's text gives way, not ⟳. The chip is
1383
+ measured at its natural width.
1384
+ - **One shell for every page: a top bar and a left rail** (0.3.0-dev.14,
1385
+ shell slice 2). Every page had its own copy of the header, nav and header
1386
+ CSS (six copies; Environments loaded none of the header's behaviour).
1387
+ `public/shell.js` now draws one frame on every page, and the pages keep
1388
+ only their content:
1389
+ - **Top bar** (48px): the theme's mark and the title in the theme's
1390
+ display face (`var(--display)`; the pages had used the mono face), the
1391
+ version pill, the health row and chip (unchanged), the workspace focus
1392
+ pill, one env picker and ◐.
1393
+ - **Rail**: Local Dev, Services, Environments, Releases, Logs, Tests, the
1394
+ current page marked `aria-current="page"`. ↑/↓, Home and End move
1395
+ between pages; it collapses to icons below 1100px (labels stay for
1396
+ assistive tech). No horizontal page scroll at 1024.
1397
+ - **One env picker for the whole shell.** Tests, Services and Logs each
1398
+ had their own; the choice is now one control, remembered across pages.
1399
+ Local Dev and Releases keep checking local health whatever it says
1400
+ (`data-health-env`), and Releases' own selector is now labelled as the
1401
+ deploy target it is.
1402
+ - A page joins the shell with `<div id="den-shell" data-page="…">` and
1403
+ `<script src="/shell.js">`; `header.js` drives it (`envReady()`,
1404
+ `healthEnv()`).
1405
+ - **Header fixes** (0.3.0-dev.14): a long workspace name ends in an
1406
+ ellipsis with the full name in its tooltip, and names in the bar never
1407
+ wrap mid-word; the Tests page's "ENV" label no longer collides with its
1408
+ select (the picker is the shell's).
1409
+ - **Environments groups services under their org again** (0.3.0-dev.14).
1410
+ Every service showed under "no org": the page read each row's `org` from
1411
+ `/api/health`, which never sends one. It now takes each repo's org from
1412
+ `/api/orgs?repos=1`, like the graph.
1413
+ - **The first health paint waits for the whole cycle** (0.3.0-dev.14). A
1414
+ stream that broke after its first results counted as a finished cycle,
1415
+ so a cold load could paint a partial row. Only the server's `done` ends
1416
+ a cycle now; a broken stream is completed from `/api/health` (or the 8s
1417
+ cap paints what landed, and a check with no result is never shown as
1418
+ down). `healthCycle` in `public/health-fit.js`.
1419
+ - **The Theme menu says when your theme isn't in use** (0.3.0-dev.10). A
1420
+ stored theme that fails validation selects nothing, instead of looking
1421
+ like "Org default". One line explains: "Your theme <id> isn't valid
1422
+ (<first problem>), so <name> is in use", or "isn't installed". Picking
1423
+ any option replaces the stored choice.
1424
+
1425
+ ### Migrations
1426
+
1427
+ - 0.3.0-dev.40, the edge's owner labels: plan one `edge up`. The edge's
1428
+ compose.yml gains `den.edge.root` and `den.edge.org` labels, so the first
1429
+ `edge up` (or `stack up`) after upgrading recreates the edge once, from
1430
+ its own state dir. Routes drop only while the container restarts, then
1431
+ stay up: the edge rejoins running stacks' networks. Run it once, from
1432
+ the edge's own dir, when a brief blip is acceptable. A scratch or test root that sets `DEN_EDGE_DIR` needs an
1433
+ `edge.org` of its own; with the real one it is refused.
1434
+ - 0.3.0-dev.40, commands that detach: a command other than up and restart
1435
+ that should keep running detached says `detachable: true`.
1436
+ - 0.3.0-dev.39, the UI language: nothing required. With no `language:`
1437
+ in `user.yml` the UI is English, as before. A distribution or plugin
1438
+ that adds its own words with `denShell.addMessages(lang, table)` can
1439
+ add `ja` and `es` tables the same way.
1440
+ - 0.3.0-dev.39, pre-dev.37 records: nothing required. Their next `up`,
1441
+ `down` or `restart` from the CLI or the web adopts or drops them (said).
1442
+ - 0.3.0-dev.38, the restart banner: **restart the web server once after
1443
+ installing.** The first upgrade from a server older than dev.38 can't
1444
+ show the "Den was updated" banner: the old server has no MD-2 code, so
1445
+ it keeps serving the new pages from the old engine until it restarts
1446
+ (`<cli> web restart`, or your service restart). From dev.38 on, the
1447
+ banner says when a restart is needed.
1448
+ - 0.3.0-dev.38, `/api` error bodies: **they changed from text to JSON.**
1449
+ Every `/api/*` error now answers `Content-Type: application/json` with
1450
+ `{ ok: false, error, code, requestId }`, where some routes sent
1451
+ `text/plain` (a message, or an empty body). A script that read the text
1452
+ should read `.error`; a status check needs nothing. Den's own pages and
1453
+ the MCP server read the new shape.
1454
+ - 0.3.0-dev.38, records from before dev.37: nothing required. A service
1455
+ still running from such a record is adopted on its next `up` or `down`
1456
+ (by its port, else its pid); one that can't be matched is dropped with a
1457
+ warning and never signalled.
1458
+ - 0.3.0-dev.37, `down` and process groups: nothing required. A service
1459
+ started by an older Den has no recorded group, so its first `down
1460
+ <repo>` after the upgrade (for a service with no down command and no
1461
+ container) warns, drops that record and exits non-zero: stop it
1462
+ yourself once; its next `up` records the group.
1463
+ - 0.3.0-dev.36, `stack_namespace`: optional, no change required. Without it
1464
+ every Compose project, container, volume and network keeps its name.
1465
+ `stack_namespace` in a blueprint needs Den 0.3.0-dev.36 or later: an
1466
+ older Den rejects the unknown key.
1467
+ - 0.3.0-dev.25, edge TLS. Nothing is required: every existing edge config
1468
+ renders as before (only `edge trust` under `issue_cert: false` with no
1469
+ `caroot` now refuses). To silence the warnings, say what your config already means,
1470
+ in the org config (`config/<cli>.yml`):
1471
+ - **No `tls`, `caroot` or `issue_cert`** (the implicit default):
1472
+
1473
+ ```yaml
1474
+ edge:
1475
+ tls: { strategy: mkcert }
1476
+ ```
1477
+
1478
+ Or, if the edge needs no https, `tls: none`.
1479
+ - **`caroot: <ca>`**: replace it with
1480
+
1481
+ ```yaml
1482
+ edge:
1483
+ tls: { strategy: mkcert, ca: <ca> }
1484
+ ```
1485
+
1486
+ - **`issue_cert: false`** (with `include` and `mounts`): replace it with
1487
+
1488
+ ```yaml
1489
+ edge:
1490
+ tls:
1491
+ strategy: files
1492
+ include: edge/dynamic # was edge.include
1493
+ mounts: { <host path>: <container path> } # was edge.mounts
1494
+ ```
1495
+
1496
+ `include` and `mounts` may also stay where they were. With `caroot`
1497
+ too, move it to `ca: <ca>` under `tls`.
1498
+ - `edge.tls` beside `caroot` or `issue_cert` is an error: move them.
1499
+ - `github_org: your-github-org` from an earlier `den create`: set your
1500
+ real org, or delete the line.
1501
+ - 0.3.0-dev.23, secrets. Nothing is required of a distribution that uses
1502
+ no secrets; the YAML below goes in the org config (`config/<cli>.yml`).
1503
+ - **Relied on the old 1Password defaults** (the saved sudo password or
1504
+ the journeys key in the developer-keys item): add the old layout as
1505
+ your own refs. These are the exact former defaults; `<PREFIX>` is your
1506
+ env prefix (`ACME` for `acme`, `DEN` for Den's own checkout).
1507
+
1508
+ ```yaml
1509
+ secrets:
1510
+ provider: 1password
1511
+ onepassword:
1512
+ account: example.1password.com # your account; Den injects none now
1513
+ refs:
1514
+ sudo:
1515
+ { vault: Employee, item: Developer API Keys, field: <PREFIX>_LOCAL_ADMIN_PWD }
1516
+ journeys_agent_key:
1517
+ { vault: Employee, item: Developer API Keys, field: CURSOR_API_KEY }
1518
+ developer_keys: { vault: Employee, item: Developer API Keys }
1519
+ ```
1520
+ - **Relied on doppler or env for the saved sudo password** (the flat
1521
+ `<PREFIX>_LOCAL_ADMIN_PWD` key):
1522
+
1523
+ ```yaml
1524
+ secrets:
1525
+ provider: doppler # or env
1526
+ refs:
1527
+ sudo: { key: <PREFIX>_LOCAL_ADMIN_PWD }
1528
+ ```
1529
+
1530
+ - **Relied on the implicit doppler default**: it still works. To silence
1531
+ the deprecation warning, say so: `secrets: { provider: doppler }`. A
1532
+ distribution that uses no secrets store can set `secrets: { provider:
1533
+ none }`.
1534
+ - A page or script that read `opAvailable` from
1535
+ `/api/sudo-password/vault-status` reads `backendAvailable`.
1536
+ - A `make auth-configure` or `make <cli>-setup-1password` in your own
1537
+ scripts needs `secrets.provider: 1password`.
1538
+ - None. Optional: set `brand.theme: <id>` in the org config to give the
1539
+ distribution's web UI its own default theme.
1540
+ - A `plugins/<id>/` with no `den-plugin.json` or `den-theme.json` now fails
1541
+ `plugin list` and `plugin validate`: give it a manifest or move it out of
1542
+ `plugins/`. A theme added from a `file://` path (0.3.0-dev.2 warned) stays
1543
+ listed, but re-add it from its published URL before committing the lock.
1544
+ - 0.3.0-dev.20: the header's health row (`#health`) and Local Dev's
1545
+ `#sudo-banner` are gone, and so are `planRow`, `fitPlan`, `moreChip`,
1546
+ `mountMoreChip`, `openMoreList` and `closeMoreList` in
1547
+ `public/health-fit.js`. A distribution page that read them reads the
1548
+ `<cli>:health-update` event (unchanged) or the status chip (`#den-status`)
1549
+ instead, and unlocks with `requireUnlock()` from `/header.js`.
1550
+ - 0.3.0-dev.8: a `den-theme.json` used as a plugin needs an `author`
1551
+ (`theme new` sets one). A plugin's `den` range is now enforced: write
1552
+ `>=0.3.0-0 <0.4.0` to admit Den's 0.3.0 prereleases, because `>=0.3.0`
1553
+ excludes them.
7
1554
  ## [0.2.8] - 2026-11-05
8
1555
 
9
1556
  ### Added