@bongos/core 1.19.399 → 1.19.401
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 +60 -40
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0199-restart-an-unreachable-active-project.md +49 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +74 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +233 -232
- package/docs/copy-registry.json +293 -284
- package/docs/design/projects-hub-direction-v2.md +38 -0
- package/docs/recipes/ui-look-before-you-ship.md +1 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/provisioning/migrations/provisioning_011_restart.sql +28 -0
- package/modules/provisioning/provisioning.js +42 -4
- package/modules/provisioning/routes/provisioning.js +59 -0
- package/modules/ui-design/kit/lib.js +6 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +88 -35
- package/public-landing/index.html +13 -15
- package/public-landing/index.probes.json +8 -5
- package/public-landing/projects.html +111 -16
- package/public-landing/projects.probes.json +17 -10
- package/public-landing/projects.states.json +1 -1
- package/scripts/gds/provision.js +104 -0
- package/src/module-api.js +5 -1
- package/tests/landing_page.mjs +8 -4
- package/tests/projects_hub.mjs +28 -1
- package/tests/projects_hub_pre_uat.mjs +40 -0
- package/tests/provision_restart.mjs +242 -0
- package/tests/provisioning_restart.mjs +285 -0
- package/tests/provisioning_settings_apply.mjs +8 -3
- package/tests/provisioning_teardown_intent.mjs +9 -5
- package/tests/ui_design_kit.mjs +5 -4
|
@@ -106,3 +106,41 @@ The criterion `apex-hub-chrome-and-card` moves to the builds: task 1003300 (the
|
|
|
106
106
|
- **Task 1003300** builds v2's bar, menus (Sky / Table / Your projects / New project) and search, with Only mine beside the search and the toolbar retired, into `public-landing/projects.html` + `cosmos.css` (the hub's sheet moves onto the world's derived tier the way `world.css` did, keeping `--line` and the status vocabulary), retargets the tests in §6, regenerates the copy registry.
|
|
107
107
|
- **Task 1003301** builds the picked card and its ladder, adds the enrichment fields (§6) inside the same `Promise.allSettled` with the shared null object, gates the hall entry on the new hall-address field and membership, and keeps the orbs and the hover card untouched.
|
|
108
108
|
- The unpicked versions stay on disk as built and verified; what they leave behind is listed per version in §3.
|
|
109
|
+
|
|
110
|
+
## 9. The owner's review of the shipped chrome, and the table in the world (task 1003332)
|
|
111
|
+
|
|
112
|
+
**Date:** 2026-08-28 · three corrections from the owner's screenshot review of `/projects` (the Projects band open, the table view). Each overrides §7 where they conflict. Rendered through the kit before and after (`node modules/ui-design/kit/render.js --page /projects`, both modes, 1440 / 390 / 320; the landing the same) — ALL CLEAN throughout; the probes 26/26 on the hub and 11/11 on the landing.
|
|
113
|
+
|
|
114
|
+
### 9.1 About and Docs leave the Projects band: the footer row
|
|
115
|
+
|
|
116
|
+
They are site pages, not project views. Two placements were rendered as a before/after on both pages (dark and light, 1440 and 390): **(A)** beside the identity slot as two lowercase text triggers (`about docs projects`), folding away under 900; **(B)** the footer row the landing already had (About · Docs · Privacy · Terms · Contact · Builders' hall). **Pick: B, the footer, on both pages.** One home at every width (A needs the footer anyway under 900 — two homes for two links); the bar stays the Operate bar given 3 asked for ("this way we dont clutter the bar"); About and Docs are the same kind of thing as Privacy / Terms / Contact. Consequences: the `bandMore` rung and its rules left both sheets; the hub's footer row wraps (`flex-wrap`) and `--foot-h` was re-measured with six links in it (136px under 420, 154px under 340, so the sky still fits the viewport); the About anchor stays the landing's lifecycle band (`/#about` from the hub, `#about` on the landing) until goal 1000063's page takes it. Probes retargeted: the band's last item is New project, the signed-out band is three cells, the footer carries both hrefs on both pages.
|
|
117
|
+
|
|
118
|
+
### 9.2 The bands overlay; the page does not reflow
|
|
119
|
+
|
|
120
|
+
`.menuBand` is `position:absolute; left:0; right:0; top:var(--top-h); z-index:49` against the document in both sheets. Everything else the disclosure did stays: full width on `bg-card` closed by one hairline, `aria-expanded` + `aria-controls` and no menu role, Escape / arrows / Enter / Tab-out / click-outside, one open at a time, the 220ms rise + fade on the world's ease-out, dead under the kill switch. The stack: band 49 < bar 50 < the folded search row 51 < the card scrim 60. The kit gained a `top[[sel,px]]` expectation for exactly this proof; the probes assert `#hubMain` at 64px (60 at 390) and the landing's `#main` at 72px both before and after a band opens. DESIGN.md's Navigation paragraph now says overlay.
|
|
121
|
+
|
|
122
|
+
### 9.3 Found on the way: the Table deep link landed on the sky
|
|
123
|
+
|
|
124
|
+
`setView()` rewrites the URL to `?view=map` at boot, and the shipped boot read `sub=table` *after* it — so the landing's Table item (and any shared `/projects?view=map&sub=table`) opened the sky. The boot query is now read once, before `setView`; a probe opens the deep link and expects the table on stage with Table current; `tests/projects_hub.mjs` pins the order.
|
|
125
|
+
|
|
126
|
+
### 9.4 The table in the world: one grammar, three mastheads
|
|
127
|
+
|
|
128
|
+
**Shared by every treatment (built):** the panel is gone — no `--card` ground, no 20px radius, no `--shadow-card`. The rows are DESIGN.md's ledger rows: 1px `rule-soft` hairlines, 17px of vertical air, the header a `rule` hairline under mono labels in `--label-face`. Builders is set in the figure voice (Manrope 300, 22px, tabular); the project name at 17px/500; the relationship pill is a mono label on a hairline — it states a fact and is not pressable, so the accent left it (the One Warm Thing Rule, §5); the row's door is `open →`, lowercase `accent-deep` text with an underline (the footer-link grammar, 24px tall) in place of the ghost pill; a row's hover is its hairline taking ink, the band cells' grammar, not an accent tint; the status chip keeps its fixed vocabulary; the view title is the display voice (`every project, as rows`). The objects are the landing's alpha **cutouts in both modes** — the view wrap is a stacking context, so a black plate under `lighten` could not reach the page ground the way the hero's do, and a cutout needs no blend — with the world's one contact shadow in light and none in dark. The plate tokens (`--plate-*-cut`) joined cosmos's derived tier by root-absolute path; the mode mapping (`--mast-shadow`, `--mast-scrim`) is in both dark blocks.
|
|
129
|
+
|
|
130
|
+
Three mastheads, rendered dark and light at 1440 / 390 / 320 (ALL CLEAN) and measured:
|
|
131
|
+
|
|
132
|
+
| # | Masthead | Height 1440 / 390 | Honest risk |
|
|
133
|
+
|---|---|---|---|
|
|
134
|
+
| **T1 the ledger** | the title at the left, the set's small bead (the landing's `.o3` crop) at the right | 218 / 121 px | the least new information — a chrome object beside a word; also the quietest and the cheapest |
|
|
135
|
+
| **T2 the field** | the sweeping form (the landing's `.form` crop) crossing the masthead behind the title, its left edge masked, a black scrim under the title in dark | 300 / 150 px (+5px over the viewport at 1440) | the crop is cut flat by the wrap's right edge and the masthead's bottom: on black the bottom edge shows, on paper both read as a torn photograph — DESIGN.md's own warning about a cropped form; a pick needs a bottom mask and a right feather, and the title wraps to two lines at 14ch |
|
|
136
|
+
| **T3 the specimen** | the bongo pair (the hero object, whole) at the left, the title beside it, two leader-line callouts with the rows' own numbers (projects, builders) — the landing's hero grammar | 294 / 283 px (+360px of scroll at 390) | the most height on a phone; the figures are live code, and the builders figure summed the rows before the platform's own count had landed (13, not 26) — a pick wires it to refresh when `#selfBuilders` lands and pins it in `tests/projects_hub.mjs` |
|
|
137
|
+
|
|
138
|
+
**Recommendation: T1.** It carries the world's material into the view at the smallest cost, holds at 390 and 320 without an edge problem in either mode, and is the treatment the mine view and the wizard can repeat (a title and one object). The owner picked from the rendered screens, not from this table.
|
|
139
|
+
|
|
140
|
+
**The owner's pick (2026-08-28): T1, the ledger — with the title cut to two words.** "t1, but I dont want the ', as rows'": the view title is `every project`. The two unpicked mastheads, the `?tbl=` switch, the three render-round states and T3's figure writes left with the pick; `tests/projects_hub.mjs` pins the masthead, the ledger rules, the row's door and the absence of the switch.
|
|
141
|
+
|
|
142
|
+
### 9.5 What carries forward
|
|
143
|
+
|
|
144
|
+
- Task 1003301 (the poster card): the ledger grammar above is the card's facts strip, too.
|
|
145
|
+
- Task 1003319 (the planets): the masthead object rides the same crop maths the orbs will take.
|
|
146
|
+
- The mine view and the wizard are the next views to take a masthead in the picked treatment.
|
|
@@ -69,7 +69,7 @@ Then a state's own `expect` (a band open, a dialog present, a field's value, a c
|
|
|
69
69
|
}
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
Actions: `click`, `focus`, `press`, `fill`, `wait`, `clickAt x y`, `clickIfVisible`, `dispatchClick`, `check`, `back`, `forward`. Expectations: `expanded`, `dialog`, `visible[]`, `hidden[]`, `open`, `closed`, `activeId`, `activeText`, `expandedIds[]`, `param{}`, `attr[[sel,name,value]]`, `text[[sel,value]]`, `href[[sel,value]]`, `count[[sel,n]]`, `value[[sel,v]]`, `hasClass[[sel,class,bool]]`, `checked`, `noOverflow
|
|
72
|
+
Actions: `click`, `focus`, `press`, `fill`, `wait`, `clickAt x y`, `clickIfVisible`, `dispatchClick`, `check`, `back`, `forward`. Expectations: `expanded`, `dialog`, `visible[]`, `hidden[]`, `open`, `closed`, `activeId`, `activeText`, `expandedIds[]`, `param{}`, `attr[[sel,name,value]]`, `text[[sel,value]]`, `href[[sel,value]]`, `count[[sel,n]]`, `value[[sel,v]]`, `hasClass[[sel,class,bool]]`, `checked`, `noOverflow`, `top[[sel,px]]` (an element's viewport top — assert it closed, open a band, assert it again: the proof an overlay did not push the page). The kit validates the file before it opens a browser and names every problem.
|
|
73
73
|
|
|
74
74
|
The worked examples: `public-landing/projects.states.json` (the hub's thirteen), `public-landing/index.states.json` (the landing's four), `modules/hall-ui/public/ranks.states.json` (a second surface under its own prefix — it renders **red**, deliberately: the hall predates the 24px floor and Ranks has no `<h1>`, and the states file declares the shell's un-fixtured feeds rather than hiding the findings; idea 1000863 carries them).
|
|
75
75
|
|
|
@@ -620,6 +620,8 @@ function createClient(opts = {}) {
|
|
|
620
620
|
postProvisioningInstancesIdForceTeardown: (args) => request("POST", "/provisioning/instances/{id}/force-teardown", { hasBody: true }, args),
|
|
621
621
|
// POST /provisioning/instances/{id}/github-app — rank: any-builder — POST /provisioning/instances/:id/github-app
|
|
622
622
|
postProvisioningInstancesIdGithubApp: (args) => request("POST", "/provisioning/instances/{id}/github-app", { hasBody: true }, args),
|
|
623
|
+
// POST /provisioning/instances/{id}/restart — rank: any-builder — POST /provisioning/instances/:id/restart
|
|
624
|
+
postProvisioningInstancesIdRestart: (args) => request("POST", "/provisioning/instances/{id}/restart", { hasBody: true }, args),
|
|
623
625
|
// GET /provisioning/instances/{id}/settings — rank: any-builder — GET /provisioning/instances/:id/settings
|
|
624
626
|
getProvisioningInstancesIdSettings: (args) => request("GET", "/provisioning/instances/{id}/settings", { hasBody: false }, args),
|
|
625
627
|
// PATCH /provisioning/instances/{id}/settings — rank: any-builder — PATCH /provisioning/instances/:id/settings
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-- provisioning_011_restart.sql — the restart intent action (task 1003238,
|
|
2
|
+
-- goal 1000058: an honest owner action for an ACTIVE instance whose probe is
|
|
3
|
+
-- down).
|
|
4
|
+
--
|
|
5
|
+
-- WHY. The liveness overlay (task 1002696) can say an instance is unreachable
|
|
6
|
+
-- while its lifecycle status is still 'active' — liveness is an overlay, never a
|
|
7
|
+
-- seventh state. Until now the owner had no action for that: PROVISION_ENQUEUE_FROM
|
|
8
|
+
-- deliberately excludes 'active' (task 1002691 — a blind re-enqueue left a stale
|
|
9
|
+
-- open provision intent that then blocked teardown), so a Restart button wired to
|
|
10
|
+
-- the provision path would have refused on every press. 'settings-apply' does
|
|
11
|
+
-- restart an active instance, but only as the tail of a change that must differ
|
|
12
|
+
-- from the stored row — the one case where restarting is useless.
|
|
13
|
+
--
|
|
14
|
+
-- So: its own action, whose whole job is the restart + the patient healthz verify
|
|
15
|
+
-- that settings-apply already performs (ADR 0190 §3), minus the env patch. The
|
|
16
|
+
-- web tier still only ENQUEUES; the control-plane runner remains the sole
|
|
17
|
+
-- token-holder (ADR 0111 §2).
|
|
18
|
+
--
|
|
19
|
+
-- Widening a CHECK enum is additive (permits MORE values; no row changes) — the
|
|
20
|
+
-- provisioning_003/005/010 precedent. Applied only when the provisioning module
|
|
21
|
+
-- is enabled. Safe to re-run. Keep INTENT_ACTIONS in provisioning.js in lockstep.
|
|
22
|
+
|
|
23
|
+
ALTER TABLE provisioning_intents DROP CONSTRAINT IF EXISTS provisioning_intents_action_check;
|
|
24
|
+
ALTER TABLE provisioning_intents ADD CONSTRAINT provisioning_intents_action_check
|
|
25
|
+
CHECK (action IN ('provision', 'teardown', 'oauth-manifest', 'domain-attach', 'settings-apply', 'restart'));
|
|
26
|
+
|
|
27
|
+
INSERT INTO schema_migrations (version) VALUES ('provisioning_011_restart')
|
|
28
|
+
ON CONFLICT (version) DO NOTHING;
|
|
@@ -46,7 +46,11 @@ const PROJECT_TYPES = ['game', 'research', 'business', 'non-profit', 'not-sure']
|
|
|
46
46
|
// per-project setting to an already-ACTIVE instance — patch its web.env settings
|
|
47
47
|
// lines in place + restart, since the branding pack is memoized for the process
|
|
48
48
|
// lifetime (task 1003139 built only the standup leg).
|
|
49
|
-
|
|
49
|
+
// 'restart' (task 1003238, migration provisioning_011): restart an already-ACTIVE
|
|
50
|
+
// instance whose liveness probe says it is down — the settings-apply leg minus the
|
|
51
|
+
// env patch. Its own action because no existing one fits: provision refuses
|
|
52
|
+
// 'active' and settings-apply requires a value that actually changed.
|
|
53
|
+
const INTENT_ACTIONS = ['provision', 'teardown', 'oauth-manifest', 'domain-attach', 'settings-apply', 'restart'];
|
|
50
54
|
// Every state a teardown can be asked for — i.e. everything except the one
|
|
51
55
|
// state where there is provably nothing left to release. DERIVED from
|
|
52
56
|
// INSTANCE_STATES on purpose (task 1003042, walk finding N5): the old hand-kept
|
|
@@ -84,6 +88,19 @@ const SETTINGS_APPLY_FROM = new Set(['active']);
|
|
|
84
88
|
// to it (the placeManifestCreds refusal) — so the route refuses the push HONESTLY
|
|
85
89
|
// rather than queue one that never lands.
|
|
86
90
|
const SETTINGS_APPLY_SHAPES = new Set(['co-tenant', 'standalone']);
|
|
91
|
+
// The ONE state a restart is meaningful from (task 1003238). Liveness is an
|
|
92
|
+
// OVERLAY, not a seventh state — an instance the heartbeat cannot reach is still
|
|
93
|
+
// 'active', and 'active' is exactly where a restart applies. Every earlier state
|
|
94
|
+
// has nothing running to restart and is already served by the provision path
|
|
95
|
+
// (PROVISION_ENQUEUE_FROM covers 'requested'/'provisioning'/'error'/'torn_down');
|
|
96
|
+
// 'tearing_down' is mid-reclaim. Kept as its own Set rather than reusing
|
|
97
|
+
// SETTINGS_APPLY_FROM: each action declaring its own gate is this module's
|
|
98
|
+
// convention, and the two answer different questions.
|
|
99
|
+
const RESTART_FROM = new Set(['active']);
|
|
100
|
+
// Same reachability rule as the settings push, for the same reason: a 'dedicated'
|
|
101
|
+
// droplet is stood up by cloud-init and the runner has no post-hoc exec path to
|
|
102
|
+
// it, so the route refuses HONESTLY rather than queue a restart that never lands.
|
|
103
|
+
const RESTART_SHAPES = new Set(['co-tenant', 'standalone']);
|
|
87
104
|
|
|
88
105
|
// Per-project settings (task 1003040, goal 1000058): the known keys + their
|
|
89
106
|
// vocabularies, enforced at the API layer — the jsonb `settings` column
|
|
@@ -632,7 +649,10 @@ async function enqueueIntent(db, instanceId, action, requestedBy) {
|
|
|
632
649
|
// (the state CHECK has no 'superseded'; last_error says why) so the runner never
|
|
633
650
|
// stands it up. A PENDING settings push is superseded the same way (task 1003140):
|
|
634
651
|
// a push into a project about to be torn down is dead work, and letting it hold the
|
|
635
|
-
// delete hostage would repeat 1002691. A
|
|
652
|
+
// delete hostage would repeat 1002691. A pending 'restart' joins them for the same
|
|
653
|
+
// reason (task 1003238) — and more sharply, since the owner reaches for Restart
|
|
654
|
+
// precisely when a project looks broken, which is also when they give up and delete
|
|
655
|
+
// it. A RUNNING intent is mid-flight and never yanked —
|
|
636
656
|
// the caller gets { conflict: 'open_intent' } with the blocking intent so the route
|
|
637
657
|
// can answer truthfully. Each statement is atomic and the index stays the arbiter:
|
|
638
658
|
// any interleaving with the runner or a concurrent enqueue lands on one of these
|
|
@@ -642,7 +662,7 @@ async function enqueueTeardown(db, instanceId, requestedBy) {
|
|
|
642
662
|
`UPDATE provisioning_intents
|
|
643
663
|
SET state = 'error', last_error = 'superseded: teardown requested',
|
|
644
664
|
updated_at = now(), resolved_at = now()
|
|
645
|
-
WHERE instance_id = $1 AND action IN ('provision', 'settings-apply') AND state = 'pending'
|
|
665
|
+
WHERE instance_id = $1 AND action IN ('provision', 'settings-apply', 'restart') AND state = 'pending'
|
|
646
666
|
RETURNING id`,
|
|
647
667
|
[instanceId]
|
|
648
668
|
);
|
|
@@ -668,6 +688,23 @@ async function enqueueSettingsApply(db, instanceId, requestedBy) {
|
|
|
668
688
|
return { intent, created: false, conflict: 'open_intent' };
|
|
669
689
|
}
|
|
670
690
|
|
|
691
|
+
// Queue a restart for an ACTIVE instance (task 1003238). Idempotent against its
|
|
692
|
+
// own kind in EITHER open state, which is where it differs from the settings push:
|
|
693
|
+
// that one must refuse to ride a RUNNING sibling because the runner may have read
|
|
694
|
+
// the settings row before this write, so the in-flight run would not carry the
|
|
695
|
+
// change. A restart carries no payload — a restart already running IS the thing
|
|
696
|
+
// the owner asked for, so reporting "already under way" is the truth, not a
|
|
697
|
+
// swallowed request. Anything else holding the single open slot is reported as
|
|
698
|
+
// { conflict: 'open_intent' } so the route can name it. The state gate and the
|
|
699
|
+
// shape gate are the ROUTE's job (RESTART_FROM / RESTART_SHAPES); this only
|
|
700
|
+
// arbitrates the open slot, like enqueueTeardown.
|
|
701
|
+
async function enqueueRestart(db, instanceId, requestedBy) {
|
|
702
|
+
const { intent, created } = await enqueueIntent(db, instanceId, 'restart', requestedBy);
|
|
703
|
+
if (created) return { intent, created: true };
|
|
704
|
+
if (intent.action === 'restart') return { intent, created: false };
|
|
705
|
+
return { intent, created: false, conflict: 'open_intent' };
|
|
706
|
+
}
|
|
707
|
+
|
|
671
708
|
// The instance's currently-open (pending|running) intent, if any.
|
|
672
709
|
async function getOpenIntent(db, instanceId) {
|
|
673
710
|
const { rows } = await db.query(
|
|
@@ -860,6 +897,7 @@ module.exports = {
|
|
|
860
897
|
INSTANCE_STATES, HOSTING_SHAPES, ONBOARD_MODES, PROJECT_TYPES, INTENT_ACTIONS, TEARDOWN_FROM,
|
|
861
898
|
settingsEnvLines, settingsEnvVars,
|
|
862
899
|
PROVISION_ENQUEUE_FROM, DOMAIN_ATTACH_FROM, SETTINGS_APPLY_FROM, SETTINGS_APPLY_SHAPES,
|
|
900
|
+
RESTART_FROM, RESTART_SHAPES,
|
|
863
901
|
TIER_MONTHLY_USD, CERT_ELIGIBLE_STATES,
|
|
864
902
|
SETTINGS_VOCAB, DEFAULT_SETTINGS,
|
|
865
903
|
// pure
|
|
@@ -873,7 +911,7 @@ module.exports = {
|
|
|
873
911
|
// data access — verification overlay (task 1002696)
|
|
874
912
|
listActiveInstancesForLiveness, recordLivenessProbe, recordDnsNote, clearErrorNote,
|
|
875
913
|
createInstance, setInstanceStatus, updateInstanceDomain, updateInstanceSettings,
|
|
876
|
-
enqueueIntent, enqueueTeardown, enqueueSettingsApply, getOpenIntent, claimNextIntent, resolveIntent,
|
|
914
|
+
enqueueIntent, enqueueTeardown, enqueueSettingsApply, enqueueRestart, getOpenIntent, claimNextIntent, resolveIntent,
|
|
877
915
|
recordEvent, instanceStateForSlug, costLedger,
|
|
878
916
|
// data access — GitHub App Manifest attempts (task 2080)
|
|
879
917
|
createOAuthManifest, getOAuthManifestByState, attachManifestCode,
|
|
@@ -795,6 +795,65 @@ module.exports = function provisioningRoutes() {
|
|
|
795
795
|
}
|
|
796
796
|
});
|
|
797
797
|
|
|
798
|
+
// POST /provisioning/instances/:id/restart — restart an ACTIVE project the
|
|
799
|
+
// heartbeat cannot reach (task 1003238). rank: any authenticated builder (own
|
|
800
|
+
// resource; archon may restart any — the override is audited via the actor).
|
|
801
|
+
//
|
|
802
|
+
// WHY it is its own route. The liveness overlay (task 1002696) can say a project
|
|
803
|
+
// is unreachable while its lifecycle status is still 'active' — and until now
|
|
804
|
+
// that reading came with no action the owner could take. Re-provision is not it:
|
|
805
|
+
// PROVISION_ENQUEUE_FROM deliberately excludes 'active' (task 1002691), so a
|
|
806
|
+
// Restart wired to that path would have refused on every press — the finding this
|
|
807
|
+
// route closes. The web tier only ENQUEUES; the control-plane runner performs the
|
|
808
|
+
// restart and the patient healthz verify (ADR 0111 §2).
|
|
809
|
+
//
|
|
810
|
+
// Every refusal names the action that IS available in that state rather than
|
|
811
|
+
// hiding behind a code, and no branch promises a duration — a restart's downtime
|
|
812
|
+
// is exactly what this route cannot know (the F16 rule, ADR 0190 §7).
|
|
813
|
+
router.post('/provisioning/instances/:id/restart', auth.requireBuilder, async (req, res) => {
|
|
814
|
+
const id = parseId(req, res); // (req, res) — parseId sends 400 bad_id + returns null on failure
|
|
815
|
+
if (!id) return;
|
|
816
|
+
try {
|
|
817
|
+
const inst = await provisioning.getInstanceById(pool, id);
|
|
818
|
+
if (instanceAccessDenied(inst, req.builder)) return res.fail('instance_not_found', 404);
|
|
819
|
+
// The archon override carries WHICH archon (the force-teardown convention).
|
|
820
|
+
const actor = String(inst.owner_builder_id) === String(req.builder.id) ? 'api:self' : `api:archon:${req.builder.id}`;
|
|
821
|
+
if (!provisioning.RESTART_FROM.has(inst.status)) {
|
|
822
|
+
const offline = inst.status === 'torn_down' || inst.status === 'tearing_down';
|
|
823
|
+
return res.fail('not_restartable', { status: 409, details: { status: inst.status }, message: offline
|
|
824
|
+
? 'This project is offline, so there is nothing running to restart — stand it up again instead.'
|
|
825
|
+
: inst.status === 'error'
|
|
826
|
+
? 'This project’s standup stopped partway, so there is nothing running to restart — stand it up again to run the standup afresh.'
|
|
827
|
+
: 'This project hasn’t finished standing up yet, so there is nothing to restart.' });
|
|
828
|
+
}
|
|
829
|
+
// A dedicated droplet is stood up by cloud-init and the runner has no post-hoc
|
|
830
|
+
// exec path to it — refuse honestly rather than queue a restart that never
|
|
831
|
+
// lands (the F16 lie), and name the command whoever runs that machine needs.
|
|
832
|
+
if (!provisioning.RESTART_SHAPES.has(inst.hosting_shape)) {
|
|
833
|
+
return res.fail('restart_unreachable', { status: 409, details: { hosting_shape: inst.hosting_shape },
|
|
834
|
+
message: `This project runs on a machine the platform doesn’t manage, so it can’t be restarted from here — whoever runs that machine has to run 'systemctl restart ${inst.slug}' there.` });
|
|
835
|
+
}
|
|
836
|
+
const r = await provisioning.enqueueRestart(pool, id, actor);
|
|
837
|
+
if (r.conflict) {
|
|
838
|
+
return res.fail('intent_conflict', { status: 409,
|
|
839
|
+
message: `A '${r.intent.action}' run is currently ${r.intent.state} for this project — the restart could not be queued; try again once that settles.`,
|
|
840
|
+
details: { action: r.intent.action, state: r.intent.state } });
|
|
841
|
+
}
|
|
842
|
+
if (r.created) {
|
|
843
|
+
await provisioning.recordEvent(pool, {
|
|
844
|
+
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'restart',
|
|
845
|
+
detail: 'restart requested by owner', actor,
|
|
846
|
+
}).catch(() => {});
|
|
847
|
+
}
|
|
848
|
+
res.json({ ok: true, queued: r.created, action: 'restart', status: inst.status, message: r.created
|
|
849
|
+
? 'Restart queued. Your project will stop and start again — this page says whether it answered afterwards.'
|
|
850
|
+
: 'A restart is already under way for this project.' });
|
|
851
|
+
} catch (err) {
|
|
852
|
+
log.error('[provisioning] POST /provisioning/instances/:id/restart', err);
|
|
853
|
+
res.fail('restart_failed', { status: 500, message: 'internal error' });
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
|
|
798
857
|
// POST /provisioning/instances/:id/domain — give the caller's OWN instance its
|
|
799
858
|
// address (task 1002692, gap 5: no path existed to add a domain after creation).
|
|
800
859
|
// Validates + records the domain; for an already-ACTIVE instance it also enqueues
|
|
@@ -123,7 +123,9 @@ async function runActions(page, actions = []) {
|
|
|
123
123
|
// A state may EXPECT (render.js) and a probe must EXPECT (probe.js). Selectors are
|
|
124
124
|
// evaluated in the page; scalars compared here. `open`/`visible` = present and not
|
|
125
125
|
// hidden; `closed`/`hidden` = absent or hidden.
|
|
126
|
-
|
|
126
|
+
// `top` pins an element's viewport top in px (rounded) — the probe that proves an overlay
|
|
127
|
+
// did not push the page (task 1003332): assert it closed, open it, assert it again.
|
|
128
|
+
const EXPECT_KEYS = Object.freeze(['expanded', 'dialog', 'visible', 'hidden', 'open', 'closed', 'activeId', 'activeText', 'expandedIds', 'param', 'attr', 'text', 'href', 'count', 'checked', 'value', 'hasClass', 'noOverflow', 'top']);
|
|
127
129
|
function validateExpect(expect, at) {
|
|
128
130
|
if (expect === undefined) return [];
|
|
129
131
|
if (!expect || typeof expect !== 'object' || Array.isArray(expect)) return [`${at}: an object`];
|
|
@@ -134,7 +136,7 @@ function validateExpect(expect, at) {
|
|
|
134
136
|
for (const k of ['expanded', 'dialog', 'noOverflow']) if (expect[k] !== undefined && typeof expect[k] !== 'boolean') errors.push(`${at}.${k}: a boolean`);
|
|
135
137
|
if (expect.expandedIds !== undefined && !Array.isArray(expect.expandedIds)) errors.push(`${at}.expandedIds: an array of ids`);
|
|
136
138
|
if (expect.param !== undefined && (typeof expect.param !== 'object' || Array.isArray(expect.param))) errors.push(`${at}.param: an object of name → value (null = absent)`);
|
|
137
|
-
for (const k of ['attr', 'text', 'href', 'count', 'value', 'hasClass']) if (expect[k] !== undefined && !(Array.isArray(expect[k]) && expect[k].every(Array.isArray))) errors.push(`${at}.${k}: an array of [selector, ...]`);
|
|
139
|
+
for (const k of ['attr', 'text', 'href', 'count', 'value', 'hasClass', 'top']) if (expect[k] !== undefined && !(Array.isArray(expect[k]) && expect[k].every(Array.isArray))) errors.push(`${at}.${k}: an array of [selector, ...]`);
|
|
138
140
|
return errors;
|
|
139
141
|
}
|
|
140
142
|
// Runs IN THE PAGE: collects the facts the expectations compare against, plus the
|
|
@@ -183,6 +185,7 @@ function factsInPage(spec) {
|
|
|
183
185
|
if (e.checked) { const el = q(e.checked); out.checked = el ? !!el.checked : null; }
|
|
184
186
|
if (e.value) out.value = e.value.map(([s]) => { const el = q(s); return el ? el.value : null; });
|
|
185
187
|
if (e.hasClass) out.hasClass = e.hasClass.map(([s, c]) => { const el = q(s); return el ? el.classList.contains(c) : null; });
|
|
188
|
+
if (e.top) out.top = e.top.map(([s]) => { const el = q(s); return el ? Math.round(el.getBoundingClientRect().top) : null; });
|
|
186
189
|
return out;
|
|
187
190
|
}
|
|
188
191
|
// Pure: compare collected facts against an expectation. Returns problem strings.
|
|
@@ -208,6 +211,7 @@ function expectProblems(facts, expect = {}) {
|
|
|
208
211
|
if (expect.checked !== undefined && facts.checked !== true) p.push(`${expect.checked} is not checked`);
|
|
209
212
|
(expect.value || []).forEach(([s, v], i) => { if (facts.value[i] !== v) p.push(`${s} value "${facts.value[i]}", expected "${v}"`); });
|
|
210
213
|
(expect.hasClass || []).forEach(([s, c, want = true], i) => { if (facts.hasClass[i] !== want) p.push(`${s} ${want ? 'lacks' : 'still has'} class "${c}"`); });
|
|
214
|
+
(expect.top || []).forEach(([s, v], i) => { if (facts.top[i] !== v) p.push(`${s} top ${facts.top[i]}px, expected ${v}px`); });
|
|
211
215
|
if (expect.noOverflow && facts.scrollWidth > facts.clientWidth + 1) p.push(`horizontal overflow ${facts.scrollWidth} > ${facts.clientWidth}`);
|
|
212
216
|
return p;
|
|
213
217
|
}
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.401",
|
|
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.401",
|
|
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.401",
|
|
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",
|
|
@@ -130,6 +130,8 @@
|
|
|
130
130
|
--label-face:var(--font-body);
|
|
131
131
|
--label-size:11.5px;
|
|
132
132
|
--label-space:.1em;
|
|
133
|
+
/* the view masthead's object (task 1003332): dark drops the contact shadow */
|
|
134
|
+
--mast-shadow:none;
|
|
133
135
|
color-scheme:dark;
|
|
134
136
|
}
|
|
135
137
|
@media (prefers-color-scheme: dark){
|
|
@@ -165,6 +167,8 @@
|
|
|
165
167
|
--label-face:var(--font-body);
|
|
166
168
|
--label-size:11.5px;
|
|
167
169
|
--label-space:.1em;
|
|
170
|
+
/* the view masthead's object (task 1003332): dark drops the contact shadow */
|
|
171
|
+
--mast-shadow:none;
|
|
168
172
|
color-scheme:dark;
|
|
169
173
|
}
|
|
170
174
|
}
|
|
@@ -246,6 +250,15 @@
|
|
|
246
250
|
--radius:16px; --radius-sm:10px; --radius-pill:999px;
|
|
247
251
|
--top-h:64px;
|
|
248
252
|
--gutter:clamp(16px,4vw,34px);
|
|
253
|
+
/* the world's object for the hub's view masthead (task 1003332): the apex
|
|
254
|
+
shares the landing's plates because it is the same world (DESIGN.md
|
|
255
|
+
"Materials"); the hub takes the alpha CUTOUT in both modes — the view
|
|
256
|
+
wraps are stacking contexts, so a black plate under `lighten` could not
|
|
257
|
+
reach the page ground the way the hero's do, and a cutout needs no blend.
|
|
258
|
+
Root-absolute path: a url() in a custom property resolves against the
|
|
259
|
+
DOCUMENT. Light gives the object the world's one contact shadow. */
|
|
260
|
+
--plate-set-cut:url(/assets/world/r2-chrome-blobs-set-cut.webp);
|
|
261
|
+
--mast-shadow:drop-shadow(0 26px 30px color-mix(in srgb,var(--ink) 20%,transparent));
|
|
249
262
|
/* the footer's measured height per width — the sky is 100vh minus the bar and
|
|
250
263
|
this, so the map fits the viewport with no scrollbar (T2's rule); the
|
|
251
264
|
toolbar row that used to be in that sum is gone (task 1003300) */
|
|
@@ -255,8 +268,13 @@
|
|
|
255
268
|
@media (max-width:900px){
|
|
256
269
|
:root{--top-h:60px;--foot-h:108px;}
|
|
257
270
|
}
|
|
258
|
-
|
|
259
|
-
|
|
271
|
+
/* with About + Docs in the row (task 1003332) the six links wrap to a second
|
|
272
|
+
row under ~420px, and the sentence takes a third line under ~340px — measured */
|
|
273
|
+
@media (max-width:420px){
|
|
274
|
+
:root{--foot-h:136px;}
|
|
275
|
+
}
|
|
276
|
+
@media (max-width:340px){
|
|
277
|
+
:root{--foot-h:154px;}
|
|
260
278
|
}
|
|
261
279
|
|
|
262
280
|
*{box-sizing:border-box;margin:0;padding:0;}
|
|
@@ -413,7 +431,7 @@ a.orb{cursor:pointer;}
|
|
|
413
431
|
.foot a:hover{text-decoration:underline;}
|
|
414
432
|
/* the footer doors (task 1003251): privacy · terms · contact on one row under the
|
|
415
433
|
line, on every landing surface — the same link ink, the current page underlined */
|
|
416
|
-
.foot .footNav{display:flex;justify-content:center;gap:16px;margin-top:8px;}
|
|
434
|
+
.foot .footNav{display:flex;flex-wrap:wrap;justify-content:center;gap:4px 16px;margin-top:8px;}
|
|
417
435
|
.foot .footNav a[aria-current="page"]{text-decoration:underline;}
|
|
418
436
|
|
|
419
437
|
/* ── progress strips: one visual, per-flow vocabularies (direction §4).
|
|
@@ -611,13 +629,17 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
611
629
|
.switch:has(input:checked){color:var(--ink);}
|
|
612
630
|
|
|
613
631
|
/* ── THE BANDS: a menu is not a floating card. It unfolds under the bar's edge,
|
|
614
|
-
full width on the card ground, closed by one hairline,
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
.
|
|
632
|
+
full width on the card ground, closed by one hairline, as a LAYER OVER the
|
|
633
|
+
page: the page does not reflow (the owner's correction, task 1003332 — the
|
|
634
|
+
shipped bands pushed the page down). Absolute against the document, top at
|
|
635
|
+
the bar's height, so the sky, the table and the footer keep their place; the
|
|
636
|
+
bar (50) stays above it, the card scrim (60) above both. Cells in a
|
|
637
|
+
hairline-separated row, each a lowercase display title over one clause; the
|
|
638
|
+
whole cell is the target. The current view carries a 2px accent hairline on
|
|
639
|
+
its top edge — the rail-step-lit grammar from DESIGN.md, moved onto a menu.
|
|
640
|
+
One authored move, a 6px rise + fade on the world's exponential ease-out,
|
|
641
|
+
dead under the kill switch at the end of this file. ── */
|
|
642
|
+
.menuBand{position:absolute;left:0;right:0;top:var(--top-h);z-index:49;background:var(--bg-card);border-bottom:1px solid var(--rule);
|
|
621
643
|
animation:bandIn .22s cubic-bezier(.16,1,.3,1) both;}
|
|
622
644
|
@keyframes bandIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
|
623
645
|
.bandIn{padding:20px var(--gutter) 28px;}
|
|
@@ -642,9 +664,6 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
642
664
|
/* the deferred Settings slot: a place it would go, rendering nothing at all —
|
|
643
665
|
not even the hairline that declares a list item in this band */
|
|
644
666
|
.bandSlot{border:0;}
|
|
645
|
-
/* the row under the hairline: About and Docs, a rung smaller than the views */
|
|
646
|
-
.bandMore{margin-top:18px;}
|
|
647
|
-
.bandMore .bandT{font-size:17px;font-weight:400;min-height:24px;display:inline-flex;align-items:center;}
|
|
648
667
|
|
|
649
668
|
@media (max-width:900px){
|
|
650
669
|
.bar{gap:10px;padding:0 12px;}
|
|
@@ -766,35 +785,63 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
766
785
|
.hintbar kbd{font-family:var(--font-mono);background:color-mix(in srgb,var(--accent) 24%,transparent);
|
|
767
786
|
border-radius:5px;padding:1px 6px;font-size:11px;color:var(--chrome-accent);}
|
|
768
787
|
|
|
769
|
-
/* ──
|
|
770
|
-
|
|
771
|
-
.
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
788
|
+
/* ── THE TABLE IN THE WORLD (task 1003332; owner's review of the shipped chrome).
|
|
789
|
+
The table is a VIEW of the same world as the sky, not a panel beside it: a
|
|
790
|
+
masthead in the world's material, then LEDGER ROWS (DESIGN.md "Ledger rows")
|
|
791
|
+
— hairlines, no ground, no radius, no shadow; mono labels on the columns;
|
|
792
|
+
the figure voice on the numbers; the accent only on what is pressable (the
|
|
793
|
+
open link) or live (the status chip); the display voice on the view title.
|
|
794
|
+
The relationship pill states a fact and is not pressable, so it lost the
|
|
795
|
+
accent (the One Warm Thing Rule); a row's hover is the hairline taking ink,
|
|
796
|
+
the bands' grammar. The masthead is the owner's pick from three rendered
|
|
797
|
+
treatments (docs/design/projects-hub-direction-v2.md §9): "the ledger" —
|
|
798
|
+
the view title in the display voice at the left, one chrome object at the
|
|
799
|
+
right, nothing else. ── */
|
|
800
|
+
.tblMast{display:grid;grid-template-columns:1fr auto;position:relative;align-items:end;gap:26px;padding:clamp(26px,3.6vw,44px) 0 30px;}
|
|
801
|
+
.tblTitle{font-family:var(--font-display);font-weight:300;font-size:clamp(30px,3.6vw,48px);line-height:1;
|
|
802
|
+
letter-spacing:-.035em;text-transform:lowercase;color:var(--ink);text-shadow:var(--type-halo);margin:0;}
|
|
803
|
+
/* the object, on the world's crop maths (DESIGN.md "The crop maths"): one
|
|
804
|
+
rectangle of a plate, expressed against the object's own width so it scales
|
|
805
|
+
with the row — the set's small bead, r2-chrome-blobs-set u[.810,.955] v[.556,.840] */
|
|
806
|
+
.mastObj{display:block;--dia:clamp(96px,10vw,132px);width:var(--dia);aspect-ratio:.91476;background-image:var(--plate-set-cut);
|
|
807
|
+
background-repeat:no-repeat;background-size:calc(var(--dia) * 6.89655) auto;
|
|
808
|
+
background-position:calc(var(--dia) * -5.58621) calc(var(--dia) * -2.14014);filter:var(--mast-shadow);}
|
|
809
|
+
|
|
810
|
+
.tbl{width:100%;border-collapse:collapse;background:none;}
|
|
811
|
+
.tbl th{text-align:left;font-family:var(--label-face);font-size:var(--label-size);font-weight:400;
|
|
812
|
+
letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);padding:0 14px 12px;
|
|
813
|
+
border-bottom:1px solid var(--rule);white-space:nowrap;}
|
|
814
|
+
.tbl th:first-child,.tbl td:first-child{padding-left:0;}
|
|
815
|
+
.tbl th:last-child,.tbl td:last-child{padding-right:0;}
|
|
776
816
|
/* the sort control is a real button so the keyboard can reach it; it must look
|
|
777
817
|
like the header it replaced, so it inherits everything the th declares. */
|
|
778
818
|
.tbl th button{all:unset;cursor:pointer;}
|
|
779
819
|
.tbl th button:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
|
|
780
|
-
.tbl td{padding:
|
|
781
|
-
color:var(--ink-soft);vertical-align:middle;}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
/* the whole row opens the project card, so it must look like it does */
|
|
820
|
+
.tbl td{padding:17px 14px;border-bottom:1px solid var(--rule-soft);font-size:15px;font-weight:400;
|
|
821
|
+
color:var(--ink-soft);vertical-align:middle;transition:border-color .2s cubic-bezier(.2,.7,.3,1);}
|
|
822
|
+
/* the whole row opens the project card, so it must look like it does: the
|
|
823
|
+
hairline under it takes ink, as a band cell's does */
|
|
785
824
|
.tbl tbody tr{cursor:pointer;}
|
|
786
|
-
.tbl
|
|
787
|
-
.tbl .
|
|
788
|
-
|
|
789
|
-
.tbl .ptitle{font-family:var(--font-display);font-weight:
|
|
825
|
+
.tbl tbody tr:hover td{border-bottom-color:var(--ink-faint);}
|
|
826
|
+
.tbl .pname{display:flex;align-items:center;gap:12px;}
|
|
827
|
+
.tbl .pdot{width:10px;height:10px;border-radius:50%;background:var(--acc);flex:0 0 auto;}
|
|
828
|
+
.tbl .ptitle{font-family:var(--font-display);font-weight:500;font-size:17px;letter-spacing:-.015em;color:var(--ink);text-decoration:none;}
|
|
790
829
|
.tbl a.ptitle:hover{text-decoration:underline;}
|
|
791
|
-
.tbl .ptag{font-size:
|
|
792
|
-
.tbl .num{font-family:var(--font-
|
|
830
|
+
.tbl .ptag{font-size:13.5px;color:var(--ink-soft);font-weight:400;}
|
|
831
|
+
.tbl .num{font-family:var(--font-display);font-weight:300;font-size:22px;letter-spacing:-.02em;color:var(--ink);
|
|
832
|
+
font-variant-numeric:tabular-nums;}
|
|
793
833
|
.tbl .tags{display:flex;gap:6px;flex-wrap:wrap;}
|
|
794
834
|
.tbl .tags span{font-family:var(--font-mono);font-size:10.5px;padding:2px 8px;border-radius:999px;
|
|
795
|
-
border:1px solid var(--
|
|
796
|
-
.tbl .pill{display:inline-block;font-family:var(--
|
|
797
|
-
border-radius:999px;
|
|
835
|
+
border:1px solid var(--rule);color:var(--ink-faint);white-space:nowrap;}
|
|
836
|
+
.tbl .pill{display:inline-block;font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);
|
|
837
|
+
text-transform:uppercase;padding:3px 9px;border-radius:999px;border:1px solid var(--rule);color:var(--ink-faint);}
|
|
838
|
+
/* the row's door: the accent on the one pressable thing in the row, as text
|
|
839
|
+
(the footer-link grammar), lowercase like every control in the world */
|
|
840
|
+
.rowGo{display:inline-flex;align-items:center;min-height:24px;padding:3px 0;font-family:var(--font-body);font-size:15px;
|
|
841
|
+
text-transform:lowercase;color:var(--accent-deep);text-decoration:underline;text-underline-offset:4px;
|
|
842
|
+
text-decoration-color:color-mix(in srgb,var(--accent) 55%,transparent);white-space:nowrap;
|
|
843
|
+
transition:text-decoration-color .2s cubic-bezier(.2,.7,.3,1);}
|
|
844
|
+
.rowGo:hover{text-decoration-color:var(--accent-deep);}
|
|
798
845
|
.tbl td.right{text-align:right;}
|
|
799
846
|
/* Nothing in an action or status cell may wrap: "Manage →" breaking after the
|
|
800
847
|
space and the chip's recency dropping to a second line both read as a broken
|
|
@@ -807,8 +854,14 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
807
854
|
.tbl .colType{width:1%;white-space:nowrap;}
|
|
808
855
|
/* the declared-type tag (task 1002772) — its own vocabulary: not the relationship
|
|
809
856
|
.pill, not the liveness .chip. Two different facts never wear the same chrome. */
|
|
810
|
-
.typeTag{font-family:var(--font-mono);font-size:10.5px;font-weight:500;color:var(--ink-soft);border:1px solid var(--
|
|
857
|
+
.typeTag{font-family:var(--font-mono);font-size:10.5px;font-weight:500;color:var(--ink-soft);border:1px solid var(--rule);border-radius:999px;padding:2px 9px;white-space:nowrap;}
|
|
811
858
|
@media (max-width:780px){.tbl .colTags,.tbl .colType{display:none;}}
|
|
859
|
+
@media (max-width:900px){
|
|
860
|
+
.tblMast{gap:16px;padding:22px 0 20px;}
|
|
861
|
+
.tblTitle{font-size:clamp(28px,7.4vw,34px);}
|
|
862
|
+
.mastObj{--dia:clamp(72px,18vw,96px);}
|
|
863
|
+
.tbl th,.tbl td{padding-left:10px;padding-right:10px;}
|
|
864
|
+
}
|
|
812
865
|
|
|
813
866
|
/* ── the living sky (task 1002762): camera, spotlight, hover card ──
|
|
814
867
|
The camera is ONE transform on .skyCam; orbs carry only their own physics
|
|
@@ -191,10 +191,11 @@ main:focus{outline:none;box-shadow:none}
|
|
|
191
191
|
@media (prefers-color-scheme:dark){:root:not([data-mode="light"]) .sun{display:block}
|
|
192
192
|
:root:not([data-mode="light"]) .moon{display:none}}
|
|
193
193
|
|
|
194
|
-
/* the menu bands (.menuBand — .band is this page's content sections): not floating cards. Full width on the card ground, closed by one hairline,
|
|
195
|
-
the page
|
|
196
|
-
|
|
197
|
-
.
|
|
194
|
+
/* the menu bands (.menuBand — .band is this page's content sections): not floating cards. Full width on the card ground, closed by one hairline, a LAYER
|
|
195
|
+
over the page under the bar — the page does not reflow (owner, 2026-08-28; task 1003332). Cells in a
|
|
196
|
+
hairline-separated row, a lowercase display title over one clause; the whole cell is the target.
|
|
197
|
+
One authored move, dead under world.css's kill switch. */
|
|
198
|
+
.menuBand{position:absolute;left:0;right:0;top:var(--top-h);z-index:49;background:var(--bg-card);border-bottom:1px solid var(--rule);
|
|
198
199
|
animation:bandIn .22s cubic-bezier(.16,1,.3,1) both}
|
|
199
200
|
@keyframes bandIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
|
|
200
201
|
.bandIn{padding:20px 26px 28px}
|
|
@@ -207,8 +208,6 @@ main:focus{outline:none;box-shadow:none}
|
|
|
207
208
|
.bandCell:hover .bandT{color:var(--ink)}
|
|
208
209
|
.bandCell:hover{border-top-color:var(--ink-faint)}
|
|
209
210
|
.bandC{display:block;margin-top:5px;font-size:13.5px;line-height:1.45;color:var(--ink-soft)}
|
|
210
|
-
.bandMore{margin-top:18px}
|
|
211
|
-
.bandMore .bandT{font-size:17px;font-weight:400;min-height:24px;display:inline-flex;align-items:center}
|
|
212
211
|
.bandProfile{display:flex;align-items:flex-end;justify-content:space-between;gap:34px;flex-wrap:wrap}
|
|
213
212
|
.bandWho{display:flex;align-items:center;gap:10px;padding-top:15px;border-top:1px solid var(--rule-soft);
|
|
214
213
|
flex:0 1 auto;font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);
|
|
@@ -843,9 +842,10 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
843
842
|
</header>
|
|
844
843
|
|
|
845
844
|
<!-- the two bands: full-width, on the card ground, closed by one hairline; they unfold under the
|
|
846
|
-
bar's edge
|
|
847
|
-
foot of the page): Escape closes and returns focus
|
|
848
|
-
Tab out closes, a click outside closes, opening one
|
|
845
|
+
bar's edge as a layer over the page, which does not reflow (task 1003332). One disclosure
|
|
846
|
+
component, two instances (the script at the foot of the page): Escape closes and returns focus
|
|
847
|
+
to the trigger, the arrows walk the items, Tab out closes, a click outside closes, opening one
|
|
848
|
+
closes the other. -->
|
|
849
849
|
<div class="menuBand" id="menuProjects" data-menu="projects" hidden>
|
|
850
850
|
<div class="bandIn">
|
|
851
851
|
<ul class="bandRow">
|
|
@@ -854,12 +854,8 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
854
854
|
<li class="bandCell" id="mineCell" hidden><a class="bandT" href="/projects?view=mine">Your projects</a><span class="bandC">the ones you own or build on</span></li>
|
|
855
855
|
<li class="bandCell"><a class="bandT" href="/projects?view=new">New project</a><span class="bandC">five questions and a home</span></li>
|
|
856
856
|
</ul>
|
|
857
|
-
<!--
|
|
858
|
-
|
|
859
|
-
<ul class="bandRow bandMore">
|
|
860
|
-
<li class="bandCell"><a class="bandT" href="#about">About</a></li>
|
|
861
|
-
<li class="bandCell"><a class="bandT" href="/docs">Docs</a></li>
|
|
862
|
-
</ul>
|
|
857
|
+
<!-- About and Docs are site pages, not project views: they live in the footer row with
|
|
858
|
+
Privacy / Terms / Contact (owner, 2026-08-28; task 1003332). -->
|
|
863
859
|
</div>
|
|
864
860
|
</div>
|
|
865
861
|
<div class="menuBand" id="menuProfile" data-menu="profile" hidden>
|
|
@@ -1096,6 +1092,8 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
1096
1092
|
<footer>
|
|
1097
1093
|
<div class="footrow">
|
|
1098
1094
|
<p><!--apex-only:start-->A non-profit, capture-resistant commons. <!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">Open source (AGPL-3.0)</a>.</p>
|
|
1095
|
+
<a href="#about">About</a>
|
|
1096
|
+
<a href="/docs">Docs</a>
|
|
1099
1097
|
<a href="/privacy">Privacy</a>
|
|
1100
1098
|
<a href="/terms">Terms</a>
|
|
1101
1099
|
<a href="/contact">Contact</a>
|