@bongos/core 1.19.1052 → 1.19.1054
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.
- package/.bongos-core.json +33 -33
- package/docs/copy-inventory.md +100 -114
- package/docs/copy-registry.json +163 -401
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/goal-map.html +18 -24
- package/modules/hall-ui/public/goal-map.states.json +2 -2
- package/modules/hall-ui/public/sky-data.js +22 -18
- package/modules/hall-ui/public/sky-interaction.js +23 -22
- package/modules/hall-ui/public/sky-layout.js +5 -5
- package/modules/hall-ui/public/sky-panel.js +3 -2
- package/modules/hall-ui/public/sky-tour.js +37 -26
- package/modules/hall-ui/public/thinking.css +2 -0
- package/modules/hall-ui/public/thinking.html +16 -25
- package/modules/hall-ui/public/thinking.states.json +1 -1
- package/modules/hall-ui/records/sky.md +11 -0
- package/modules/provisioning/control-plane-guard.js +14 -1
- package/modules/provisioning/provisioning.js +5 -4
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/provision-units.js +38 -2
- package/scripts/gds/provision.js +6 -6
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_door.mjs +16 -2
- package/tests/goal_map_page.mjs +49 -25
- package/tests/hall_sky.mjs +49 -12
- package/tests/helpers.mjs +2 -1
- package/tests/provision_settings_apply.mjs +84 -0
- package/tests/provisioning_settings_apply.mjs +50 -5
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
(tests/hall_tokens.mjs: no hall page hardcodes a web-font stylesheet).
|
|
14
14
|
The template carried its own Google Fonts link for the same two faces. -->
|
|
15
15
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
16
|
-
<link rel="stylesheet" href="/builders/thinking.css?v=2026-09-
|
|
16
|
+
<link rel="stylesheet" href="/builders/thinking.css?v=2026-09-27-split">
|
|
17
17
|
</head>
|
|
18
18
|
<!-- THE SKY (task 1004231 / sky part 3 of 5, goal 1000095; ADR 0331, ADR 0227
|
|
19
19
|
§11). This page is docs/design/mocks/sky/sky.template.html — the prototype
|
|
@@ -41,12 +41,13 @@
|
|
|
41
41
|
<header>: the shell's top bar is the page's one <header>, and in full
|
|
42
42
|
screen it stays in the document as the way back.
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
44
|
+
ONLY YOUR THINKING (task 1004328, owner decision 2026-09-27). The sheet,
|
|
45
|
+
the seven scripts and the drawing are shared with THE PROJECT SKY at
|
|
46
|
+
/goal-map; the lenses and the tools are not. This page's switcher is
|
|
47
|
+
"simple" and "your thinking" and nothing else. "The whole project", the
|
|
48
|
+
"my ideas" overlay, explore and its start dialog are the project map's
|
|
49
|
+
and live only in goal-map.html, and nothing here leaves for that page as
|
|
50
|
+
a lens. -->
|
|
50
51
|
<body data-page="thinking" data-page-title="Your thinking" data-fullscreen-switch>
|
|
51
52
|
|
|
52
53
|
<div class="app">
|
|
@@ -74,15 +75,12 @@
|
|
|
74
75
|
<div class="lens" role="group" aria-label="Filter" id="lensGroup">
|
|
75
76
|
<button type="button" id="lensSimple" aria-pressed="false">simple</button>
|
|
76
77
|
<button type="button" id="lensMine" aria-pressed="true">your thinking</button>
|
|
77
|
-
<button type="button" id="lensAll" aria-pressed="false">the whole project</button>
|
|
78
78
|
</div>
|
|
79
79
|
<div class="tools" id="tools">
|
|
80
80
|
<button type="button" class="pill" id="roomBtn" aria-pressed="false" title="The same sky, as a list (L)">list</button>
|
|
81
81
|
<button type="button" class="pill" id="recentBtn" aria-pressed="false" hidden title="Only the thoughts of the last thirty days.">recent</button>
|
|
82
82
|
<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>
|
|
83
83
|
<button type="button" class="pill" id="influenceBtn" aria-pressed="false" title="Only the goals your thoughts touched — everything else fades.">my influence</button>
|
|
84
|
-
<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>
|
|
85
|
-
<button type="button" class="pill" id="ideasBtn" aria-pressed="false" hidden title="Lay your thoughts over the project map.">my ideas</button>
|
|
86
84
|
<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>
|
|
87
85
|
<button type="button" class="pill icon" id="clockBtn" title="Show or hide the clock" aria-label="Show or hide the clock">◷</button>
|
|
88
86
|
<button type="button" class="pill icon" id="tourBtn" title="How to read the sky (?)" aria-label="How to read the sky">?</button>
|
|
@@ -117,13 +115,6 @@
|
|
|
117
115
|
|
|
118
116
|
<aside class="panel" id="panel" hidden aria-live="polite"></aside>
|
|
119
117
|
|
|
120
|
-
<div class="start glass" id="start" hidden role="dialog" aria-label="Where do you begin?">
|
|
121
|
-
<span class="lbl">explore</span>
|
|
122
|
-
<h3>Where do you begin?</h3>
|
|
123
|
-
<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>
|
|
124
|
-
<div class="opts" id="startOpts"></div>
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
118
|
<div class="tour glass" id="tour" hidden role="dialog" aria-label="How to read the sky">
|
|
128
119
|
<div class="step"><span class="lbl" id="tourN"></span><span class="dots" id="tourDots"></span></div>
|
|
129
120
|
<h3 id="tourH"></h3>
|
|
@@ -150,13 +141,13 @@
|
|
|
150
141
|
<script src="/builders/shell.js?v=2026-09-27-fullscreen"></script>
|
|
151
142
|
<script src="/builders/palette.js" defer></script>
|
|
152
143
|
|
|
153
|
-
<script src="/builders/idea-objects.js?v=2026-09-
|
|
154
|
-
<script src="/builders/sky-data.js?v=2026-09-
|
|
155
|
-
<script src="/builders/sky-layout.js?v=2026-09-
|
|
156
|
-
<script src="/builders/sky-draw.js?v=2026-09-
|
|
157
|
-
<script src="/builders/sky-interaction.js?v=2026-09-
|
|
158
|
-
<script src="/builders/sky-panel.js?v=2026-09-
|
|
159
|
-
<script src="/builders/sky-composer.js?v=2026-09-
|
|
160
|
-
<script src="/builders/sky-tour.js?v=2026-09-
|
|
144
|
+
<script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
|
|
145
|
+
<script src="/builders/sky-data.js?v=2026-09-27-split"></script>
|
|
146
|
+
<script src="/builders/sky-layout.js?v=2026-09-27-split"></script>
|
|
147
|
+
<script src="/builders/sky-draw.js?v=2026-09-27-split"></script>
|
|
148
|
+
<script src="/builders/sky-interaction.js?v=2026-09-27-split"></script>
|
|
149
|
+
<script src="/builders/sky-panel.js?v=2026-09-27-split"></script>
|
|
150
|
+
<script src="/builders/sky-composer.js?v=2026-09-27-split"></script>
|
|
151
|
+
<script src="/builders/sky-tour.js?v=2026-09-27-split"></script>
|
|
161
152
|
</body>
|
|
162
153
|
</html>
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"sky": {
|
|
17
17
|
"auth": true,
|
|
18
|
-
"_": "The 'your thinking' lens, bare: composer, lenses, tools, legend and the clock.",
|
|
18
|
+
"_": "The 'your thinking' lens, bare: composer, the two lenses (simple, your thinking), tools, legend and the clock.",
|
|
19
19
|
"actions": [["wait", 1200], ["clickIfVisible", "#tourSkip"], ["wait", 300]],
|
|
20
20
|
"expect": { "visible": ["#sky", "#composerWrap"] }
|
|
21
21
|
},
|
|
@@ -30,3 +30,14 @@ Pinned by `tests/hall_sky.mjs` (31 cases: the reads, the five objects, the two c
|
|
|
30
30
|
- **The sky is drawn on a STAGE.** Everything the sky owns lives in `main.sky-stage`, fixed under the top bar and beside the 64px rail (the whole window in full screen, no rail under 768). `contain: layout paint` makes the stage the containing block of every fixed layer, so the template's fixed layout is untouched and simply measures from the stage; its four viewport units became `%` of that box (the same number on `/goal-map`). `SKY.viewport()` gives the canvas its size and the pointer its offset (`S.OX` / `S.OY`), and a `ResizeObserver` re-measures when the switch changes the box without the window resizing.
|
|
31
31
|
- **The sky's palette lives on the stage, not the body, on `/thinking`** — so the chrome around it keeps the hall's own light and dark. `style.css` now loads under `thinking.css`, and the handful of hall rules whose names the template also uses (`.skip`, `.filters`, `.foot`, `:focus-visible`, text inputs, `* { box-sizing }`) are reset under `:where(.sky-stage)` at the same specificity, before the sky's own rules. The sky's global rules are `:where()`-scoped for the same reason, which leaves `/goal-map` byte-for-byte in its old cascade.
|
|
32
32
|
- **Both pages render in both modes now.** The chrome follows the hall theme; the sky's stage is the void in both. The `full` and `full-back` states in both states files (and `full-list` in the sky's) press the switch.
|
|
33
|
+
|
|
34
|
+
## Two skies, one renderer (task 1004328, BV2.TW21)
|
|
35
|
+
|
|
36
|
+
**Owner decision 2026-09-27: "Your thinking is only your thinking."** Part 4's "one page, two doors" above is SUPERSEDED: the two pages share the renderer and nothing else. The owner confirmed that "the your projects tab" means the whole-project lens, its switch and the project-map features. What this settled:
|
|
37
|
+
- **The lens and the features separate; the drawing does not.** `thinking.css`, the seven `sky-*.js` files and every draw path stay shared. What each page may be in is `S.lenses` (set once in `sky-data.js` from `data-sky-lens`): `['simple', 'mine']` on `/thinking`, `['all']` on `/goal-map`. `setLens` clamps to it, so nothing — a button, the tour, a jump, the panel's "descend" — can take a page into a lens it lacks, and no button leaves for the other page (`location.assign` is gone from `sky-interaction.js`).
|
|
38
|
+
- **Absent, not hidden.** `/thinking` carries no "the whole project" button, no "my ideas", no "explore" and no "where do you begin" dialog; `/goal-map` carries no switcher at all and none of the ideator's pills (recent, claude layer, my influence). The scripts touch a one-page control through `SKY.hide` / `SKY.press` / `SKY.on`, which do nothing when the element is not there. `thinking.css` gained one rule, `.brand + .tools { margin-left: auto; }`, so `/goal-map`'s tools keep the switcher's push to the right and its top strip reads as before.
|
|
39
|
+
- **Two tours from one list.** Every step in `ALL_STEPS` says which sky it belongs to; `/thinking` walks nine of the template's thirteen (the ideator's steps, the wells and rings, the clock, the composer, simple), `/goal-map` walks eight — the wells, rings, life, routes, settled systems, the clock and explore — opened by one new step, "This is the whole project.", because the template's first step is "This is your sky." and belongs to `/thinking`. The project tour turns explore off for its length and gives it back. Each tour has its own seen key (S.tourKey: sky-tour-seen here, sky-map-tour-seen on the map), so one does not silence the other.
|
|
40
|
+
- **"Descend into the well" stays in its sky.** On `/goal-map` it only flies; on `/thinking` it steps "simple" up to "your thinking" (the one lens there that zooms). The legend drops "unexplored" on `/thinking`, which has no explore.
|
|
41
|
+
- **Kept on both:** the list drawer (the room on `/thinking`, the ledger on `/goal-map`), the clock, the composer, "what if", the panel. Filing a thought from the project map still works; the owner's decision names the lens and the overlay, not the composer.
|
|
42
|
+
|
|
43
|
+
Pinned by `tests/hall_sky.mjs` (absence of every project-map control, every path into a lens, the nine-step tour never entering "all") and `tests/goal_map_page.mjs` (no switcher, setLens clamped, the eight-step tour with explore off and given back).
|
|
@@ -40,10 +40,23 @@ const { PublicRefusal } = require('./public-refusal');
|
|
|
40
40
|
// the provisioning module — so its hall is the only one with an
|
|
41
41
|
// Upgrade page at all. A preview that cannot run where the button
|
|
42
42
|
// lives is not a preview (task 1004125).
|
|
43
|
+
// settings-apply
|
|
44
|
+
// — push a SAVED setting into the box's own web.env, then restart
|
|
45
|
+
// (task 1004334). It is `restart` plus an in-place edit of the
|
|
46
|
+
// settings lines, and nothing else: the runner leg re-reads the
|
|
47
|
+
// file, replaces only the vocabulary keys (SETTINGS_VOCAB plus its
|
|
48
|
+
// declared companions) and refuses outright if the file cannot be
|
|
49
|
+
// read, so it cannot blank a credential, a port or the database
|
|
50
|
+
// name. It deletes nothing, provisions nothing and moves no pin.
|
|
51
|
+
// Refusing it here protected the box from nothing and left the
|
|
52
|
+
// owner's saved settings on the platform's own row unable to reach
|
|
53
|
+
// the live site — PATCH …/settings on this row stored 'off' while
|
|
54
|
+
// cloudbongos.com kept reading 'strict', and the only remedy was a
|
|
55
|
+
// hand edit on a machine the owner cannot reach.
|
|
43
56
|
//
|
|
44
57
|
// Adding to this list is a deliberate edit someone has to argue for. It is pinned
|
|
45
58
|
// by name in tests/core_upgrade_door.mjs so it cannot grow as a side effect.
|
|
46
|
-
const CONTROL_PLANE_ACTIONS = Object.freeze(['core-upgrade', 'core-upgrade-preflight', 'restart']);
|
|
59
|
+
const CONTROL_PLANE_ACTIONS = Object.freeze(['core-upgrade', 'core-upgrade-preflight', 'restart', 'settings-apply']);
|
|
47
60
|
|
|
48
61
|
/**
|
|
49
62
|
* May this intent action be aimed at an instance of this hosting shape? PURE.
|
|
@@ -104,8 +104,9 @@ const MAX_RELEASED_DOMAINS = 8;
|
|
|
104
104
|
// excludes 'active' (task 1002691).
|
|
105
105
|
const SETTINGS_APPLY_FROM = new Set(['active']);
|
|
106
106
|
// The hosting shapes the runner has a write path to. 'byo-host' is absent (the owner runs
|
|
107
|
-
// it, we reach into nothing)
|
|
108
|
-
|
|
107
|
+
// it, we reach into nothing). 'control-plane' is the runner's OWN box, admitted by name in
|
|
108
|
+
// control-plane-guard.js (task 1004334) so a setting saved on the platform's row reaches it.
|
|
109
|
+
const SETTINGS_APPLY_SHAPES = new Set(['co-tenant', 'cloud-host', 'control-plane']);
|
|
109
110
|
// The ONE state a restart is meaningful from (task 1003238). Liveness is an
|
|
110
111
|
// OVERLAY, not a seventh state — an instance the heartbeat cannot reach is still
|
|
111
112
|
// 'active', and 'active' is exactly where a restart applies. Every earlier state
|
|
@@ -115,8 +116,8 @@ const SETTINGS_APPLY_SHAPES = new Set(['co-tenant', 'cloud-host']);
|
|
|
115
116
|
// SETTINGS_APPLY_FROM: each action declaring its own gate is this module's
|
|
116
117
|
// convention, and the two answer different questions.
|
|
117
118
|
const RESTART_FROM = new Set(['active']);
|
|
118
|
-
// Same reachability rule as the settings push
|
|
119
|
-
// the
|
|
119
|
+
// Same reachability rule as the settings push (the runner can only restart what it runs),
|
|
120
|
+
// minus 'control-plane': task 1004334 gave the self row a settings push, not a restart door.
|
|
120
121
|
const RESTART_SHAPES = new Set(['co-tenant', 'cloud-host']);
|
|
121
122
|
|
|
122
123
|
// Per-project settings (task 1003040, goal 1000058): the known keys + their
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.1054",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.1054",
|
|
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.
|
|
3
|
+
"version": "1.19.1054",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -7613,5 +7613,17 @@
|
|
|
7613
7613
|
"id": "1004327",
|
|
7614
7614
|
"text": "The studio and Your thinking now sit inside the builders hall like every other page, with the sidebar and the top bar around them. Each has one new button in the top-right corner that goes full screen: the sidebar and top bar"
|
|
7615
7615
|
}
|
|
7616
|
+
],
|
|
7617
|
+
"1.19.1053": [
|
|
7618
|
+
{
|
|
7619
|
+
"id": "1004328",
|
|
7620
|
+
"text": "Your thinking and the project map are now two separate pages that share only their drawing. Your thinking keeps just your own views, simple and your thinking, with your own tools: the list, recent, claude layer, my influence,"
|
|
7621
|
+
}
|
|
7622
|
+
],
|
|
7623
|
+
"1.19.1054": [
|
|
7624
|
+
{
|
|
7625
|
+
"id": "1004334",
|
|
7626
|
+
"text": "The platform can now apply a saved setting to its own site, not just to the projects it hosts. Turning the artist gate off on Cloud Bongos will actually reach cloudbongos.com: the platform updates its own settings file and res"
|
|
7627
|
+
}
|
|
7616
7628
|
]
|
|
7617
7629
|
}
|
|
@@ -304,9 +304,44 @@ function originEnvVarsFor(inst) {
|
|
|
304
304
|
};
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
|
|
307
|
+
// `envBody` is the web.env being patched; only the control-plane row reads it (see
|
|
308
|
+
// selfEnvPrefix). Every other shape keeps the provisioned-instance prefix, unchanged.
|
|
309
|
+
function settingsEnvVarsFor(inst, envBody) {
|
|
308
310
|
const { settingsEnvVars } = require(path.join(REPO_ROOT, 'modules/provisioning/provisioning')); // eslint-disable-line global-require
|
|
309
|
-
|
|
311
|
+
const prefix = inst && inst.hosting_shape === 'control-plane' ? selfEnvPrefix({ envBody }) : provisionedEnvPrefix();
|
|
312
|
+
return settingsEnvVars(inst, prefix);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// The env-var prefix the CONTROL PLANE's own web process resolves (task 1004334). The
|
|
316
|
+
// self row is not a provisioned instance: it loads the pack src/branding.js loads, from
|
|
317
|
+
// ITS instance root, so provisionedEnvPrefix() (the neutral pack's floor) is the wrong
|
|
318
|
+
// question — a value written under a prefix the site does not resolve is read by
|
|
319
|
+
// nothing, and the push would report success over a site that never changed.
|
|
320
|
+
//
|
|
321
|
+
// Precedence mirrors loadBranding: the instance pack is a <PREFIX>_BRANDING_FILE line in
|
|
322
|
+
// the web.env being patched (the file the web service actually boots with) when there is
|
|
323
|
+
// one, else <instanceRoot>/config/branding.json; under it sits the core's neutral pack,
|
|
324
|
+
// then the fallback. The first that names an envPrefix wins. A wrong answer is not silent: the
|
|
325
|
+
// leg's read-back (settingsConsumed) compares the live manifest after the restart and
|
|
326
|
+
// records "still reports the OLD values".
|
|
327
|
+
function selfEnvPrefix({ envBody = '', instanceRoot, readFile = (p) => fs.readFileSync(p, 'utf8') } = {}) {
|
|
328
|
+
const { FALLBACK_ENV_PREFIX, LEGACY_ENV_PREFIXES } = require('../../src/instance-config');
|
|
329
|
+
const root = instanceRoot || require('./provision-repo.js').selfInstanceRoot(); // eslint-disable-line global-require
|
|
330
|
+
const names = [FALLBACK_ENV_PREFIX, ...LEGACY_ENV_PREFIXES].map((p) => `${p}_BRANDING_FILE`);
|
|
331
|
+
const override = String(envBody || '').split(/\r?\n/)
|
|
332
|
+
.map((l) => /^([A-Z]+_BRANDING_FILE)=(.*)$/.exec(l.trim()))
|
|
333
|
+
.find((m) => m && names.includes(m[1]) && m[2].trim());
|
|
334
|
+
const candidates = [
|
|
335
|
+
override ? path.resolve(root, override[2].trim()) : path.join(root, 'config', 'branding.json'),
|
|
336
|
+
path.join(REPO_ROOT, ENV_PREFIX_PACK),
|
|
337
|
+
];
|
|
338
|
+
for (const p of candidates) {
|
|
339
|
+
try {
|
|
340
|
+
const pack = JSON.parse(readFile(p));
|
|
341
|
+
if (pack && typeof pack.envPrefix === 'string' && pack.envPrefix) return pack.envPrefix;
|
|
342
|
+
} catch { /* absent or unreadable — the next candidate answers */ }
|
|
343
|
+
}
|
|
344
|
+
return FALLBACK_ENV_PREFIX;
|
|
310
345
|
}
|
|
311
346
|
|
|
312
347
|
// The instance's own public manifest — the settings read-back ADR 0190 names
|
|
@@ -375,6 +410,7 @@ module.exports = {
|
|
|
375
410
|
instanceManifestCmd,
|
|
376
411
|
originEnvVarsFor,
|
|
377
412
|
provisionedEnvPrefix,
|
|
413
|
+
selfEnvPrefix,
|
|
378
414
|
serviceUnit,
|
|
379
415
|
serviceUnitPath,
|
|
380
416
|
settingsConsumed,
|
package/scripts/gds/provision.js
CHANGED
|
@@ -875,12 +875,12 @@ async function domainAttachInstance(inst, deps) {
|
|
|
875
875
|
// ---------------------------------------------------------------------------
|
|
876
876
|
async function settingsApplyInstance(inst, deps) {
|
|
877
877
|
const { db, provisioning, exec, writeFile, log, apply, privileged } = deps;
|
|
878
|
-
//
|
|
879
|
-
const
|
|
880
|
-
const boxExec =
|
|
881
|
-
const boxWriteFile =
|
|
878
|
+
// A standalone runs control-plane-local; the control plane's own row IS this box (task 1004334).
|
|
879
|
+
const onControlBox = inst.hosting_shape === 'cloud-host' || inst.hosting_shape === 'control-plane';
|
|
880
|
+
const boxExec = onControlBox ? (deps.controlExec || exec) : exec;
|
|
881
|
+
const boxWriteFile = onControlBox ? (deps.controlWriteFile || writeFile) : writeFile;
|
|
882
882
|
const sudoP = privileged ? 'sudo ' : '';
|
|
883
|
-
const vars = settingsEnvVarsFor(inst);
|
|
883
|
+
const vars = settingsEnvVarsFor(inst); // the self row's prefix is re-read from the live file at the write
|
|
884
884
|
const shown = Object.entries(vars).map(([k, v]) => `${k}=${v}`).join(' ');
|
|
885
885
|
log(`settings-apply ${inst.slug} (shape=${inst.hosting_shape}, status=${inst.status}): ${shown}`);
|
|
886
886
|
|
|
@@ -917,7 +917,7 @@ async function settingsApplyInstance(inst, deps) {
|
|
|
917
917
|
}
|
|
918
918
|
try {
|
|
919
919
|
log(' [config] patch the settings lines of web.env in place (creds + origins untouched)');
|
|
920
|
-
boxWriteFile(webEnvPath(inst), upsertEnvVars(current,
|
|
920
|
+
boxWriteFile(webEnvPath(inst), upsertEnvVars(current, settingsEnvVarsFor(inst, current)), { mode: 0o600, sudo: true });
|
|
921
921
|
log(' [service] restart so the memoized branding pack re-reads them');
|
|
922
922
|
pmFor(deps).restartService(inst, { exec: boxExec, sudoP });
|
|
923
923
|
} catch (e) {
|
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.
|
|
74
|
+
const CORE_VERSION = '1.19.1054'; // 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');
|
|
@@ -102,18 +102,32 @@ await t('the allow-list is exactly the acts that cannot destroy the box', () =>
|
|
|
102
102
|
// dry run returns at pre-flight having moved no pin, run no npm and restarted
|
|
103
103
|
// nothing, and the control plane is the only instance whose hall HAS an Upgrade
|
|
104
104
|
// page — so refusing the preview there protected nothing and removed everything.
|
|
105
|
+
// Widened a second time, by 'settings-apply' (task 1004334), for the reason argued in
|
|
106
|
+
// the guard's own comment: it is restart plus an in-place edit of the settings lines,
|
|
107
|
+
// and without it a setting saved on the platform's row could never reach the live site.
|
|
105
108
|
assert.deepEqual([...G.CONTROL_PLANE_ACTIONS].sort(),
|
|
106
|
-
['core-upgrade', 'core-upgrade-preflight', 'restart']);
|
|
109
|
+
['core-upgrade', 'core-upgrade-preflight', 'restart', 'settings-apply']);
|
|
107
110
|
});
|
|
108
111
|
|
|
109
112
|
await t('the preview is permitted on the control plane — the one row that can use it', () => {
|
|
110
113
|
assert.equal(G.intentAllowedForShape('core-upgrade-preflight', 'control-plane'), true);
|
|
111
114
|
});
|
|
112
115
|
|
|
116
|
+
await t('the settings push is permitted on the control plane, and the guard says WHY by name (task 1004334)', () => {
|
|
117
|
+
assert.equal(G.intentAllowedForShape('settings-apply', 'control-plane'), true);
|
|
118
|
+
// The argument is part of the edit: an entry added without its reason is exactly the
|
|
119
|
+
// "grew as a side effect" this pin exists to stop.
|
|
120
|
+
const src = read('modules', 'provisioning', 'control-plane-guard.js');
|
|
121
|
+
const reason = src.slice(src.indexOf('// settings-apply'), src.indexOf('const CONTROL_PLANE_ACTIONS'));
|
|
122
|
+
assert.ok(reason.length > 0 && src.includes('// settings-apply'), 'the guard comment carries a settings-apply entry');
|
|
123
|
+
assert.match(reason, /task 1004334/, 'the reason names the task that argued it');
|
|
124
|
+
assert.match(reason, /deletes nothing/, 'the reason states the property that makes it safe');
|
|
125
|
+
});
|
|
126
|
+
|
|
113
127
|
await t('and widening it did NOT let the destructive acts through', () => {
|
|
114
128
|
// The whole risk of arguing one action onto an allow-list is arguing four.
|
|
115
129
|
for (const a of P.INTENT_ACTIONS) {
|
|
116
|
-
if (['core-upgrade', 'core-upgrade-preflight', 'restart'].includes(a)) continue;
|
|
130
|
+
if (['core-upgrade', 'core-upgrade-preflight', 'restart', 'settings-apply'].includes(a)) continue;
|
|
117
131
|
assert.equal(G.intentAllowedForShape(a, 'control-plane'), false,
|
|
118
132
|
`'${a}' must still be refused against the box the platform runs on`);
|
|
119
133
|
}
|
package/tests/goal_map_page.mjs
CHANGED
|
@@ -20,7 +20,13 @@
|
|
|
20
20
|
// The REAL goal-map.html markup and the REAL seven sky-*.js files boot in a vm
|
|
21
21
|
// through bootSkyPage (tests/helpers.mjs) — the same helper tests/hall_sky.mjs
|
|
22
22
|
// uses for /thinking, pointed at this page's own file, so a button lost from
|
|
23
|
-
// either
|
|
23
|
+
// either page fails its own suite.
|
|
24
|
+
//
|
|
25
|
+
// TWO SKIES, ONE RENDERER (task 1004328, owner decision 2026-09-27). The
|
|
26
|
+
// project sky is the ONLY home of the whole-project lens and its features —
|
|
27
|
+
// explore, "my ideas", the ledger of goals — and it offers no other lens: no
|
|
28
|
+
// switcher, no "simple", no "your thinking", and the ideator's pills
|
|
29
|
+
// (recent, claude layer, my influence) are /thinking's alone.
|
|
24
30
|
//
|
|
25
31
|
// Run: node tests/goal_map_page.mjs
|
|
26
32
|
import assert from 'node:assert/strict';
|
|
@@ -85,45 +91,45 @@ const goal = (p, id) => p.S().goalById.get(String(id));
|
|
|
85
91
|
|
|
86
92
|
// ═══ the lens is home ════════════════════════════════════════════════════════
|
|
87
93
|
|
|
88
|
-
await test('it opens in "the whole project": the brand line, the
|
|
94
|
+
await test('it opens in "the whole project": the brand line, the tools and the figures are the project sky\'s', async () => {
|
|
89
95
|
const p = await boot({ sky: FOUR(), versions: versions(), mine: mine([star()]) });
|
|
90
96
|
assert.ok(p.ready(), 'the page booted');
|
|
91
97
|
const S = p.S();
|
|
92
98
|
assert.equal(S.home, 'all', 'the body says which lens is home');
|
|
93
99
|
assert.equal(S.lens, 'all');
|
|
94
|
-
assert.equal(p.el('lensAll').getAttribute('aria-pressed'), 'true');
|
|
95
|
-
assert.equal(p.el('lensMine').getAttribute('aria-pressed'), 'false');
|
|
96
100
|
assert.equal(p.el('lensWord').textContent, '— the whole project');
|
|
97
101
|
assert.equal(p.el('lensWord').parentNode.textContent, 'the sky — the whole project', 'the h1 is the project sky\'s');
|
|
98
102
|
assert.equal(p.el('roomTitle').textContent, 'the ledger');
|
|
99
103
|
assert.equal(p.el('exploreBtn').hidden, false, 'explore is a tool here');
|
|
100
104
|
assert.equal(p.el('ideasBtn').hidden, false, 'so is "my ideas"');
|
|
101
|
-
assert.equal(p.el(
|
|
102
|
-
assert.equal(p.el('influenceBtn').hidden, true);
|
|
105
|
+
for (const id of ['claudeBtn', 'influenceBtn', 'recentBtn']) assert.equal(p.el(id), null, `the ideator's #${id} is not on this page at all`);
|
|
103
106
|
assert.match(p.stats(), /<b>4<\/b> goals · <b>1<\/b> alive · <b>2<\/b> wells · <b>1<\/b> settled · <b>1<\/b> collapsed · <b>1<\/b> routes · <b>13<\/b> tasks/, 'the figures line is the goals\' line');
|
|
104
107
|
const urls = p.requests.map((r) => r.url);
|
|
105
108
|
for (const want of ['/api/bongos/sky', '/api/bongos/sky/mine', '/api/bongos/me', '/api/bongos/inbox/template']) assert.ok(urls.includes(want), `reads ${want}`);
|
|
106
109
|
assert.ok(!urls.some((u) => /goals\/graph/.test(u)), 'the goal map\'s aggregate read is gone — the sky\'s own reads carry the routes');
|
|
107
110
|
});
|
|
108
111
|
|
|
109
|
-
await test('the same seven files open /thinking in "your thinking" — one
|
|
112
|
+
await test('the same seven files open /thinking in "your thinking" — one renderer, two skies', async () => {
|
|
110
113
|
const p = bootSkyPage({ page: 'thinking', sky: FOUR(), versions: versions() }); await drain();
|
|
111
114
|
assert.equal(p.S().home, 'mine');
|
|
112
115
|
assert.equal(p.S().lens, 'mine');
|
|
113
116
|
assert.equal(p.el('lensWord').parentNode.textContent, 'your sky — your thinking');
|
|
114
117
|
});
|
|
115
118
|
|
|
116
|
-
await test('
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
assert.
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
assert.equal(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
assert.equal(
|
|
126
|
-
|
|
119
|
+
await test('THE WHOLE PROJECT ONLY: no switcher, no "simple", no "your thinking" — and no lens this page lacks is ever entered', async () => {
|
|
120
|
+
const p = await boot({ sky: FOUR(), versions: versions(), mine: mine([star()]) });
|
|
121
|
+
const S = p.S(); const SKY = p.SKY();
|
|
122
|
+
assert.deepEqual(JSON.parse(JSON.stringify(S.lenses)), ['all']);
|
|
123
|
+
for (const id of ['lensGroup', 'lensSimple', 'lensMine', 'lensAll']) assert.equal(p.el(id), null, `#${id} is not on this page`);
|
|
124
|
+
const html = read('goal-map.html').replace(/<!--[\s\S]*?-->/g, '');
|
|
125
|
+
assert.equal(/your thinking|>simple</.test(html), false, 'the page offers neither lens in its markup');
|
|
126
|
+
assert.equal(/href="[^"]*\/thinking"/.test(html), false, 'and never links to /thinking (the shared sheet is thinking.css, which is not a link to the page)');
|
|
127
|
+
for (const l of ['mine', 'simple']) { SKY.setLens(l, true); assert.equal(S.lens, 'all', `setLens('${l}') stays in the whole project`); }
|
|
128
|
+
assert.equal(p.el('lensWord').parentNode.textContent, 'the sky — the whole project');
|
|
129
|
+
SKY.pick({ kind: 'goal', ref: goal(p, '7') }, false); p.click('descendBtn');
|
|
130
|
+
assert.equal(S.lens, 'all', 'descending stays on the project map');
|
|
131
|
+
assert.equal(p.navigated, null, 'nothing leaves for /thinking');
|
|
132
|
+
assert.ok(/\.brand \+ \.tools \{ margin-left: auto; \}/.test(read('thinking.css')), 'the tools take the switcher\'s push to the right, so the top strip reads as before');
|
|
127
133
|
});
|
|
128
134
|
|
|
129
135
|
// ═══ the wells ═══════════════════════════════════════════════════════════════
|
|
@@ -339,19 +345,37 @@ await test('a band is a version, and clicking one fills the screen with it', asy
|
|
|
339
345
|
|
|
340
346
|
// ═══ the tour ════════════════════════════════════════════════════════════════
|
|
341
347
|
|
|
342
|
-
await test('the tour
|
|
343
|
-
const p = await boot({ sky: FOUR(), versions: versions() });
|
|
348
|
+
await test('the tour is the project map\'s own eight steps, all in the whole project, with explore off while it runs and given back after', async () => {
|
|
349
|
+
const p = await boot({ sky: FOUR(), versions: versions(), storage: {} });
|
|
350
|
+
const S = p.S();
|
|
351
|
+
assert.deepEqual(S.explore.on, true, 'explore is on before the tour');
|
|
352
|
+
assert.deepEqual(JSON.parse(JSON.stringify(p.SKY().STEPS.map((s) => s.h))), [
|
|
353
|
+
'This is the whole project.', 'A goal is a gravity well.', 'A ring is a criterion.', 'An alive well glows.',
|
|
354
|
+
'Routes run between goals that wait on each other.', 'A settled system is a goal that finished.', 'The sky has a clock.', 'By default, you explore.',
|
|
355
|
+
]);
|
|
344
356
|
p.key('?');
|
|
345
357
|
assert.equal(p.el('tour').hidden, false);
|
|
346
|
-
assert.equal(p.
|
|
347
|
-
|
|
348
|
-
assert.equal(
|
|
358
|
+
assert.equal(p.el('tourN').textContent, 'how to read the sky · 1 of 8');
|
|
359
|
+
assert.equal(S.lens, 'all', 'step one is the whole project');
|
|
360
|
+
assert.equal(S.explore.on, false, 'and the tour sees every well');
|
|
361
|
+
assert.equal(p.el('start').hidden, true, 'so "where do you begin" waits');
|
|
362
|
+
const lenses = new Set([S.lens]);
|
|
363
|
+
for (let n = 0; n < 7; n++) { p.click('tourNext'); lenses.add(S.lens); }
|
|
364
|
+
assert.deepEqual([...lenses], ['all'], 'no step of this tour leaves the whole project');
|
|
365
|
+
assert.equal(p.el('tourN').textContent, 'how to read the sky · 8 of 8');
|
|
349
366
|
p.key('Escape');
|
|
350
367
|
assert.equal(p.el('tour').hidden, true);
|
|
351
|
-
assert.equal(
|
|
352
|
-
assert.equal(
|
|
368
|
+
assert.equal(S.lens, 'all', 'back to the whole project');
|
|
369
|
+
assert.equal(S.explore.on, true, 'explore is given back');
|
|
370
|
+
assert.equal(p.el('exploreBtn').getAttribute('aria-pressed'), 'true');
|
|
353
371
|
const first = await boot({ sky: FOUR(), tourSeen: false });
|
|
354
372
|
assert.equal(first.timers.pending(), 1, 'a first visit schedules the tour here as well');
|
|
373
|
+
// The two tours differ, so each keeps its own seen key (task 1004328).
|
|
374
|
+
const afterThinking = await boot({ sky: FOUR(), tourSeen: false, storage: { 'sky-explore': 'off', 'sky-tour-seen': '4' } });
|
|
375
|
+
assert.equal(afterThinking.timers.pending(), 1, 'having seen /thinking\'s tour does not mark this one seen');
|
|
376
|
+
assert.equal(afterThinking.S().tourKey, 'sky-map-tour-seen');
|
|
377
|
+
const seenHere = await boot({ sky: FOUR(), tourSeen: false, storage: { 'sky-explore': 'off', 'sky-map-tour-seen': '4' } });
|
|
378
|
+
assert.equal(seenHere.timers.pending(), 0, 'and this one, once seen, stays seen');
|
|
355
379
|
});
|
|
356
380
|
|
|
357
381
|
// ═══ born of a thought (part 5) ══════════════════════════════════════════════
|
package/tests/hall_sky.mjs
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// tests/hall_thinking_room.mjs.
|
|
21
21
|
|
|
22
22
|
import assert from 'node:assert/strict';
|
|
23
|
+
import * as FS from 'node:fs';
|
|
23
24
|
import { makeRunner, settle, bootSkyPage } from './helpers.mjs';
|
|
24
25
|
|
|
25
26
|
const { test, summary } = makeRunner();
|
|
@@ -367,11 +368,38 @@ await test('the bands come from the versions read: building at the centre, shipp
|
|
|
367
368
|
for (const f of src) assert.doesNotMatch(fs.readFileSync(path.join('modules/hall-ui/public', f), 'utf8'), /BONGOS-V\d|GDS-V\d|PMS-V\d|CB-V\d/, `${f} names no instance version`);
|
|
368
369
|
});
|
|
369
370
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
371
|
+
// ═══ only your thinking (task 1004328, owner decision 2026-09-27) ═══════════
|
|
372
|
+
// The whole-project lens and the project map's features live only on
|
|
373
|
+
// /goal-map (tests/goal_map_page.mjs). Here they are ABSENT from the markup,
|
|
374
|
+
// not merely hidden, and nothing — a button, the tour, a jump, the panel —
|
|
375
|
+
// takes this sky into "the whole project" or leaves for that page.
|
|
376
|
+
|
|
377
|
+
await test('ONLY YOUR THINKING: no "the whole project" lens, no "my ideas", no explore — absent from the page, not hidden', async () => {
|
|
378
|
+
const p = await boot({ mine: mine(FIVE()), sky: sky([well()]), versions: versions() });
|
|
379
|
+
same(p.S().lenses, ['simple', 'mine']);
|
|
380
|
+
for (const id of ['lensAll', 'ideasBtn', 'exploreBtn', 'start', 'startOpts']) assert.equal(p.el(id), null, `#${id} is the project map's, not this page's`);
|
|
381
|
+
for (const id of ['lensSimple', 'lensMine', 'recentBtn', 'claudeBtn', 'influenceBtn', 'rehearse']) assert.ok(p.el(id), `#${id} is your thinking's own tool`);
|
|
382
|
+
// The markup a person meets — the header comments may explain the split, and do.
|
|
383
|
+
const html = FS.readFileSync('modules/hall-ui/public/thinking.html', 'utf8').replace(/<!--[\s\S]*?-->/g, '');
|
|
384
|
+
assert.doesNotMatch(html, /the whole project/, 'the words are gone from the markup too');
|
|
385
|
+
assert.doesNotMatch(html, /goal-map/, 'and this page never names the project map');
|
|
386
|
+
assert.doesNotMatch(p.el('legend').innerHTML, /unexplored/, 'the legend does not teach explore, which this sky does not have');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
await test('nothing takes /thinking into the whole project: setLens, the panel and the switcher all stay in your sky', async () => {
|
|
390
|
+
const p = await boot({ mine: mine(FIVE()), sky: sky([well()]), versions: versions() });
|
|
391
|
+
p.SKY().setLens('all', true);
|
|
392
|
+
assert.equal(p.S().lens, 'mine', 'a lens this page does not have is never entered');
|
|
393
|
+
p.click('lensSimple'); assert.equal(p.S().lens, 'simple');
|
|
394
|
+
p.click('lensMine'); assert.equal(p.S().lens, 'mine');
|
|
395
|
+
assert.equal(p.navigated, null, 'no lens leaves for another page');
|
|
396
|
+
p.SKY().pick({ kind: 'goal', ref: p.S().goals[0] }, false);
|
|
397
|
+
p.click('descendBtn');
|
|
398
|
+
assert.equal(p.S().lens, 'mine', 'descending into a well stays in your thinking');
|
|
399
|
+
p.click('lensSimple'); p.SKY().pick({ kind: 'goal', ref: p.S().goals[0] }, false); p.click('descendBtn');
|
|
400
|
+
assert.equal(p.S().lens, 'mine', 'from "simple", descending steps up to your thinking — the one lens here that zooms');
|
|
401
|
+
const src = FS.readFileSync('modules/hall-ui/public/sky-interaction.js', 'utf8');
|
|
402
|
+
assert.doesNotMatch(src, /location\.assign|'\/goal-map'|'\/thinking'/, 'the switcher has no door to the other page');
|
|
375
403
|
});
|
|
376
404
|
|
|
377
405
|
// ═══ the words are never markup ══════════════════════════════════════════════
|
|
@@ -392,25 +420,34 @@ await test('a title never becomes markup — in the drawer, the panel and the ho
|
|
|
392
420
|
|
|
393
421
|
// ═══ the tour, the lenses and the keys ═══════════════════════════════════════
|
|
394
422
|
|
|
395
|
-
await test('the tour is the template\'s thirteen steps — ? opens it, Escape closes it', async () => {
|
|
423
|
+
await test('the tour is your thinking\'s nine of the template\'s thirteen steps, in order and never in the whole project — ? opens it, Escape closes it', async () => {
|
|
396
424
|
const p = await boot({ mine: mine(FIVE()), sky: sky([well()]), versions: versions() });
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
same(STEPS.map((s) => s.h), [
|
|
425
|
+
// The template's thirteen are all still in the list; the four the project map owns are /goal-map's (task 1004328).
|
|
426
|
+
const template = [
|
|
400
427
|
'This is your sky.', 'A goal is a gravity well.', 'A ring is a criterion.', 'A burning thought is a star.', 'Some thoughts became the structure.',
|
|
401
428
|
'Unclaimed thoughts drift.', 'An alive well glows.', 'Routes run between goals that wait on each other.', 'A settled system is a goal that finished.',
|
|
402
429
|
'The sky has a clock.', 'By default, you explore.', 'Release a thought here.', 'When it is too much, go simple.',
|
|
430
|
+
];
|
|
431
|
+
same(p.SKY().ALL_STEPS.map((s) => s.h).filter((h) => template.includes(h)), template, 'every template step survives, in its order');
|
|
432
|
+
const STEPS = p.SKY().STEPS;
|
|
433
|
+
assert.equal(STEPS.length, 9);
|
|
434
|
+
same(STEPS.map((s) => s.h), [
|
|
435
|
+
'This is your sky.', 'A goal is a gravity well.', 'A ring is a criterion.', 'A burning thought is a star.', 'Some thoughts became the structure.',
|
|
436
|
+
'Unclaimed thoughts drift.', 'The sky has a clock.', 'Release a thought here.', 'When it is too much, go simple.',
|
|
403
437
|
]);
|
|
404
438
|
assert.equal(p.el('tour').hidden, true);
|
|
405
439
|
p.key('?');
|
|
406
440
|
assert.equal(p.el('tour').hidden, false);
|
|
407
|
-
assert.equal(p.el('tourN').textContent, 'how to read the sky · 1 of
|
|
408
|
-
|
|
409
|
-
|
|
441
|
+
assert.equal(p.el('tourN').textContent, 'how to read the sky · 1 of 9');
|
|
442
|
+
const lenses = new Set([p.S().lens]);
|
|
443
|
+
for (let n = 0; n < 8; n++) { p.click('tourNext'); lenses.add(p.S().lens); }
|
|
444
|
+
assert.equal(lenses.has('all'), false, 'no step of this tour enters the whole project');
|
|
445
|
+
assert.equal(p.el('tourN').textContent, 'how to read the sky · 9 of 9');
|
|
410
446
|
assert.equal(p.el('tourNext').textContent, 'start reading');
|
|
411
447
|
p.key('Escape');
|
|
412
448
|
assert.equal(p.el('tour').hidden, true);
|
|
413
449
|
assert.equal(p.S().lens, 'mine', 'the tour hands the sky back to your thinking');
|
|
450
|
+
assert.equal(p.navigated, null);
|
|
414
451
|
});
|
|
415
452
|
|
|
416
453
|
await test('the tour opens by itself on a first visit, and only then', async () => {
|
package/tests/helpers.mjs
CHANGED
|
@@ -904,7 +904,8 @@ export function bootSkyPage({
|
|
|
904
904
|
};
|
|
905
905
|
|
|
906
906
|
// ── the window ───────────────────────────────────────────────────────────
|
|
907
|
-
|
|
907
|
+
// Each page keeps its own tour-seen key (task 1004328); tourSeen marks both.
|
|
908
|
+
const store = { ...(tourSeen ? { 'sky-tour-seen': '4', 'sky-map-tour-seen': '4' } : {}), ...storage };
|
|
908
909
|
let raf = null;
|
|
909
910
|
const windowObj = {
|
|
910
911
|
__BRANDING__: {}, __MODULES__: { enabled: {} },
|