@bongos/core 1.24.30 → 1.24.31

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 (101) hide show
  1. package/.bongos-core.json +153 -103
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +8 -0
  4. package/clients/bongos-client/index.cjs +8 -0
  5. package/clients/bongos-client/index.d.ts +8 -0
  6. package/clients/bongos-client/index.mjs +8 -0
  7. package/docs/api/openapi.json +139 -1
  8. package/docs/api-reference.md +7 -3
  9. package/docs/copy-inventory.md +192 -187
  10. package/docs/copy-registry.json +530 -458
  11. package/docs/module-api-changelog.md +2 -2
  12. package/docs/page-inventory.json +15 -35
  13. package/docs/page-readings.json +226 -263
  14. package/modules/copy-desk/module.json +1 -1
  15. package/modules/copy-desk/routes/copy-desk-studio.js +103 -0
  16. package/modules/copy-desk/tests/copy_no_cms.mjs +2 -2
  17. package/modules/hall-ui/public/ai-studio.html +1 -1
  18. package/modules/hall-ui/public/atlas.html +1 -1
  19. package/modules/hall-ui/public/blockers.html +1 -1
  20. package/modules/hall-ui/public/board-room.html +1 -1
  21. package/modules/hall-ui/public/city.html +1 -1
  22. package/modules/hall-ui/public/collab.html +1 -1
  23. package/modules/hall-ui/public/commands.html +1 -1
  24. package/modules/hall-ui/public/copy-desk.html +1 -1
  25. package/modules/hall-ui/public/craft-mode.js +1 -1
  26. package/modules/hall-ui/public/deploy.html +1 -1
  27. package/modules/hall-ui/public/diagrams.html +1 -1
  28. package/modules/hall-ui/public/drachmae.html +1 -1
  29. package/modules/hall-ui/public/fleet.html +1 -1
  30. package/modules/hall-ui/public/gate.html +1 -1
  31. package/modules/hall-ui/public/goal-map.html +1 -1
  32. package/modules/hall-ui/public/goals.html +1 -1
  33. package/modules/hall-ui/public/government.html +1 -1
  34. package/modules/hall-ui/public/hall-render.js +62 -86
  35. package/modules/hall-ui/public/home-city-draw.js +360 -52
  36. package/modules/hall-ui/public/home-city.css +47 -11
  37. package/modules/hall-ui/public/home-city.js +156 -40
  38. package/modules/hall-ui/public/home-gallery.css +129 -0
  39. package/modules/hall-ui/public/home-gallery.js +349 -0
  40. package/modules/hall-ui/public/home-studio.css +72 -0
  41. package/modules/hall-ui/public/home-studio.js +203 -0
  42. package/modules/hall-ui/public/idea.html +1 -1
  43. package/modules/hall-ui/public/ideas.html +1 -1
  44. package/modules/hall-ui/public/index.html +4 -1
  45. package/modules/hall-ui/public/market-watch-item.html +1 -1
  46. package/modules/hall-ui/public/market-watch-rules.html +1 -1
  47. package/modules/hall-ui/public/market-watch-settings.js +6 -1
  48. package/modules/hall-ui/public/market-watch.html +141 -54
  49. package/modules/hall-ui/public/market-watch.js +54 -0
  50. package/modules/hall-ui/public/market-watch.states.json +2 -1
  51. package/modules/hall-ui/public/modules.html +1 -1
  52. package/modules/hall-ui/public/my-agents.html +1 -1
  53. package/modules/hall-ui/public/primer.html +1 -1
  54. package/modules/hall-ui/public/profile.html +1 -1
  55. package/modules/hall-ui/public/project-settings.html +1 -1
  56. package/modules/hall-ui/public/ranks.html +1 -1
  57. package/modules/hall-ui/public/roadmap.html +1 -1
  58. package/modules/hall-ui/public/roster.html +1 -1
  59. package/modules/hall-ui/public/sessions.html +1 -1
  60. package/modules/hall-ui/public/settings.html +1 -1
  61. package/modules/hall-ui/public/shell.js +2 -3
  62. package/modules/hall-ui/public/street.html +1 -1
  63. package/modules/hall-ui/public/studio.html +9 -1
  64. package/modules/hall-ui/public/style.css +74 -27
  65. package/modules/hall-ui/public/task.html +1 -1
  66. package/modules/hall-ui/public/thinking.html +1 -1
  67. package/modules/hall-ui/public/tweak-editor.html +1 -1
  68. package/modules/hall-ui/public/watch.html +1 -1
  69. package/modules/hall-ui/public/work.html +1 -1
  70. package/modules/hall-ui/records/home-city.md +123 -0
  71. package/modules/hall-ui/records/loader.md +1 -1
  72. package/modules/hall-ui/records/market-watch-settings.md +16 -3
  73. package/modules/hall-ui/records/market-watch.md +3 -1
  74. package/modules/lifecycle/home-city.js +58 -10
  75. package/modules/lifecycle/home-gallery.js +320 -0
  76. package/modules/lifecycle/migrations/lifecycle_020_work_appreciations.sql +36 -0
  77. package/modules/lifecycle/module.json +1 -0
  78. package/modules/lifecycle/routes/home-gallery.js +37 -0
  79. package/modules/market-watch/routes/settings.js +1 -1
  80. package/package-lock.json +2 -2
  81. package/package.json +1 -1
  82. package/release-notes.json +29 -1
  83. package/scripts/bongos/leak-scan-allowlist.js +8 -0
  84. package/scripts/hall-preview/README.md +1 -0
  85. package/scripts/hall-preview/server.js +10 -0
  86. package/src/bongos/serve-internal.js +17 -3
  87. package/src/module-api.js +1 -1
  88. package/tests/hall_builders_page_pass.mjs +18 -27
  89. package/tests/hall_loading.mjs +1 -1
  90. package/tests/hall_nav.mjs +11 -4
  91. package/tests/hall_page_gate_map.mjs +7 -4
  92. package/tests/home_city.mjs +200 -5
  93. package/tests/home_city_widget.mjs +167 -27
  94. package/tests/home_gallery.mjs +255 -0
  95. package/tests/home_gallery_widget.mjs +220 -0
  96. package/tests/home_studio.mjs +169 -0
  97. package/tests/home_studio_widget.mjs +157 -0
  98. package/tests/market_watch_pages.mjs +162 -0
  99. package/tests/nav_permission_atoms.mjs +2 -2
  100. package/modules/hall-ui/public/market-watch-settings.html +0 -115
  101. package/modules/hall-ui/public/market-watch-settings.states.json +0 -14
@@ -3339,8 +3339,53 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
3339
3339
 
3340
3340
  /* ---------- landing (signed-out) ---------- */
3341
3341
 
3342
- .landing { max-width: var(--measure); }
3343
- .landing__lede { font-size: 14px; color: var(--ink-soft); }
3342
+ /* The signed-out page is a sign-in screen, not the hall: html[data-door="out"]
3343
+ (hall-render.js markSignedOut) drops the rail, the top bar, the foot and the
3344
+ page head, and centres one card in the window. The h1 stays in the markup for
3345
+ screen readers (one-header-main-h1), visually hidden. */
3346
+ html[data-door="out"] .app-sidebar,
3347
+ html[data-door="out"] .app-scrim,
3348
+ html[data-door="out"] .app-topbar,
3349
+ html[data-door="out"] .app-foot,
3350
+ html[data-door="out"] #welcome-reopen { display: none; }
3351
+ html[data-door="out"] .app-body { margin-left: 0; }
3352
+ /* body in the selector outranks Home's own body[data-page="home"] .page-head--hall. */
3353
+ html[data-door="out"] body .page-head {
3354
+ position: absolute;
3355
+ width: 1px; height: 1px;
3356
+ overflow: hidden;
3357
+ clip: rect(0 0 0 0);
3358
+ margin: 0; padding: 0;
3359
+ }
3360
+ html[data-door="out"] .app-content {
3361
+ min-height: 100vh;
3362
+ display: flex;
3363
+ align-items: center;
3364
+ justify-content: center;
3365
+ padding: 40px 16px;
3366
+ }
3367
+ html[data-door="out"] #hall-main { width: 100%; }
3368
+
3369
+ .landing {
3370
+ width: 100%;
3371
+ max-width: 400px;
3372
+ margin: 0 auto;
3373
+ padding: 40px 32px 28px;
3374
+ border-radius: var(--radius);
3375
+ background: var(--bg-card);
3376
+ text-align: center;
3377
+ }
3378
+ .landing__logo { display: block; width: 56px; height: 56px; margin: 0 auto 18px; overflow: visible; }
3379
+ .landing__ring { stroke: var(--ink); }
3380
+ .landing__planet { fill: var(--ink); }
3381
+ .landing__title {
3382
+ margin: 0 0 28px;
3383
+ font-family: var(--font-display);
3384
+ font-size: 26px;
3385
+ font-weight: 300;
3386
+ letter-spacing: -0.025em;
3387
+ color: var(--ink);
3388
+ }
3344
3389
  /* A link inside landing prose is still a control: 3px of vertical padding is
3345
3390
  what clears the 24px floor in a line that has no rhythm of its own
3346
3391
  (DESIGN.md §The footer applies the same rule to its links). */
@@ -3355,6 +3400,7 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
3355
3400
  display: inline-flex;
3356
3401
  align-items: center;
3357
3402
  justify-content: center;
3403
+ gap: 10px;
3358
3404
  min-height: 46px;
3359
3405
  padding: 0 22px;
3360
3406
  border-radius: var(--radius-pill);
@@ -3367,16 +3413,33 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
3367
3413
  cursor: pointer;
3368
3414
  }
3369
3415
  .landing__btn:hover { background: var(--accent-lift); text-decoration: none; color: var(--accent-on); }
3416
+ .landing__btn--block { display: flex; width: 100%; }
3417
+ .landing__gh { flex-shrink: 0; }
3418
+ .landing__btn--quiet {
3419
+ background: transparent;
3420
+ color: var(--ink);
3421
+ box-shadow: inset 0 0 0 1px var(--border-strong);
3422
+ }
3423
+ .landing__btn--quiet:hover { background: var(--surface-2); color: var(--ink); }
3370
3424
  .landing__note { font-size: 12.5px; color: var(--ink-faint); }
3371
3425
  .landing__error { color: var(--danger); }
3372
3426
  .landing__error-h { color: var(--danger); margin: 0 0 4px; font-size: 14px; }
3373
- .landing__request {
3374
- border: 0;
3375
- border-radius: var(--radius);
3376
- background: var(--surface-2);
3377
- padding: 18px 20px;
3378
- margin: 16px 0;
3427
+ .landing__door { margin: 18px 0 0; font-size: 13px; color: var(--ink-soft); }
3428
+ /* apply mode: the request form folds under the sign-in button. */
3429
+ .landing__disclosure { margin-top: 18px; }
3430
+ .landing__summary {
3431
+ display: inline-flex;
3432
+ align-items: center;
3433
+ min-height: 24px;
3434
+ list-style: none;
3435
+ cursor: pointer;
3436
+ font-size: 13px;
3437
+ color: var(--ink-soft);
3379
3438
  }
3439
+ .landing__summary::-webkit-details-marker { display: none; }
3440
+ .landing__summary:hover { color: var(--ink); }
3441
+ .landing__disclosure[open] .landing__summary { color: var(--ink); }
3442
+ .landing__request { margin-top: 14px; text-align: left; }
3380
3443
  .landing__request-h { margin: 0 0 6px; font-family: var(--font-display); font-size: 18px; font-weight: 300; letter-spacing: -0.025em; }
3381
3444
  .landing__request-lede { margin: 0 0 12px; font-size: 13px; color: var(--ink-soft); }
3382
3445
  .landing__request-lede--soft { color: var(--ink-faint); }
@@ -3386,28 +3449,12 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
3386
3449
  .landing__req-mark { color: var(--danger); }
3387
3450
  .landing__opt { color: var(--ink-faint); font-weight: 400; }
3388
3451
  .landing__form-hint { font-size: 12.5px; color: var(--ink-faint); margin: 0; }
3389
- .landing__btn--request { align-self: flex-start; }
3390
3452
  .landing__request-status { font-size: 13px; margin: 0; color: var(--ok); }
3391
3453
  .landing__request-status[data-kind="err"] { color: var(--danger); }
3392
- /* The returning builder's door, rendered only on the `apply` landing (task
3393
- 1002851). Same panel as the request block and the same button geometry, so
3394
- signing in reads as a real control rather than a footnote — but a quiet
3395
- fill, because the request path is what most of this page's traffic needs. */
3396
- .landing__returning {
3397
- border: 0;
3398
- border-radius: var(--radius);
3399
- background: var(--surface-2);
3400
- padding: 18px 20px;
3401
- margin: 16px 0;
3402
- }
3403
- .landing__returning .landing__cta { margin: 0; }
3404
- .landing__btn--quiet {
3405
- background: transparent;
3406
- color: var(--ink);
3407
- box-shadow: inset 0 0 0 1px var(--border-strong);
3454
+ .landing__signin-note { margin: 22px 0 0; font-size: 12px; color: var(--ink-faint); }
3455
+ @media (max-width: 480px) {
3456
+ .landing { padding: 28px 20px 22px; }
3408
3457
  }
3409
- .landing__btn--quiet:hover { background: var(--surface-2); color: var(--ink); }
3410
- .landing__signin-note { font-size: 13px; color: var(--ink-soft); }
3411
3458
 
3412
3459
  /* ---------- goals ---------- */
3413
3460
 
@@ -59,7 +59,7 @@
59
59
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
60
60
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
61
61
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
62
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
62
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
63
63
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
64
64
  <script src="/builders/palette.js" defer></script>
65
65
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N deep-link overlay
@@ -141,7 +141,7 @@
141
141
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
142
142
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
143
143
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
144
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
144
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
145
145
  <script src="/builders/palette.js" defer></script>
146
146
 
147
147
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -145,7 +145,7 @@
145
145
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
146
146
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
147
147
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
148
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
148
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
149
149
  <script src="/builders/palette.js" defer></script>
150
150
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
151
151
  <script src="/builders/tweak-editor-lib.js?v=2026-09-29-tw27"></script>
@@ -168,7 +168,7 @@
168
168
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
169
169
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
170
170
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
171
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
171
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
172
172
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
173
173
  <script src="/builders/palette.js" defer></script>
174
174
  <!-- shared component kit (window.OTBKit) — tabs, ledger, pager (task 2166). -->
@@ -174,7 +174,7 @@
174
174
  <script src="/builders/dom-utils.js?v=2026-10-25-escape"></script>
175
175
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
176
176
  <script src="/builders/craft-mode.js?v=2026-10-30-market-watch"></script>
177
- <script src="/builders/shell.js?v=2026-11-03-update-pill"></script>
177
+ <script src="/builders/shell.js?v=2026-11-04-one-market-watch"></script>
178
178
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
179
179
  <script src="/builders/palette.js" defer></script>
180
180
  <!-- task 1004727: the ONE karma vote control every task/idea list renders -->
@@ -57,6 +57,124 @@ from the other fixtures); `tests/home_city_widget.mjs` runs the real widget in a
57
57
  redraws, play stops on now, a refused read leaves no empty plate, no sideways scroll at 390); the kit stub does
58
58
  not serve the read, so `index.states.json` ignores it and its renders show the city hidden, as for a visitor.
59
59
 
60
+ ## v16 — one city for every craft (task 1005058)
61
+
62
+ The owner's round of 2026-11-04 ([`v16-building.html`](../../../docs/design/mocks/hall/home/v16-building.html),
63
+ with `v16-city.js` and `v16-frame.js`; the v15 and v16 rounds for the other crafts sit beside it). Step 1 of 4:
64
+ the builder's Home. Steps 2–4 (the ideator's sky, the artist's gallery, the studio) are tasks 1005059–1005061.
65
+
66
+ - **The civic core is the Governor City's own.** The nine institutions stand on the plaza at the centre, drawn by
67
+ `city-draw.js` (`GovCity.isoInstitution` at `GovCity.CIVIC_SLOTS`, its `isoTheme` palette), which Home now loads
68
+ ahead of `home-city-draw.js`; the drawing takes it as `opts.gov` and is never handed a copy. Without it the city
69
+ still draws, minus its core.
70
+ - **The working areas stand on a ring of sixteen plots** (`RING`) around the plaza, tallest first into the plots
71
+ farthest back (smallest x + y), so no tower hides the core; the art museum and its garden (after the Cleveland
72
+ Museum of Art: the marble block and portico, the atrium between granite wings, the Fine Arts Garden and lagoon)
73
+ stand on the front edge. A test holds every plot clear of the plaza, the museum and each other.
74
+ - **Districts are doors, one hover each.** The core is one link to the street (`GovCity.STREET_HREF`); the museum
75
+ and the band of stars are craft doors (`data-craft-door`, `OTBCraftMode.choose`); a building links to its goal
76
+ (`/goals#/goal/N`). On the builder's Home the core and the museum are faded (`.hc-quiet`) so the new buildings
77
+ lead; `opts.focus` keeps a craft's own district in colour for the Homes still to come.
78
+ - **The ideas are a thin band of stars high above the city**, apart from it — a hint at the sky, not part of it.
79
+ - **Time is live.** The slider opens at **Live** (a pulsing dot) and takes any value (`step="any"`); the drawing
80
+ accepts a fractional week (`doneAt` adds that part of the next week), so moving the slider or the quiet player
81
+ (rewind, play/pause, fast forward) **glides** the city through every week in between — the owner asked for a
82
+ rewind, not a jump. A date pill rides the handle; *Back to live* returns.
83
+ - **The one motion driven from script.** The glide is a `requestAnimationFrame` loop that redraws the city at a
84
+ fractional week — a deliberate exception to the hall's "motion stays CSS" rule, at the owner's request. It is
85
+ gated on `prefers-reduced-motion` before it starts: with reduced motion nothing animates and every control jumps
86
+ straight to its destination (`tests/home_city_widget.mjs` pins both).
87
+
88
+ ## v16 step 2 — the ideator's sky (task 1005059)
89
+
90
+ The mock is [`v16-ideating.html`](../../../docs/design/mocks/hall/home/v16-ideating.html). While the craft is
91
+ **Ideating** (`OTBCraftMode.current()`, redrawn on `otb:craft`) the same widget, from the same one read, draws
92
+ `home-city-draw.js` `skySvg` instead of the city. Every other craft keeps the builder's city untouched.
93
+
94
+ - **The skyline is the city.** `skylineOf` turns every building `citySvg` stands into a silhouette at the
95
+ same screen x (a box from (x0,y0) to (x1,y1) spans `P(x0,y1)` to `P(x1,y0)` in the city's own frame) and
96
+ the same height: each tower is its plinth plus what it has built by the slider's week, a civic building is
97
+ `GovCity.ROOF` at the core's scale, and the museum is its marble block and atrium. A test holds each
98
+ silhouette to its tower's drawn faces.
99
+ - **Nearer covers farther, outlines included.** The silhouettes are drawn back to front, each fill followed
100
+ by its own outline, so a nearer building hides the outline behind it. Over them lies one hit link per
101
+ silhouette, also back to front, so the nearest building takes the pointer. Hovering or focusing any link
102
+ in a group outlines the whole group (CSS `:has()` in `home-city.css`; no script). Each group is one door
103
+ with one keyboard stop: **Building** opens `/goals`, **Government** opens `GovCity.STREET_HREF`, and
104
+ **Art** switches to Artistry (`data-craft-door`). A low building still offers a 30-unit band to point at.
105
+ - **The ideas.** An idea's area is the goal of the work it became, else the goal its filer aimed it at
106
+ (`suggested_goal_id`) when that goal is an area of this version. Each area with ideas gets a constellation,
107
+ and the rest go in the *Not placed yet* field. Star size is net votes. A salmon ring, only at Live, marks a
108
+ new idea (these two weeks) that is open, in no area, not the viewer's, and not yet voted on by them. In the
109
+ week an idea became work it falls into its tower; after that it lights a window there. The moon is the
110
+ version, its phase what the areas have shipped over every task under them (`version.tasks`).
111
+ - **The ladder** ranks the week's ideators: ideas filed, idea votes cast, and ideas of theirs that became work.
112
+ - **The read stays one and bounded.** `GET /home/city` adds these fields to the ideas list it already sent
113
+ (still newest first, at most 120 rows), plus idea votes per builder per week and a per-area task count.
114
+ The `ideas_votes` table is probed first, so a project with the ideas module off answers without votes.
115
+
116
+ ## The gallery — the artist's Home (task 1005060)
117
+
118
+ Step 3 of 4, the owner's round of 2026-11-04 ([`v16-artistry.html`](../../../docs/design/mocks/hall/home/v16-artistry.html), v16b):
119
+ "not about quality but about appreciation". In Artistry, Home is a seat on a bench in the city's art museum, facing a
120
+ quiet wall; the museum's `data-craft-door="artistry"` leads here.
121
+
122
+ - **One section, two Homes.** It is not a hall widget of its own (Home's stats zone holds one): `home-city.js`
123
+ attaches `home-gallery.js` beside the city and, on `otb:craft`, shows the gallery in Artistry (`.hc--gallery`) and
124
+ the city otherwise. The builder's Home is unchanged and never asks for the wall.
125
+ - **What a work is.** A shipped `artist`/`ui` task (or a page tweak round) that carries a picture: a round hangs its
126
+ own desktop AFTER renders (light and dark), any other ship its ship-time visual. The artist is `shipped_by`, the
127
+ line on the label its value summary. A picture whose file is gone is not hung. Read by `GET /home/gallery`
128
+ (`modules/lifecycle/home-gallery.js`, at most 60 works, Most popular or Latest ordered in SQL); a security-sensitive
129
+ task, or one under a private goal the viewer cannot read, never hangs.
130
+ - **Appreciation, never a grade.** One heart per builder per work (`lifecycle_work_appreciations`, lifecycle_020,
131
+ primary key `(task_id, builder_id)`), given and taken back with `PUT /home/gallery/:id/heart { on }`. The count
132
+ shows only in the credit line and orders Most popular; no rank or leaderboard reads it. The heart never changes size.
133
+ - **The label is the museum's.** Small, at the work's foot; a click opens it closer (`transform`, so the wall does not
134
+ re-flow) with the value summary and the credit line. The signature (a first name, an initial and the year, in a
135
+ handwriting stack the hall already has on the machine, no web font) sits on the wall under it, beside the heart.
136
+ - **Full screen** (the button or F) is the art, a caption, the signature and the heart; Escape leaves. Where the
137
+ browser refuses the Fullscreen API it falls back to a fixed layer.
138
+ - **Wayfinding:** "The full gallery" hangs every work at once, salon-style (a click sits you in front of one);
139
+ "Your studio" goes to `/studio` until step 4 (task 1005061) builds the studio behind the gallery.
140
+ - **Colour** is the hall's tokens mixed; the frame and shadows are black with alpha. Motion is CSS only.
141
+ - Pinned in `tests/home_gallery.mjs` (the read, the heart, the label words) and `tests/home_gallery_widget.mjs` (the
142
+ real widget: the walk, the zoom, the heart, Latest, full screen, the salon, phone width, the craft switch). The
143
+ harness answers with `scripts/hall-preview/fixtures/home__gallery.json` (sample names, the project's real works);
144
+ `HALL_PREVIEW_VISUALS=<dir>` serves real renders.
145
+
146
+ ## The studio — behind the gallery (task 1005061)
147
+
148
+ Step 4 of 4 ([`v16-studio.html`](../../../docs/design/mocks/hall/home/v16-studio.html)): "then you are in your studio,
149
+ where you are at peace and can think best." The studio is the hall's **`/studio`** page: the sidebar's Studio and the
150
+ gallery's **Your studio** door both lead there. Its five shelves (`home-studio.js`, mounted into
151
+ `#studio-shelves`) sit under the page's illustrated room, which keeps its tally, the tweak flow, the approvals and the
152
+ AI door; the shelves wear the gallery's wall, lit from a north window: "Quiet. Nothing here is due." No ranks, no
153
+ leaderboard.
154
+
155
+ - **On your easel** — the page round you hold (the Tweak/CopyWrite Flow's `resume`), with "Pick up where you left off"
156
+ into the tweak editor; with none, "Nothing on your easel" and the page the flow would recommend next. The picture is
157
+ that page's newest work on the wall (`GET /home/studio/works?page=`), else a quiet empty frame.
158
+ - **Waiting for new inspiration** — pages whose words changed since an artist last designed them (the flow's `changed`
159
+ tier, drift from the page readings), most changed lines first, four shown and the rest counted.
160
+ - **Words to tune** — the three newest open copy-desk flags, as text, linking to the copy desk.
161
+ - **Pinned from the gallery** — the works you hearted, newest heart first (a heart on the bench pins a work here).
162
+ - **Your works in the collection** — the works you signed, most appreciated first, with their heart counts.
163
+ - **Back into the gallery:** "The bench" is `/`, "The full gallery" `/?gallery=all`, and each work `/?work=<id>`, which
164
+ the gallery reads once on arrival (`landing`) to open its salon or sit you in front of that work, then puts the
165
+ address back.
166
+ - **Two bounded reads:** `GET /copy-desk/studio` (`modules/copy-desk/routes/copy-desk-studio.js`, composed by
167
+ `page-status.js` `composeNext`, so it cannot disagree with the room's own tweak flow) and `GET /home/studio/works`
168
+ (`modules/lifecycle/home-gallery.js`, the wall's own WHERE, six a shelf). Without the copy desk its shelves say so and
169
+ the works still hang. A link from a shelf row goes only to the tweak editor or the copy desk.
170
+ - **Not built, said rather than invented:** the mock's "The style library →" (the style library is files in
171
+ `modules/ui-design/styles/`, with no hall page to link) and the easel's line about the owner's notes on a round (notes
172
+ on a page round are stored nowhere the studio can read).
173
+ - **Open for the owner:** whether the shelves should one day replace the illustrated room above them (its Resume and the
174
+ easel now say the same thing twice).
175
+ - Pinned in `tests/home_studio.mjs` and `tests/home_studio_widget.mjs` (real browser, empty states included); harness
176
+ fixtures `copy-desk__studio.json` and `home__studio__works.json`.
177
+
60
178
  ## Private areas
61
179
 
62
180
  A private goal the viewer is neither a member of nor an Archon over still stands as a tower (its counts are structure), but the server sends no title for it or for the ideas tied to it; the drawing labels them "A private area" and "A private idea" (ADR 0112, as the goal map and the sky apply it). Pinned in `tests/home_city.mjs`.
@@ -64,3 +182,8 @@ A private goal the viewer is neither a member of nor an Archon over still stands
64
182
  ## Sign-off
65
183
 
66
184
  The owner chose v14 from fourteen mock rounds, approved the before and after review sheet (1280 dark and light, 390 dark) on 2026-11-02, and said "Yes, ship it".
185
+
186
+ **Step 2, the ideator's sky (task 1005059).** The owner approved the four-step v16 plan on 2026-11-04 and asked
187
+ for each step to land; the step's shots (Ideating at 1440 and 390, dark and light; the three hovers; the
188
+ builder's Home unchanged) went to the owner after landing, and the visual review holds the deploy, never the
189
+ land (ADR 0241). Their OK of those shots is recorded here when given — not before.
@@ -15,7 +15,7 @@ shapes breathing 0.4 to 1 and back over 0.8s, in the place the content will land
15
15
  rewrites IN PLACE (atlas's caption, sessions' status line, the goal dependency ledes, the profile's
16
16
  activity placeholder) keeps its plain words, because the class would stay on the finished sentence.
17
17
  - **Pages that are blank while they wait get a page loader that puts itself away.** The sealed/body
18
- pages (board-room, deploy, fleet, gate, government, market-watch-settings, project-settings,
18
+ pages (board-room, deploy, fleet, gate, government, project-settings,
19
19
  sessions) open their `<main>` on `.hall-loading--wait`, which shows only while every section after
20
20
  it is `[hidden]`. Home and Watch, whose zones and panes are always present, use
21
21
  `.hall-loading--zones`: it shows while no showing zone has a showing child. Both are `:has()`
@@ -1,11 +1,24 @@
1
- # Market Watch settings, at /market-watch-settings (task 1004938)
1
+ # Market Watch settings, the owner's Settings tab on /market-watch (task 1004938; a tab since task 1005045)
2
+
3
+ **Since task 1005045 (owner decision 2026-11-03, idea 1001310) this is a tab, not a page.** The sidebar showed "Market Watch" twice — the page and these settings — so the settings page is gone and its markup is the `#mw-panel-settings` panel in `market-watch.html`. What that move settled:
4
+
5
+ - **Only the owner gets the strip.** `market-watch.js` adopts the shell's one `/me` read (`window.__hallReads`, no second request); for an archon it builds two kit tabs (`watch`, `settings`, `hash: true`) and calls `OTBMarketWatchSettings.load()`. Anyone else gets no tab strip at all, and their page never asks for the settings. An unreadable `/me` is not an owner. Cosmetic only: the four routes below are still the wall (ADR 0016).
6
+ - **`market-watch-settings.js` no longer boots itself** — it exports `load` and waits to be called. `market-watch-settings.css` rides along on the page.
7
+ - **The old URL lands on the tab:** `/market-watch-settings` (apex and subdomain, with or without `.html`) 302s to `/market-watch#settings` in `serve-internal.js` (`RETIRED_MW_SETTINGS_RE`), before any page gate. The tab ids are the contract.
8
+ - **Where the module is off there is no tab:** the page shows its quiet off line and asks nothing, so there is nothing to set. A watch that fails to READ keeps the tab: the settings are their own read, and a broken watch must not lock the owner out of its switch and key.
9
+ - **The link to the rules** sits at the foot of the tab, as it does under the desk's streak.
10
+ - **The tab opens on the page loader** (`.hall-loading--wait`, records/loader.md): every section after it starts hidden, so an owner landing on `#settings` sees "Loading…" until the read draws a section, never a blank panel.
11
+ - **Governing mode lost its Market Watch entry.** The settings were in the Governing mode's list; the Market Watch page is not (`tests/craft_mode_nav.mjs` pins Governing as the shortest list), so in that mode the owner reaches the tab from another mode, the palette, or the URL.
12
+
13
+ What the page settled before the move still holds:
14
+
2
15
 
3
16
  **The owner's fuel gauge for Market Watch (goal 1000137), built to `docs/design/mocks/market-watch/settings.html`, option B.** One panel: the gauge (spend this month against the monthly AI cap, with a faint arc for what the next run will use), the on/off pill, the cap slider, the cadence chips (weekly, every 2 weeks, monthly), the strip of recent runs, and the project's Anthropic key. Five things it settled:
4
17
 
5
- - **The routes are the wall, not the page.** `GET`/`PATCH /market-watch/settings` and `PUT`/`DELETE /market-watch/settings/key` are owner-only: the `project.settings.manage` atom, then an in-route owner-seat (Archon) check (modules/market-watch/routes/settings.js). The shell joins `SIGNEDIN_BUILDERS_PAGE_RE` and holds no data; a 403 draws the owner-only panel and a 404 (module off, so its routes are not mounted) the "turn the module on" panel. The nav item sits in Government, `archon`, `module: 'market-watch'`.
18
+ - **The routes are the wall, not the page.** `GET`/`PATCH /market-watch/settings` and `PUT`/`DELETE /market-watch/settings/key` are owner-only: the `project.settings.manage` atom, then an in-route owner-seat (Archon) check (modules/market-watch/routes/settings.js). A 403 draws the owner-only panel and a 404 (module off, so its routes are not mounted) the "turn the module on" panel.
6
19
  - **The key is write-only.** The page learns `key_set` and nothing else, not the last four. The field is emptied before the request goes out; Clear asks twice by turning into its own question.
7
20
  - **The page redraws from the server's answer.** Every write returns the whole view, so the gauge never shows a value the server did not store.
8
21
  - **The strip is the run ledger, oldest left.** A ran run shows the names it watched; a skipped one is a hatched dash, with its reason in the cell's label and the legend (`cap reached`, `switched off`, `no key`, …); the dashed tile is the next run and the two after it are empty. When the watch is off the future tiles go. The seed run never shows. At 620px and below the dates stack day over month, or nine tiles overflow at 390px.
9
22
  - **Mockup colours were not kept.** The ring, chips and tiles use the hall's `--accent`, `--accent-tint`, `--accent-text`, `--rule*` and `--ok-weak`; the mock's own fonts were dropped for the pack's.
10
23
 
11
- Fixture: `scripts/hall-preview/fixtures/market-watch__settings.json` (the mockup's month). Tests: `tests/market_watch_settings.mjs` (403s for a Metic and a Xenos on all four routes, switching off stops the next run against `run.js`'s own gate, the key never in a response).
24
+ Fixture: `scripts/hall-preview/fixtures/market-watch__settings.json` (the mockup's month). Tests: `tests/market_watch_pages.mjs` (SETTINGS TAB: who gets the tab, the panel's controls, the redirect), `tests/market_watch_settings.mjs` (403s for a Metic and a Xenos on all four routes, switching off stops the next run against `run.js`'s own gate, the key never in a response).
@@ -30,8 +30,10 @@ its known facts, and the sortable case file (found, run, type, takeaway, confide
30
30
  item, a score or an entry. The direction of a move comes from the item on the server, never the client.
31
31
  - **Pin and "read" are the viewer's own** (localStorage `mw-pins`, `mw-read`): conveniences, not the
32
32
  record. Pinned items list under the scouts.
33
- - **"sweep now" is not here.** A manual run spends the project's AI budget, so it is the settings page's
33
+ - **"sweep now" is not here.** A manual run spends the project's AI budget, so it is the settings'
34
34
  (task 1004938) to offer with the cap beside it.
35
+ - **The owner's settings are a tab here** (task 1005045): an archon viewer gets a `watch` / `settings`
36
+ strip, everyone else none. See `records/market-watch-settings.md`.
35
37
  - **Off is quiet.** Where the default-off module is not enabled the nav item is dropped, the page shows
36
38
  one line pointing at Modules, and it asks the server nothing; a 404 from unmounted routes reads the same.
37
39
 
@@ -20,7 +20,14 @@
20
20
  // people — per builder per week: tasks shipped (build / art), ideas filed, and
21
21
  // govern acts (board votes cast, blockers resolved).
22
22
  // ideas — the window's ideas for the sky, newest first, bounded, each with the
23
- // goal of the task it became.
23
+ // goal of the task it became, the area it is aimed at (that goal, else
24
+ // the filer's suggested goal), the week it became work, its net votes,
25
+ // and whether it waits on THIS viewer (task 1005059, the ideator's sky:
26
+ // a new idea no area holds yet, not the viewer's own, that they have not
27
+ // voted on — "to place or vote on").
28
+ // people — (as above) plus the idea votes each builder cast per week, for the
29
+ // ideator's ladder.
30
+ // version.tasks — every task under the areas, the moon's whole (how far along).
24
31
  // now — who holds an open claim in each area (an Autobongos runner working it
25
32
  // marks the site as an agent's), and how many matters in each area
26
33
  // wait on THIS viewer.
@@ -150,7 +157,7 @@ async function readCity({ weeks, viewer }, deps = {}) {
150
157
  const rank = viewer ? String(viewer.rank || '').toLowerCase() : '';
151
158
  const isArchon = rank === 'archon';
152
159
  const viewerId = viewer && viewer.id != null ? String(viewer.id) : null;
153
- const [{ rows: [win] }, version, hasVotes, hasRunners] = await Promise.all([
160
+ const [{ rows: [win] }, version, hasVotes, hasRunners, hasIdeaVotes] = await Promise.all([
154
161
  pool.query(
155
162
  `SELECT ((date_trunc('week', now() AT TIME ZONE 'UTC') AT TIME ZONE 'UTC') - make_interval(weeks => $1 - 1)) AS start`,
156
163
  [N],
@@ -158,9 +165,10 @@ async function readCity({ weeks, viewer }, deps = {}) {
158
165
  versionBeingBuilt(pool, deps),
159
166
  tableExists(pool, 'government_board_votes'),
160
167
  tableExists(pool, 'autonomy_runner_heartbeat'),
168
+ tableExists(pool, 'ideas_votes'),
161
169
  ]);
162
170
  const start = win.start;
163
- const out = { N, start, version, areas: [], areaWeeks: [], leads: [], peopleWeeks: [], ideaCounts: [], ideas: [], gov: [], builders: [], sites: [], waiting: [] };
171
+ const out = { N, start, version, areas: [], areaWeeks: [], leads: [], peopleWeeks: [], ideaCounts: [], ideas: [], gov: [], builders: [], sites: [], waiting: [], ideaVotes: [], taskTotal: null };
164
172
 
165
173
  // VISIBILITY (ADR 0112's leak rule, as goal-graph.js applies it): a private
166
174
  // goal the viewer is neither a member of nor an Archon over stays a tower (its
@@ -183,7 +191,15 @@ async function readCity({ weeks, viewer }, deps = {}) {
183
191
  const none = Promise.resolve({ rows: [] });
184
192
  const friction = rank === 'xenos' ? `AND i.kind <> '${SKILL_FRICTION_KIND}' AND i.title NOT LIKE '${SKILL_FRICTION_TAG}%'` : '';
185
193
 
186
- const [areaWeeks, leads, peopleWeeks, ideaCounts, ideas, gov, sites, waiting] = await Promise.all([
194
+ // the idea's net votes and the viewer's own vote in ONE look-up per idea (at most
195
+ // MAX_IDEAS of them, each an index read on idx_ideas_votes_target), read only when the
196
+ // ideas module's votes table is there (a project with ideas switched off answers without them)
197
+ const ideaVoteCols = hasIdeaVotes ? 'iv.votes, iv.voted' : '0 AS votes, false AS voted';
198
+ const ideaVoteJoin = hasIdeaVotes
199
+ ? `LEFT JOIN LATERAL (SELECT coalesce(sum(v.vote), 0)::int AS votes, coalesce(bool_or(v.voter_builder_id = $2::bigint), false) AS voted
200
+ FROM ideas_votes v WHERE v.target_idea_id = i.id) iv ON true`
201
+ : '';
202
+ const [areaWeeks, leads, peopleWeeks, ideaCounts, ideas, gov, sites, waiting, ideaVotes, taskTotal] = await Promise.all([
187
203
  goalIds.length ? pool.query(
188
204
  `SELECT t.goal_id::text AS goal_id,
189
205
  CASE WHEN t.shipped_at < $2 THEN -1 ELSE ${wk('t.shipped_at', '$2')} END AS wk,
@@ -222,13 +238,17 @@ async function readCity({ weeks, viewer }, deps = {}) {
222
238
  [start],
223
239
  ),
224
240
  pool.query(
225
- `SELECT i.id::text AS id, i.title, i.captured_by::text AS by, ${wk('i.captured_at', '$1')} AS wk, pt.goal_id::text AS goal_id
241
+ `SELECT i.id::text AS id, i.title, i.captured_by::text AS by, ${wk('i.captured_at', '$1')} AS wk, pt.goal_id::text AS goal_id,
242
+ i.suggested_goal_id::text AS aim_id, i.status,
243
+ CASE WHEN pt.id IS NULL THEN NULL ELSE ${wk('pt.created_at', '$1')} END AS became_wk,
244
+ ${ideaVoteCols}
226
245
  FROM idea_inbox i
227
246
  LEFT JOIN tasks pt ON pt.id = i.promoted_to_task_id
247
+ ${ideaVoteJoin}
228
248
  WHERE i.captured_at >= $1 ${friction}
229
249
  ORDER BY i.captured_at DESC
230
250
  LIMIT ${MAX_IDEAS}`,
231
- [start],
251
+ hasIdeaVotes ? [start, viewerId] : [start],
232
252
  ),
233
253
  pool.query(
234
254
  `SELECT id, wk, sum(n)::int AS n FROM (
@@ -251,14 +271,27 @@ async function readCity({ weeks, viewer }, deps = {}) {
251
271
  [goalIds],
252
272
  ) : none,
253
273
  goalIds.length ? waitingOnViewer(pool, goalIds, viewer, deps) : Promise.resolve([]),
274
+ hasIdeaVotes ? pool.query(
275
+ `SELECT v.voter_builder_id::text AS id, ${wk('v.recorded_at', '$1')} AS wk, count(*)::int AS n
276
+ FROM ideas_votes v WHERE v.recorded_at >= $1 GROUP BY 1, 2`,
277
+ [start],
278
+ ) : none,
279
+ goalIds.length ? pool.query(
280
+ `SELECT t.goal_id::text AS goal_id, count(*)::int AS n FROM tasks t
281
+ WHERE t.goal_id = ANY($1::bigint[]) AND t.status <> 'abandoned'
282
+ AND t.title NOT LIKE '${SMOKE_TITLE}' ESCAPE '\\'
283
+ GROUP BY 1`,
284
+ [goalIds],
285
+ ) : none,
254
286
  ]);
255
287
  Object.assign(out, {
256
288
  areaWeeks: areaWeeks.rows, leads: leads.rows, peopleWeeks: peopleWeeks.rows, ideaCounts: ideaCounts.rows,
257
289
  ideas: ideas.rows, gov: gov.rows, sites: sites.rows, waiting,
290
+ ideaVotes: ideaVotes.rows, taskTotal: goalIds.length ? taskTotal.rows.reduce((n, r) => n + (Number(r.n) || 0), 0) : null,
258
291
  });
259
292
 
260
293
  const ids = new Set();
261
- [out.leads.map((r) => r.shipped_by), out.peopleWeeks.map((r) => r.id), out.ideaCounts.map((r) => r.id), out.ideas.map((r) => r.by), out.gov.map((r) => r.id), out.sites.map((r) => r.builder_id)]
294
+ [out.leads.map((r) => r.shipped_by), out.peopleWeeks.map((r) => r.id), out.ideaCounts.map((r) => r.id), out.ideas.map((r) => r.by), out.gov.map((r) => r.id), out.sites.map((r) => r.builder_id), out.ideaVotes.map((r) => r.id)]
262
295
  .forEach((list) => list.forEach((id) => { if (id != null) ids.add(String(id)); }));
263
296
  if (viewerId) ids.add(viewerId);
264
297
  if (ids.size) {
@@ -300,24 +333,39 @@ function shapeCity(raw, viewer) {
300
333
  if (!known.has(key)) return null; // a machine principal or a deleted builder is never drawn
301
334
  if (!people.has(key)) {
302
335
  const b = known.get(key);
303
- people.set(key, { id: key, login: b.github_login || '', name: b.display_name || b.github_login || 'Builder', avatar_url: b.avatar_url || null, agent: false, build: zeros(), art: zeros(), idea: zeros(), gov: zeros() });
336
+ people.set(key, { id: key, login: b.github_login || '', name: b.display_name || b.github_login || 'Builder', avatar_url: b.avatar_url || null, agent: false, build: zeros(), art: zeros(), idea: zeros(), gov: zeros(), vote: zeros() });
304
337
  }
305
338
  return people.get(key);
306
339
  };
307
340
  raw.peopleWeeks.forEach((r) => { const p = person(r.id); if (p && inWeek(r.wk)) (r.art ? p.art : p.build)[r.wk] += Number(r.n) || 0; });
308
341
  raw.ideaCounts.forEach((r) => { const p = person(r.id); if (p && inWeek(r.wk)) p.idea[r.wk] += Number(r.n) || 0; });
309
342
  raw.gov.forEach((r) => { const p = person(r.id); if (p && inWeek(r.wk)) p.gov[r.wk] += Number(r.n) || 0; });
343
+ (raw.ideaVotes || []).forEach((r) => { const p = person(r.id); if (p && inWeek(r.wk)) p.vote[r.wk] += Number(r.n) || 0; });
310
344
  if (viewer && viewer.id != null) person(viewer.id);
311
345
  areas.forEach((a) => { a.lead = a.lead.map((id) => (id && known.has(id) ? id : null)); });
312
346
 
313
- const ideas = raw.ideas.filter((r) => inWeek(r.wk)).map((r) => ({ id: String(r.id), week: r.wk, by: r.by && known.has(String(r.by)) ? String(r.by) : null, title: hiddenGoals.has(String(r.goal_id)) ? null : (r.title || ''), goal_id: r.goal_id && byGoal.has(String(r.goal_id)) ? String(r.goal_id) : null }));
347
+ // The sky's facts per idea (task 1005059). Its AREA is the goal of the work it became,
348
+ // else the goal its filer aimed it at — an area of this version, or none ("not placed
349
+ // yet"). It BECAME work the week its task was made, never before it was filed and never
350
+ // past this week. It WAITS on the viewer when it is new (these two weeks), open, in no
351
+ // area, not theirs, and they have not voted on it.
352
+ const viewerKey = viewer && viewer.id != null ? String(viewer.id) : null;
353
+ const ideas = raw.ideas.filter((r) => inWeek(r.wk)).map((r) => {
354
+ const goal = r.goal_id && byGoal.has(String(r.goal_id)) ? String(r.goal_id) : null;
355
+ const aim = r.aim_id && byGoal.has(String(r.aim_id)) ? String(r.aim_id) : null;
356
+ const area = goal || aim;
357
+ const became = Number.isInteger(r.became_wk) && goal ? Math.max(r.wk, Math.min(N - 1, r.became_wk)) : null;
358
+ const hidden = hiddenGoals.has(String(r.goal_id)) || hiddenGoals.has(String(r.aim_id));
359
+ const waits = !!viewerKey && r.status === 'open' && !area && r.wk >= N - 2 && String(r.by) !== viewerKey && !r.voted;
360
+ return { id: String(r.id), week: r.wk, by: r.by && known.has(String(r.by)) ? String(r.by) : null, title: hidden ? null : (r.title || ''), goal_id: goal, area_id: area, became, votes: Number(r.votes) || 0, waits };
361
+ });
314
362
  const sites = raw.sites.filter((s) => byGoal.has(String(s.goal_id)) && known.has(String(s.builder_id)))
315
363
  .map((s) => { person(s.builder_id); return { goal_id: String(s.goal_id), builder_id: String(s.builder_id), agent: !!s.agent }; });
316
364
  const waiting = raw.waiting.filter((w) => byGoal.has(String(w.goal_id)) && Number(w.n) > 0).map((w) => ({ goal_id: String(w.goal_id), count: Number(w.n) }));
317
365
 
318
366
  return {
319
367
  viewer: viewer && viewer.id != null ? { id: String(viewer.id) } : null,
320
- version: raw.version ? { id: raw.version.id, name: raw.version.name || raw.version.id } : null,
368
+ version: raw.version ? { id: raw.version.id, name: raw.version.name || raw.version.id, tasks: Number.isFinite(raw.taskTotal) ? raw.taskTotal : null } : null,
321
369
  weeks, areas, people: [...people.values()], ideas, now: { sites, waiting },
322
370
  };
323
371
  }