@bongos/core 1.19.1012 → 1.19.1014

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 (76) hide show
  1. package/.bongos-core.json +113 -78
  2. package/docs/adr/0331-the-sky-is-the-ideators-home.md +6 -1
  3. package/docs/copy-inventory.md +120 -66
  4. package/docs/copy-registry.json +984 -498
  5. package/docs/file-map.md +1 -1
  6. package/docs/module-api-changelog.md +4 -0
  7. package/modules/hall-ui/public/atlas.html +1 -1
  8. package/modules/hall-ui/public/blockers.html +1 -1
  9. package/modules/hall-ui/public/board-room.html +1 -1
  10. package/modules/hall-ui/public/collab.html +1 -1
  11. package/modules/hall-ui/public/copy-desk.html +1 -1
  12. package/modules/hall-ui/public/deploy.html +1 -1
  13. package/modules/hall-ui/public/diagrams.html +1 -1
  14. package/modules/hall-ui/public/drachmae.html +1 -1
  15. package/modules/hall-ui/public/fleet-costs.js +80 -0
  16. package/modules/hall-ui/public/fleet.html +64 -0
  17. package/modules/hall-ui/public/fleet.js +45 -0
  18. package/modules/hall-ui/public/fleet.states.json +14 -0
  19. package/modules/hall-ui/public/gate.html +1 -1
  20. package/modules/hall-ui/public/goal-map.html +1 -1
  21. package/modules/hall-ui/public/goals.html +1 -1
  22. package/modules/hall-ui/public/government.html +1 -1
  23. package/modules/hall-ui/public/idea-objects.js +4 -1
  24. package/modules/hall-ui/public/idea.html +1 -1
  25. package/modules/hall-ui/public/ideas.html +1 -1
  26. package/modules/hall-ui/public/index.html +1 -1
  27. package/modules/hall-ui/public/modules.html +1 -1
  28. package/modules/hall-ui/public/people.html +1 -1
  29. package/modules/hall-ui/public/primer.html +1 -1
  30. package/modules/hall-ui/public/profile.html +1 -1
  31. package/modules/hall-ui/public/project-settings.html +1 -1
  32. package/modules/hall-ui/public/ranks.html +1 -1
  33. package/modules/hall-ui/public/roadmap.html +1 -1
  34. package/modules/hall-ui/public/roster.html +1 -1
  35. package/modules/hall-ui/public/sessions.html +1 -1
  36. package/modules/hall-ui/public/settings.html +1 -1
  37. package/modules/hall-ui/public/shell.js +4 -0
  38. package/modules/hall-ui/public/sky-composer.js +155 -0
  39. package/modules/hall-ui/public/sky-data.js +287 -0
  40. package/modules/hall-ui/public/sky-draw.js +244 -0
  41. package/modules/hall-ui/public/sky-interaction.js +137 -0
  42. package/modules/hall-ui/public/sky-layout.js +81 -0
  43. package/modules/hall-ui/public/sky-panel.js +141 -0
  44. package/modules/hall-ui/public/sky-tour.js +80 -0
  45. package/modules/hall-ui/public/studio.html +1 -1
  46. package/modules/hall-ui/public/task.html +1 -1
  47. package/modules/hall-ui/public/thinking.css +203 -267
  48. package/modules/hall-ui/public/thinking.html +119 -58
  49. package/modules/hall-ui/public/thinking.states.json +33 -9
  50. package/modules/hall-ui/public/watch.html +1 -1
  51. package/modules/hall-ui/public/work.html +1 -1
  52. package/modules/provisioning/routes/provisioning.js +4 -4
  53. package/modules/public-landing/public/projects.html +7 -0
  54. package/modules/ui-design/kit/fixtures/provisioning-instances.json +1 -0
  55. package/package-lock.json +2 -2
  56. package/package.json +1 -1
  57. package/src/bongos/auth.js +8 -0
  58. package/src/bongos/routes/me.js +4 -1
  59. package/src/bongos/serve-internal.js +25 -6
  60. package/src/module-api.js +1 -1
  61. package/tests/auth_resolve_failure.mjs +3 -2
  62. package/tests/fleet_cost_view.mjs +189 -0
  63. package/tests/hall_audit.mjs +27 -0
  64. package/tests/hall_nav.mjs +3 -1
  65. package/tests/hall_page_gate_map.mjs +13 -5
  66. package/tests/hall_sky.mjs +393 -405
  67. package/tests/hall_thinking_room.mjs +274 -296
  68. package/tests/helpers.mjs +185 -217
  69. package/tests/ideator_large_dead_thought_proof.mjs +33 -25
  70. package/tests/ideator_traceable_proof.mjs +61 -39
  71. package/tests/nav_permission_atoms.mjs +1 -1
  72. package/modules/hall-ui/public/sky.css +0 -365
  73. package/modules/hall-ui/public/sky.html +0 -79
  74. package/modules/hall-ui/public/sky.js +0 -580
  75. package/modules/hall-ui/public/sky.states.json +0 -20
  76. package/modules/hall-ui/public/thinking.js +0 -418
@@ -2,74 +2,135 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Your thinking — {{worldName}}</title>
7
- <meta name="description" content="Everything you filed and what became of it: what it became, how much work it caused, and whether that work is still alive.">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <title>Your sky — {{worldName}}</title>
7
+ <meta name="description" content="Your thinking, drawn from the ledger: every thought you filed as one of five objects, in the goal that caught it, sized by the work it caused.">
8
8
  <link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
12
- <link rel="stylesheet" href="/builders/thinking.css">
11
+ <!-- The faces (Manrope, JetBrains Mono) come from the branding pack's font
12
+ links, injected into this head by the server like every hall page's
13
+ (tests/hall_tokens.mjs: no hall page hardcodes a web-font stylesheet).
14
+ The template carried its own Google Fonts link for the same two faces. -->
15
+ <link rel="stylesheet" href="/builders/thinking.css?v=2026-09-25-sky3">
13
16
  </head>
14
- <!-- THE PAGE THE SPEC CALLS "THE ROOM" (task 1004098 / BV2.R03). The file is
15
- `thinking` and not `room` because `room.css` is already taken, by the hall's
16
- READING-ROOM archetype (the primer, diagrams, drachmae — task 1003306,
17
- ADR 0228), and `board-room` is the government surface. Three unrelated
18
- rooms in one directory is a collision waiting to be made by whoever next
19
- links a sheet into a page; the page's own h1 is the design's phrase, so
20
- nothing about the vocabulary is lost by naming the file after it. -->
21
- <body data-page="thinking" data-page-title="Your thinking">
17
+ <!-- THE SKY (task 1004231 / sky part 3 of 5, goal 1000095; ADR 0331, ADR 0227
18
+ §11). This page is docs/design/mocks/sky/sky.template.html — the prototype
19
+ the owner adopted as the design of record — built for real over GET /sky
20
+ and GET /sky/mine (part 2). The markup below is the template's, verbatim;
21
+ the sheet is its <style>; its one script is split into the seven sky-*.js
22
+ files under the 1,500-line ratchet (data → layout → draw → interaction →
23
+ panel → composer → tour). Every deviation a gate or a route forced is
24
+ listed in the shipping task's notes, one line each; anything else that
25
+ differs from the template is a defect.
22
26
 
23
- <div class="app">
24
- <aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
25
- <div class="app-scrim" id="app-scrim" hidden></div>
26
- <div class="app-body">
27
- <header class="app-topbar" id="app-topbar"></header>
28
- <div class="app-content">
29
- <!-- The LABEL voice, like every other working surface: the page's own name
30
- is the least interesting thing on it (modules/hall-ui/CLAUDE.md, the
31
- two page-head voices). The room is a working surface, not a document,
32
- so it takes the board's head and not the reading room's. -->
33
- <div class="page-head page-head--board">
34
- <h1 class="page-head__h">Your thinking</h1>
35
- <p class="page-head__sub" id="tr-sub">Loading what you filed…</p>
36
- </div>
27
+ The file is still `thinking` and the URL still /thinking: this is the
28
+ ideator's home, the page that replaced THE ROOM (task 1004098) — and the
29
+ room survives inside it as the list drawer (L). /sky, the old ambient
30
+ page's URL, 301s here (src/bongos/serve-internal.js). It carries no hall
31
+ chrome on purpose: the sky is a full-screen surface with its own header,
32
+ and the hall's doors (the record, the goal, the task) open from the panel. -->
33
+ <body data-page="thinking" data-page-title="Your sky">
37
34
 
38
- <main id="hall-main">
39
- <section class="scroll" id="tr-scroll" aria-labelledby="tr-h">
40
- <h2 id="tr-h" class="scroll__h">What you filed, and what became of it</h2>
41
- <p class="scroll__lede">
42
- Your own view of your own thinking. Size is the work a thought caused and
43
- never falls; brightness is how much of that work is still alive — that is
44
- the part that can go out. The same trail is also a map, in
45
- <a href="/builders/sky">your sky</a>.
46
- </p>
35
+ <!-- The one <main> landmark the kit's a11y floor asks of every hall page
36
+ (one-header-main-h1). The template had none; the canvas is the page's
37
+ content, and the landmark wraps it with no rule of its own, so nothing
38
+ moves — the canvas is position: fixed. -->
39
+ <main>
40
+ <canvas id="sky" tabindex="0" role="img" aria-label="The sky, drawn from the ledger. Every thought and goal here is also a row in the list; press L to open it, ? to take the tour."></canvas>
41
+ </main>
47
42
 
48
- <!-- The figures, the filters and the rows are all written by
49
- thinking.js from ONE read, so nothing here can be a second copy of
50
- a number the server already computed. -->
51
- <div id="tr-figs" class="tr-figs" role="status" aria-live="polite"></div>
52
- <div id="tr-filters" class="tr-filters" role="group" aria-label="Show only"></div>
53
- <div id="tr-body"><p class="tr-note">Loading…</p></div>
54
- <p class="tr-foot" id="tr-foot" hidden></p>
55
- </section>
56
- </main>
57
-
58
- <footer class="app-foot" id="app-foot"></footer>
59
- </div>
43
+ <!-- The template wrote the whole top strip as the <header>. The kit's phone
44
+ floor (top-bar-80px) measures the page's <header>, and the strip wraps to
45
+ 125px at 390 by the template's own rule — so the header is the BRAND (the
46
+ h1 and the figures) and the lens and tools stay its siblings inside the
47
+ same .top strip. No selector names the element, so the render is the
48
+ template's, pixel for pixel. -->
49
+ <div class="top">
50
+ <header class="brand">
51
+ <h1>your sky <em id="lensWord">— your thinking</em></h1>
52
+ <div class="stats" id="stats"></div>
53
+ </header>
54
+ <div class="lens" role="group" aria-label="Filter" id="lensGroup">
55
+ <button type="button" id="lensSimple" aria-pressed="false">simple</button>
56
+ <button type="button" id="lensMine" aria-pressed="true">your thinking</button>
57
+ <button type="button" id="lensAll" aria-pressed="false">the whole project</button>
58
+ </div>
59
+ <div class="tools" id="tools">
60
+ <button type="button" class="pill" id="roomBtn" aria-pressed="false" title="The same sky, as a list (L)">list</button>
61
+ <button type="button" class="pill" id="recentBtn" aria-pressed="false" hidden title="Only the thoughts of the last thirty days.">recent</button>
62
+ <button type="button" class="pill" id="claudeBtn" aria-pressed="false" title="Show the thoughts a session filed under your name. Off by default: only what a person filed.">claude layer</button>
63
+ <button type="button" class="pill" id="influenceBtn" aria-pressed="false" title="Only the goals your thoughts touched — everything else fades.">my influence</button>
64
+ <button type="button" class="pill" id="exploreBtn" aria-pressed="true" hidden title="Explore: the sky reveals itself as you follow routes from where you begin.">explore</button>
65
+ <button type="button" class="pill" id="ideasBtn" aria-pressed="false" hidden title="Lay your thoughts over the project map.">my ideas</button>
66
+ <button type="button" class="pill" id="rehearse" aria-pressed="false" title="What if: invents removals, dimming and nods — none has happened in the ledger yet — so you can see how the dead would read. Sizes are never invented.">what if</button>
67
+ <button type="button" class="pill icon" id="clockBtn" title="Show or hide the clock" aria-label="Show or hide the clock">◷</button>
68
+ <button type="button" class="pill icon" id="tourBtn" title="How to read the sky (?)" aria-label="How to read the sky">?</button>
69
+ <button type="button" class="pill icon" id="fitBtn" title="Fit the sky (0)" aria-label="Fit the whole sky">⌂</button>
60
70
  </div>
61
71
  </div>
62
72
 
63
- <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
73
+ <div class="jump">
74
+ <input type="search" id="jump" placeholder="jump to a goal or a thought" autocomplete="off" aria-label="Jump to a goal or a thought">
75
+ <ul id="jumpList" hidden></ul>
76
+ </div>
77
+
78
+ <div class="composer" id="composerWrap">
79
+ <form id="file" autocomplete="off">
80
+ <input type="text" id="thought" placeholder="release a thought — one line" aria-label="Release a thought" maxlength="180">
81
+ <button type="button" class="mode" id="fullToggle" aria-pressed="false" title="File it as a Full Idea: the line, then five questions, scored on completeness. Off = a quick idea, one line.">full idea</button>
82
+ <button type="submit" class="go" id="fileGo">file</button>
83
+ </form>
84
+ <div class="gravity" id="gravity" aria-live="polite">gravity — the goal that catches it lights as you type; letting it float is a real answer</div>
85
+ <div class="steps" id="fullSteps" hidden></div>
86
+ <div class="receipt" id="receipt" hidden role="status"><span class="dot"></span><span id="receiptText"></span><button type="button" id="receiptGo">see it</button><button type="button" id="receiptX" aria-label="Dismiss">×</button></div>
87
+ </div>
88
+
89
+ <div class="card glass" id="card" role="tooltip" aria-hidden="true"></div>
90
+ <div class="menu glass" id="menu" hidden role="menu" aria-label="Chart the routes"></div>
91
+
92
+ <aside class="room" id="room" hidden aria-label="The list">
93
+ <div class="head"><h2 id="roomTitle">the room</h2><span class="lbl" id="roomCount"></span></div>
94
+ <div class="filters" id="filters" role="group" aria-label="Filter"></div>
95
+ <div class="rows" id="rows"></div>
96
+ </aside>
97
+
98
+ <aside class="panel" id="panel" hidden aria-live="polite"></aside>
99
+
100
+ <div class="start glass" id="start" hidden role="dialog" aria-label="Where do you begin?">
101
+ <span class="lbl">explore</span>
102
+ <h3>Where do you begin?</h3>
103
+ <p>The sky reveals itself as you go. Pick a well to start in; every route you chart brings the goal at its far end into view.</p>
104
+ <div class="opts" id="startOpts"></div>
105
+ </div>
106
+
107
+ <div class="tour glass" id="tour" hidden role="dialog" aria-label="How to read the sky">
108
+ <div class="step"><span class="lbl" id="tourN"></span><span class="dots" id="tourDots"></span></div>
109
+ <h3 id="tourH"></h3>
110
+ <p id="tourP"></p>
111
+ <div class="acts"><button type="button" class="door primary" id="tourNext">next</button><button type="button" class="door" id="tourBack">back</button><button type="button" class="skip" id="tourSkip">skip the tour</button></div>
112
+ </div>
113
+
114
+ <footer class="foot">
115
+ <div class="time" id="time">
116
+ <button type="button" class="play" id="play" aria-pressed="false" aria-label="Replay the sky from the first day">▶</button>
117
+ <input type="range" id="clock" min="0" max="1000" value="1000" aria-label="The day the sky is drawn for">
118
+ <span class="when" id="when">today</span>
119
+ <button type="button" class="hide" id="timeHide" aria-label="Hide the clock">×</button>
120
+ </div>
121
+ <div class="footrow">
122
+ <div class="legend" id="legend"></div>
123
+ <div class="alt" id="alt"></div>
124
+ </div>
125
+ </footer>
64
126
 
65
- <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
66
- <script src="/builders/palette.js" defer></script>
67
- <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
68
- <!-- The filter strip is the kit's tab strip (makeTabs) rather than a fifth
69
- hand-rolled pill row: it already carries the count pills, the hash sync and
70
- the narrow-screen scroller this page would otherwise reinvent. -->
71
- <script src="/builders/hall-kit.js"></script>
72
- <script src="/builders/idea-objects.js?v=2026-09-20-r03"></script>
73
- <script src="/builders/thinking.js?v=2026-09-20-r03"></script>
127
+ <script src="/builders/idea-objects.js?v=2026-09-25-sky3"></script>
128
+ <script src="/builders/sky-data.js?v=2026-09-25-sky3"></script>
129
+ <script src="/builders/sky-layout.js?v=2026-09-25-sky3"></script>
130
+ <script src="/builders/sky-draw.js?v=2026-09-25-sky3"></script>
131
+ <script src="/builders/sky-interaction.js?v=2026-09-25-sky3"></script>
132
+ <script src="/builders/sky-panel.js?v=2026-09-25-sky3"></script>
133
+ <script src="/builders/sky-composer.js?v=2026-09-25-sky3"></script>
134
+ <script src="/builders/sky-tour.js?v=2026-09-25-sky3"></script>
74
135
  </body>
75
136
  </html>
@@ -1,20 +1,44 @@
1
1
  {
2
- "_": "THE ROOM (task 1004098 / BV2.R03) through modules/ui-design/kit/render.js. Every row on this page comes from ONE authenticated read, so against the kit's own stub it renders its signed-out / empty state — which is a state the floors must hold in and is rendered here deliberately. To look at the populated page, run it against the hall-preview harness, whose inbox__by-builder.json fixture carries one of each of the five objects plus a historical size and an unreadable brightness:\n\n node scripts/hall-preview/server.js --port 4615 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/thinking.states.json --base-in http://localhost:4615 --base-out http://localhost:4615",
2
+ "_": "THE SKY (task 1004231 / sky part 3 of 5, goal 1000095; ADR 0331) through modules/ui-design/kit/render.js. The page draws GET /sky + GET /sky/mine; against the kit's own stub neither exists, so every state below renders the EMPTY sky — a real state the floors must hold in (the composer, the tour, the list drawer and the lenses all exist with no thought filed). To look at the populated sky, run it against the hall-preview harness, whose sky.json + sky__mine.json fixtures carry the wells and one of each of the five objects:\n\n node scripts/hall-preview/server.js --port 4615 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/thinking.states.json --base-in http://localhost:4615 --base-out http://localhost:4615\n\nThe tour opens by itself on a first visit (sky-tour-seen), so states that want the bare sky skip it first.",
3
3
  "page": "/builders/thinking",
4
4
  "surface": "hall-ui",
5
5
  "stub": { "prefix": "/builders/" },
6
+ "modes": ["dark"],
6
7
  "modeQuery": false,
7
- "ignoreRequests": ["/api/(gds|bongos)/"],
8
- "ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found", "load failed"],
8
+ "ignoreRequests": ["/api/(gds|bongos)/", "fonts\\.g(oogleapis|static)\\.com"],
9
+ "ignoreConsole": ["/api/(gds|bongos)/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found", "load failed", "fonts\\.g(oogleapis|static)\\.com"],
9
10
  "states": {
10
- "room": { "auth": true, "actions": [["wait", 900]], "expect": { "visible": ["#tr-figs", "#tr-body"] } },
11
- "burning": {
11
+ "tour": {
12
12
  "auth": true,
13
- "_": "The filter strip is the page's only control, and the state it selects is the one a person opens this page for. clickIfVisible and not click: the tab is HIDDEN when the trail carries no stars, which is exactly what the kit's own stub produces, so a bare click would fail the state rather than render it.",
14
- "actions": [["wait", 900], ["clickIfVisible", "[data-tab=\"star\"]"], ["wait", 200]],
15
- "expect": { "visible": ["#tr-body"] }
13
+ "_": "The first visit: the sky with the tour's first step over it.",
14
+ "actions": [["wait", 1400]],
15
+ "expect": { "visible": ["#sky", "#tour"] }
16
+ },
17
+ "sky": {
18
+ "auth": true,
19
+ "_": "The 'your thinking' lens, bare: composer, lenses, tools, legend and the clock.",
20
+ "actions": [["wait", 1200], ["clickIfVisible", "#tourSkip"], ["wait", 300]],
21
+ "expect": { "visible": ["#sky", "#composerWrap"] }
22
+ },
23
+ "list": {
24
+ "auth": true,
25
+ "_": "The list drawer — the room — opened by the L key.",
26
+ "actions": [["wait", 1200], ["clickIfVisible", "#tourSkip"], ["wait", 200], ["press", "l"], ["wait", 300]],
27
+ "expect": { "visible": ["#room", "#rows"] }
28
+ },
29
+ "simple": {
30
+ "auth": true,
31
+ "_": "The simple lens: thoughts in their wells, recent / all, nothing else to press.",
32
+ "actions": [["wait", 1200], ["clickIfVisible", "#tourSkip"], ["wait", 200], ["click", "#lensSimple"], ["wait", 300]],
33
+ "expect": { "visible": ["#sky", "#recentBtn"] }
34
+ },
35
+ "full-idea": {
36
+ "auth": true,
37
+ "_": "The composer with a line typed: gravity says what would catch it. (The full-idea switch is hidden when the template read fails, as it does against the kit stub.)",
38
+ "actions": [["wait", 1200], ["clickIfVisible", "#tourSkip"], ["wait", 200], ["fill", "#thought", "a thought about the sky"], ["wait", 600]],
39
+ "expect": { "visible": ["#composerWrap", "#gravity"] }
16
40
  },
17
41
  "out": { "auth": false, "actions": [["wait", 600]] }
18
42
  },
19
- "reducedMotion": { "auth": true, "actions": [["wait", 900]] }
43
+ "reducedMotion": { "auth": true, "actions": [["wait", 1200], ["clickIfVisible", "#tourSkip"], ["wait", 200]] }
20
44
  }
@@ -162,7 +162,7 @@
162
162
 
163
163
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
164
164
  chrome exists before the page script runs. -->
165
- <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
165
+ <script src="/builders/shell.js?v=2026-09-24-fleetcosts1"></script>
166
166
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
167
167
  <script src="/builders/palette.js" defer></script>
168
168
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
@@ -171,7 +171,7 @@
171
171
 
172
172
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
173
173
  time so the chrome exists before the deferred page scripts execute. -->
174
- <script src="/builders/shell.js?v=2026-09-22-movefeedback1"></script>
174
+ <script src="/builders/shell.js?v=2026-09-24-fleetcosts1"></script>
175
175
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
176
176
  <script src="/builders/palette.js" defer></script>
177
177
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
@@ -49,7 +49,7 @@ const express = require('express');
49
49
  const api = require('../../../src/module-api');
50
50
  const auth = api, { pool, validateOrRespond, parseId } = api;
51
51
  const provisioning = require('../provisioning');
52
- const { costLedger } = require('../cost-ledger');
52
+ const { costLedger, ownerMonthlyUsd } = require('../cost-ledger');
53
53
  const { onboardPlan, stepsForShape } = require('../onboard-plan');
54
54
  const { ALWAYS_ON_CORE, OPTIONAL_MODULES, starterBundleAnswerTable, optionalModules, effectiveModules } = require('../starter-bundles');
55
55
  const { recommendationsForDetails } = require('../recommendations');
@@ -144,10 +144,10 @@ function publishVerdict(row) {
144
144
  }
145
145
 
146
146
  // The owner-facing projection every instance read returns: the allow-listed row
147
- // plus the derived publish verdict. One helper so a new read cannot ship the row
148
- // without the verdict and leave an owner unable to see why they are dark matter.
147
+ // plus the derived publish verdict and the estimate of the owner's own hosting bill
148
+ // (task 1001949; null = unpriced, never 0). Owner- or archon-scoped reads only.
149
149
  function ownerInstance(row) {
150
- return { ...provisioning.publicInstance(row), publish: publishVerdict(row) };
150
+ return { ...provisioning.publicInstance(row), publish: publishVerdict(row), owner_monthly_usd: ownerMonthlyUsd({ hostingShape: row.hosting_shape, tier: row.tier }) };
151
151
  }
152
152
 
153
153
  // Project an instance row for the fleet view — the owner identity + the same
@@ -5573,6 +5573,12 @@ summary{min-height:24px;padding:3px 0;}
5573
5573
 
5574
5574
  /* one label · value pair of the manage ledger */
5575
5575
  function ledgerRow(r) { return '<span class="k">' + r[0] + '</span><span class="v">' + r[1] + '</span>'; }
5576
+ /* the owner's own hosting bill (task 1001949, ADR 0327): an estimate, since the
5577
+ provider bills them directly. null is unpriced — a $0 would be a price nobody quoted */
5578
+ function hostingBill(usd) {
5579
+ if (typeof usd !== 'number' || !isFinite(usd)) return 'not priced yet';
5580
+ return '$' + (usd % 1 ? usd.toFixed(2) : String(usd)) + '/month, estimated. Render bills you directly.';
5581
+ }
5576
5582
  /* the head's planet and the ledger's planet row (task 1003345) — one resolution
5577
5583
  (planetOf) for both, so the head, the row and the sky can never disagree; the
5578
5584
  planet row is the slot the picker (task 1003347) fills */
@@ -5601,6 +5607,7 @@ summary{min-height:24px;padding:3px 0;}
5601
5607
  var server = inst.hosting_shape ? esc(SHAPE_LABEL[inst.hosting_shape] || inst.hosting_shape) : '';
5602
5608
  if (inst.onboard_mode === 'adopt') server += (server ? ', ' : '') + 'adopted repo';
5603
5609
  if (server) rows.push(['server', server]);
5610
+ if (inst.hosting_shape === 'cloud-host' && inst.status !== 'torn_down') rows.push(['hosting bill', hostingBill(inst.owner_monthly_usd)]);
5604
5611
  $('mMeta').innerHTML = rows.map(ledgerRow).join('');
5605
5612
  $('mMeta').hidden = !rows.length;
5606
5613
  var addr = addressRow(inst, true);
@@ -26,6 +26,7 @@
26
26
  "domain": null,
27
27
  "target_ref": "builder/harbor",
28
28
  "hosting_shape": "cloud-host",
29
+ "owner_monthly_usd": null,
29
30
  "onboard_mode": "scaffold",
30
31
  "created_at": "$now-6m",
31
32
  "provisioned_at": null,
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1012",
3
+ "version": "1.19.1014",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.1012",
9
+ "version": "1.19.1014",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.1012",
3
+ "version": "1.19.1014",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -462,6 +462,10 @@ const PAGE_GATES = Object.freeze({
462
462
  // — a vulnerability window, and exactly the kind of topology ADR 0043 keeps off
463
463
  // unauthenticated surfaces.
464
464
  requireCorePinPage: Object.freeze({ permission: 'core.pin.move', deniedTo: '/builders' }),
465
+ // The /fleet cost page (task 1001949) — on `provisioning.fleet.manage`, the SAME atom
466
+ // its one read (GET /provisioning/fleet/cost-ledger) checks: every owner's projects
467
+ // and what they cost, so the page reaches exactly as far as the data does.
468
+ requireFleetPage: Object.freeze({ permission: 'provisioning.fleet.manage', deniedTo: '/builders' }),
465
469
  });
466
470
 
467
471
  // The `alsoAdmits` door — "is this visitor the page's OTHER audience?", asked of
@@ -637,6 +641,9 @@ async function requireBoardVotePage(req, res, next) {
637
641
  async function requireCorePinPage(req, res, next) {
638
642
  return runPageGate('requireCorePinPage', req, res, next);
639
643
  }
644
+ async function requireFleetPage(req, res, next) {
645
+ return runPageGate('requireFleetPage', req, res, next);
646
+ }
640
647
 
641
648
  // ---------------------------------------------------------------------------
642
649
  // Route-gate inventory (V3.R06 #231) — the canonical "who can call what" list.
@@ -929,6 +936,7 @@ module.exports = {
929
936
  requireProjectCuratePage,
930
937
  requireBoardVotePage,
931
938
  requireCorePinPage,
939
+ requireFleetPage,
932
940
  requireRank,
933
941
  rankMeetsThreshold,
934
942
  requirePermission,
@@ -127,7 +127,10 @@ const discordIsLinked = (builderId) =>
127
127
  // this projection omitted it. Adding an atom here is the deliberate act this comment
128
128
  // already described; tests/nav_permission_atoms.mjs now fails if shell.js gates on
129
129
  // one that is missing, so the act cannot be skipped again.
130
- const NAV_PERMISSION_ATOMS = Object.freeze(['board.vote.cast', 'core.pin.move']);
130
+ // `idea.triage` (task 1004231): the sky's panel draws "nod — make it claimable"
131
+ // on a thought awaiting a nod only for a builder who may triage ideas; the nod
132
+ // itself runs the existing POST /tasks/:id/promote, which stays the wall.
133
+ const NAV_PERMISSION_ATOMS = Object.freeze(['board.vote.cast', 'core.pin.move', 'idea.triage']);
131
134
 
132
135
  // navPermissionsFor(builderId) → { '<atom>': boolean } over NAV_PERMISSION_ATOMS,
133
136
  // or null when authority could not be established (government off, port missing,
@@ -954,6 +954,8 @@ function mountInternalSurfaces(app) {
954
954
  // statically, and a comment in that gap makes the page test as UNGATED while
955
955
  // looking gated here. Prose goes above the const, as here.
956
956
  const DEPLOY_PAGE_RE = /^\/builders\/deploy(?:\.html)?\/?$/;
957
+ // The fleet cost page (task 1001949), on the atom its one read checks.
958
+ const FLEET_PAGE_RE = /^\/builders\/fleet(?:\.html)?\/?$/;
957
959
  // /scouting WAS the legacy twin of /people — the same /api/gds/scouting
958
960
  // directory, left behind when task 1002577 graduated it into the hall. Task
959
961
  // 1003470 had already made the two one page (scouting.html rendered people's
@@ -1004,6 +1006,9 @@ function mountInternalSurfaces(app) {
1004
1006
  if (DEPLOY_PAGE_RE.test(norm)) {
1005
1007
  return gdsAuth.requireCorePinPage(req, res, next);
1006
1008
  }
1009
+ if (FLEET_PAGE_RE.test(norm)) {
1010
+ return gdsAuth.requireFleetPage(req, res, next);
1011
+ }
1007
1012
  next();
1008
1013
  });
1009
1014
 
@@ -1166,13 +1171,13 @@ function mountInternalSurfaces(app) {
1166
1171
  // task 1003479 (goal 1000089): `goal-map` — the map of which goals wait on which,
1167
1172
  // reached from Goals. It reads GET /goals/graph, which is `requireBuilder`, so a
1168
1173
  // page gated any lower would show a signed-out visitor an empty shell.
1169
- // task 1004105 (BV2.R26): `sky` — THE SKY, the ambient view over the same
1170
- // trail the room lists — joins that tier for exactly the same reasons. It is
1171
- // the SAME read (GET /inbox/by-builder/:id with the id from GET /me), drawn
1172
- // as a map instead of a table, so a floor above sign-in would protect on one
1173
- // page what it admits on the other.
1174
+ // task 1004231 (sky part 3): `thinking` is now THE SKY itself — the prototype
1175
+ // the owner adopted (docs/design/mocks/sky/, ADR 0331) built over GET /sky +
1176
+ // GET /sky/mine, with the room inside it as the list drawer. The old ambient
1177
+ // page at `sky` (task 1004105) is gone; its URL 301s to /thinking below, so it
1178
+ // leaves this regex — one URL cannot be two tiers, and a redirect is not a page.
1174
1179
  const SIGNEDIN_BUILDERS_PAGE_RE =
1175
- /^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|ideas|thinking|sky|collab|task|idea|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
1180
+ /^\/builders\/(?:work|settings|atlas|modules|goals|goal-map|roadmap|copy-desk|studio|ideas|thinking|collab|task|idea|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
1176
1181
  // The diagram image assets under /builders/diagrams/ ride the same tier as the
1177
1182
  // page that embeds them.
1178
1183
  const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
@@ -1227,6 +1232,20 @@ function mountInternalSurfaces(app) {
1227
1232
  res.redirect(301, `${prefix}/work${qs}`);
1228
1233
  });
1229
1234
 
1235
+ // task 1004231 (sky part 3 of 5, ADR 0331): the ideator's sky replaced BOTH
1236
+ // the room (/thinking) and the old ambient sky (/sky, task 1004105) with one
1237
+ // page at /thinking. The old URL survives as a 301 so a bookmark or the
1238
+ // room's old lede link lands on the sky — same shape as the /board → /work
1239
+ // redirect above: apex and subdomain forms, with or without .html, query
1240
+ // string preserved. The page gate never sees /sky: a redirect is not a page.
1241
+ app.get(/^(?:\/builders)?\/sky(?:\.html)?\/?$/, (req, res, next) => {
1242
+ if (isModuleSurfaceHost(req)) return next();
1243
+ const prefix = req.path.startsWith('/builders') ? '/builders' : '';
1244
+ const qi = req.url.indexOf('?');
1245
+ const qs = qi >= 0 ? req.url.slice(qi) : '';
1246
+ res.redirect(301, `${prefix}/thinking${qs}`);
1247
+ });
1248
+
1230
1249
  // Onboarding diagram PNGs (V3.R76 #295) live in docs/onboarding/diagrams as the
1231
1250
  // single source of truth alongside their Mermaid sources. Serve them here so the
1232
1251
  // builders'-hall diagrams page can embed them without duplicating the binaries.
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.1012'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.1014'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -98,16 +98,17 @@ test('AUDIT: every async middleware mounted directly on a router owns its reject
98
98
  // requireBoardVotePage / ADR 0266 /board-room vote gate)
99
99
  // requireCorePinPage / ADR 0293's /deploy door — the narrowest of them,
100
100
  // on the archon-floor `core.pin.move` atom.
101
+ // requireFleetPage — task 1001949's /fleet cost page, same delegation.
101
102
  // Task 1003450 removed two more — requireNonXenosPage and the R113
102
103
  // requireGovernmentManagePage — which had both ended up dispatched from no
103
104
  // route at all; tests/hall_page_gate_map.mjs now fails if that recurs.
104
105
  // Anything else async and router-mounted must be added with its own try/catch.
105
106
  const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'auth.js'), 'utf8');
106
107
  const asyncMw = [...src.matchAll(/^async function (require\w+|allow\w+)\s*\(req, res, next\)/gm)].map((m) => m[1]);
107
- assert.deepEqual(asyncMw.sort(), ['requireBoardVotePage', 'requireBuilder', 'requireBuilderPage', 'requireCorePinPage', 'requireGovernmentPage', 'requireProjectCuratePage'],
108
+ assert.deepEqual(asyncMw.sort(), ['requireBoardVotePage', 'requireBuilder', 'requireBuilderPage', 'requireCorePinPage', 'requireFleetPage', 'requireGovernmentPage', 'requireProjectCuratePage'],
108
109
  'a new async router middleware appeared — give it a try/catch and add it here');
109
110
  // The page gates must actually delegate (that is WHY they are safe).
110
- for (const name of ['requireBuilderPage', 'requireGovernmentPage', 'requireProjectCuratePage', 'requireBoardVotePage', 'requireCorePinPage']) {
111
+ for (const name of ['requireBuilderPage', 'requireGovernmentPage', 'requireProjectCuratePage', 'requireBoardVotePage', 'requireCorePinPage', 'requireFleetPage']) {
111
112
  const fn = src.slice(src.indexOf(`async function ${name}`));
112
113
  assert.match(sliceFn(fn), /runPageGate\(/, `${name} must delegate to the guarded runPageGate`);
113
114
  }