@bongos/core 1.19.484 → 1.19.486
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 +102 -52
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0232-rot-is-derived-not-swept-and-water-is-the-only-new-verb.md +155 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +147 -3
- package/docs/api-reference.md +7 -4
- package/docs/copy-inventory.md +169 -178
- package/docs/copy-registry.json +295 -405
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/migrations/core_231_project_settings_and_attention_water.sql +163 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +6 -0
- package/modules/hall-ui/public/goal-inbox.js +119 -1
- package/modules/hall-ui/public/not-ready.html +14 -10
- package/modules/hall-ui/public/not-ready.states.json +14 -0
- package/modules/hall-ui/public/pair.css +17 -67
- package/modules/hall-ui/public/pair.html +8 -1
- package/modules/hall-ui/public/pair.states.json +14 -0
- package/modules/hall-ui/public/panel.css +333 -0
- package/modules/hall-ui/public/profile.css +85 -0
- package/modules/hall-ui/public/profile.html +12 -5
- package/modules/hall-ui/public/profile.states.json +15 -0
- package/modules/hall-ui/public/settings.css +119 -336
- package/modules/hall-ui/public/settings.html +28 -19
- package/modules/hall-ui/public/settings.js +44 -35
- package/modules/hall-ui/public/settings.states.json +16 -0
- package/modules/hall-ui/public/style.css +5 -2
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/rot.js +210 -0
- package/modules/lifecycle/routes/rot.js +160 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness-checks-vocabulary.js +0 -1
- package/scripts/gds/fitness.js +9 -0
- package/src/bongos/project-settings.js +193 -0
- package/src/module-api.js +29 -1
- package/tests/hall_settings_world.mjs +274 -0
- package/tests/join_application_note.mjs +7 -15
- package/tests/module-scope-map.mjs +1 -1
- package/tests/module_api.mjs +1 -0
- package/tests/rot_inbox.mjs +532 -0
- package/modules/hall-ui/public/my-projects.css +0 -62
- package/modules/hall-ui/public/my-projects.html +0 -73
- package/modules/hall-ui/public/my-projects.js +0 -153
package/docs/file-map.md
CHANGED
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
│ ├── sessions/ ← CORE-DOMAIN carve (BV1.R82, default-on): the session_records corpus + data plane (upload, cross-builder search/deep-dive), the BFG read-the-room scorer, session-pulse/stale-claim helpers. Registers no port — reached via HTTP POST to its own /sessions route; resolves the economy `reward` port for the cost-plus token reward. CLAUDE.md, module.json, routes/sessions.js, bfg.js, pulse.js.
|
|
131
131
|
│ ├── autonomy/ ← CORE-DOMAIN carve (BV1.R83, default-on): the MAS autonomy surfaces — the $0 deterministic autonomy gate (System 3, inert until armed), the conductor/architect CLIs, and the in-process routine timers (incl. the weekly peer-vote-tally poller). Registers no port; resolves the lifecycle `tallyPeerVotes` doorway. CLAUDE.md, module.json, routes/autonomy.js, autonomy.js.
|
|
132
132
|
│ ├── status-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R84, default-on): the public status dashboard at status.<apex>, moved from public-status/. NO routes/db/port — declares contributes.webSurfaces:[{host:"status.",dir:"public"}], served by the loader web-surface seam (moduleWebSurfaces → serve-internal). CLAUDE.md, module.json, public/{index.html,status.js,style.css,cursors/,snapshots/}.
|
|
133
|
-
│ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav.
|
|
133
|
+
│ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Four ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages), `oversight.css` (the OVERSIGHT family — government, watch, harbor, gate, task 1003469: the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface) and `panel.css` (the PANEL family — settings, profile, pair, not-ready, task 1003308: the row, the switch, the field with its label above, the chip control, the four voices and the sign-in gate; the hall's my-projects page was DELETED in the same task — "my projects" is the hub's user-level page, and the hall keeps no link or redirect). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend, `settings.css` the rail layout, the group head, the token ladder and the voices grid, `profile.css` the identity band and the shelf, `pair.css` the device row, `people.css` the directory's identity cell (task 1003470 made the legacy door `/scouting` ship the same body and script as `/people`; task 1003440 then deleted the door — `scouting.html` and `scouting.states.json` are gone and the URL 301s to `/people`).
|
|
134
134
|
│ ├── lifecycle/ ← CORE-DOMAIN carve (BV1.R86, default-on): the build state machine — tasks · claims · versions · done-when · goals · dependencies · claim→ship→grade→credit · github-push + merge-lock + conflict-resolve + publish-reconciler. The LAST, most-coupled carve; registers the `lifecycle` kernel port (createTask, classifyTaskKind, tallyPeerVotes, work-tracking reads). CLAUDE.md, module.json, routes/{tasks,claims,versions,done-when,goals,dependencies,gate-approvals,analytics}.js, lifecycle.js, github-push.js, merge-lock.js, conflict-resolve.js, publish-reconciler.js, task-visuals.js + task-visual-db.js + routes/visuals.js (the optional ship-time visual, task 1003109 — the route file is NOT named task-*.js on purpose; see the module CLAUDE.md).
|
|
135
135
|
│ ├── copy-desk/ ← CORE-DOMAIN module (task 1003113 / R02 of goal 1000074, default-on): the COPY half of the design contract — ADR 0081 gave design TOKENS a repo-owned source of truth, text had none. Owns the FLAG (anyone in any role marks a string or a whole surface as needing an artist's attention, with a reason required in BOTH the route and a CHECK constraint) and the QUEUE an artist reads it from. READS R01's committed registry (docs/copy-registry.json, generated by scripts/gds/copy-inventory.js) and never writes it. NO LIVE CMS is the hard non-goal: no column here could hold replacement text, and copy_no_cms.mjs pins that. Key is `copy-desk`, not `copy` — the branding contract already owns a `copy` block, so the bare word reds the "core carries no module code" fitness check. CLAUDE.md, module.json, registry.js, flags.js, routes/copy-desk.js, migrations/copy_desk_001_flags.sql, tests/{copy_flags,copy_queue,copy_no_cms}.mjs. Its hall surface lives with hall-ui (public/copy-desk.{html,js,css}), per the one-key-per-web-surface precedent.
|
|
136
136
|
│ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans, then the detector), tells.js (task 1003329 / ADR 0223: the anti-pattern detector as two tiers — floors FAIL, craft tells WARN, kit-ignore waivers per file; the rendered twins live in lib.js factsInPage), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (modules/public-landing/public/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. skills/ (task 1003321 / ADR 0198) is the vendoring policy: policy.json (licence allowlist, verdicts that may land, sidecar fields, preamble markers), PREAMBLE.md (the platform preamble every vendored SKILL.md opens with), README.md (the roster — both origins scanned dangerous on 2026-08-28, landed as rebuild specs; nothing vendored — and the landing recipe); a landable copy goes under skills/vendor/<name>/ with PROVENANCE.md + LICENSE and is materialised into an instance's .claude/skills/ by claude-materialize.js step 1b. skills/<name>/ (task 1003325 / ADR 0220) are the in-house rebuilds from the specs, first-party and counted: design-taste-frontend (the default taste skill), design-taste-frontend-v1 (the earlier generation, kept distinct), high-end-visual-design (the polish pass), redesign-existing-projects (the in-place upgrade); each opens with the same preamble and carries a "Rebuilt from" section instead of a sidecar. impeccable/ (task 1003326 / ADR 0221) is the design-craft method as ONE skill with sub-commands: SKILL.md (the router, the menu, the craft floor, the bounded finish) + reference/<sub-command>.md (eighteen playbooks, one loaded per request); no hooks, no bypass agent, no telemetry, no live server. minimalist-ui/, industrial-brutalist-ui/, gpt-taste/, stitch-design-taste/ (task 1003327 / ADR 0230) are the four looks, composition grammars with no colours of their own: the palette is the instance's pack and the texture is a named entry in styles/; a substrate is an archetype that still ships both modes, gpt-taste's selection is derived from the route rather than rolled, and stitch-design-taste writes a DESIGN.md-shaped contract document whose thirteen are transcribed from the pack. image-to-code/, imagegen-frontend-web/, imagegen-frontend-mobile/, brandkit/ (task 1003328 / ADR 0222) are the image family under the hero-only rule: plates and hero objects only, the pair (JPEG dark plate + WebP alpha cutout) with a <file>.json sidecar beside each, through the instance's art pipeline key; the grove specimen's styles/grove/assets/ is the worked example. style/ (task 1003475 / ADR 0231) is the module's first FIRST-PARTY skill (no upstream, so a `## First party` section instead of `## Rebuilt from`): the /style session in which an owner authors their own look in four bounded steps, rendering the nearest library entries before asking, drafting inside the contract, retuning by measurement, and handing over the one-step adoption without ever writing config/branding.json. styles/ (task 1003323 / ADR 0219) is the style library: <name>/pack.json (the theme.ui overlay, exactly the fifteen) + DESIGN.md (the look's derived recipes, thirteen in lockstep with the pack) + mock.html (the specimen); chrome-world (pinned to the neutral pack), expedition, grove, blueprint (the first cool look, authored through /style at task 1003475); STYLE=<name> on the kit lays a look over the instance's pack through GDS_BRANDING_FILE; tests/ui_design_styles.mjs runs the world/cosmos/hall token suites once per alternate (UI_DESIGN_PACK). config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
|
|
@@ -1405,5 +1405,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1405
1405
|
landed since 1.19.482 with no explicit bump. run 33674657478. (task 1002620)
|
|
1406
1406
|
1.19.484 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1407
1407
|
landed since 1.19.483 with no explicit bump. run 33675143968. (task 1002620)
|
|
1408
|
+
1.19.485 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1409
|
+
landed since 1.19.484 with no explicit bump. run 33681828446. (task 1002620)
|
|
1410
|
+
1.19.486 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1411
|
+
landed since 1.19.485 with no explicit bump. run 33682949187. (task 1002620)
|
|
1408
1412
|
---------------------------------------------------------------------------
|
|
1409
1413
|
```
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
-- core_231_project_settings_and_attention_water.sql — the rot clock and the knob
|
|
2
|
+
-- that sets it (task 1003279, goal 1000072, criterion legibility-sort-and-staleness).
|
|
3
|
+
--
|
|
4
|
+
-- THE GAP. Goal 1000072's rotting half asks that "untouched-for-X triggers a
|
|
5
|
+
-- configured action rather than a silent decay". Two things were missing to make
|
|
6
|
+
-- that sentence true, and this migration adds exactly those two:
|
|
7
|
+
--
|
|
8
|
+
-- 1. X had nowhere to live. src/stale-timer.js (task 1003476, ADR 0226) owns the
|
|
9
|
+
-- CLAIM-silence timer — hours, env-configured, and the sweep that ACTS on it
|
|
10
|
+
-- releases someone's claim. The rot timer is a different measurement with a
|
|
11
|
+
-- different unit and a different consequence (days; it asks a human a
|
|
12
|
+
-- question), and task 1003280 requires it be "live, DB-backed" and warns
|
|
13
|
+
-- "do not fork a second store". So: one generic project_settings table, with
|
|
14
|
+
-- the rot timer as its first key.
|
|
15
|
+
--
|
|
16
|
+
-- 2. "Water" had nothing to write. The owner's four verbs are prune, kill,
|
|
17
|
+
-- prioritise and WATER — leave the thing alone and reset its clock. Without
|
|
18
|
+
-- somewhere to record that, watering would have to fake a real edit, and the
|
|
19
|
+
-- same line would come back on the next read; the inbox teaches people to
|
|
20
|
+
-- ignore it within a week. attention_watered_at IS the reset.
|
|
21
|
+
--
|
|
22
|
+
-- WHY A COLUMN AND NOT AN ITEMS TABLE. There is deliberately no `attention_items`
|
|
23
|
+
-- row anywhere in this design. The inbox line is DERIVED on read from the rot clock
|
|
24
|
+
-- (below) vs the timer, which makes "exactly one open item per stale thing, not one
|
|
25
|
+
-- per sweep" true by construction rather than by a de-duplicating sweep that has to
|
|
26
|
+
-- be got right.
|
|
27
|
+
-- Acting on the line (prune/kill/prioritise) changes status and the row stops
|
|
28
|
+
-- matching; watering pushes the clock forward and it stops matching. Nothing to
|
|
29
|
+
-- reconcile, nothing to garbage-collect, and no way to accrete duplicates.
|
|
30
|
+
--
|
|
31
|
+
-- Additive + forward-safe: every column is nullable with no backfill, nothing is
|
|
32
|
+
-- renamed, and no existing reader changes meaning (tests/migrations_forward_safe.mjs).
|
|
33
|
+
|
|
34
|
+
BEGIN;
|
|
35
|
+
|
|
36
|
+
-- ---------------------------------------------------------------------------
|
|
37
|
+
-- 1. project_settings — the instance's live, DB-backed knobs.
|
|
38
|
+
-- ---------------------------------------------------------------------------
|
|
39
|
+
--
|
|
40
|
+
-- Deliberately a generic key/value table rather than one column per knob: task
|
|
41
|
+
-- 1003280 puts five project fundamentals on one page, and a table that grows a
|
|
42
|
+
-- migration per knob would make that page a schema change every time. Values are
|
|
43
|
+
-- stored as TEXT and parsed by a typed accessor at the read edge
|
|
44
|
+
-- (src/bongos/project-settings.js), the same shape src/stale-timer.js already uses
|
|
45
|
+
-- for env values — one parser, one range check, one documented default.
|
|
46
|
+
--
|
|
47
|
+
-- A key that has never been set has NO ROW. Absence means "use the code default",
|
|
48
|
+
-- which is what lets a fresh instance boot correctly with an empty table, and what
|
|
49
|
+
-- lets a settings page distinguish "never configured" from "deliberately set to
|
|
50
|
+
-- the same value the default happens to be".
|
|
51
|
+
CREATE TABLE IF NOT EXISTS project_settings (
|
|
52
|
+
key text PRIMARY KEY,
|
|
53
|
+
value text NOT NULL,
|
|
54
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
55
|
+
updated_by bigint REFERENCES builders(id)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
COMMENT ON TABLE project_settings IS
|
|
59
|
+
'Live, DB-backed per-project knobs (task 1003279). Key/value so task 1003280''s settings page can grow a row without a migration. No row = never configured = use the code default; that absence is meaningful, so do not seed defaults here.';
|
|
60
|
+
COMMENT ON COLUMN project_settings.value IS
|
|
61
|
+
'Stored as text and parsed by a typed accessor at the read edge (src/bongos/project-settings.js). A value that fails its range check is IGNORED with a receipt, never coerced silently — the ADR 0226 posture.';
|
|
62
|
+
COMMENT ON COLUMN project_settings.updated_by IS
|
|
63
|
+
'The builder who last set this knob. NULL = set by a system path with no builder in the request (the goals.created_by convention, migration 160).';
|
|
64
|
+
|
|
65
|
+
-- ---------------------------------------------------------------------------
|
|
66
|
+
-- 2. The water mark, on both tiers that can rot.
|
|
67
|
+
-- ---------------------------------------------------------------------------
|
|
68
|
+
--
|
|
69
|
+
-- NULL = never watered, and the thing's own activity timestamp is the whole clock.
|
|
70
|
+
-- This is why the read expression is a GREATEST and not a bare COALESCE: a real edit
|
|
71
|
+
-- after a watering must move the clock forward again, and a watering must never make
|
|
72
|
+
-- a thing look OLDER than its last genuine touch.
|
|
73
|
+
--
|
|
74
|
+
-- THE ROT CLOCK, AND WHY IT IS SHAPED LIKE THIS:
|
|
75
|
+
--
|
|
76
|
+
-- tasks: GREATEST(updated_at, COALESCE(shipped_at, updated_at),
|
|
77
|
+
-- COALESCE(attention_watered_at, updated_at))
|
|
78
|
+
-- goals: GREATEST(updated_at, COALESCE(attention_watered_at, updated_at))
|
|
79
|
+
--
|
|
80
|
+
-- The first two terms are NOT a new definition of "touched" — they are exactly the
|
|
81
|
+
-- one modules/hall-ui/public/task-lib.js already publishes for the Tasks board and
|
|
82
|
+
-- the Goals page (`taskActivityMs = max(updated_at, shipped_at)`, task 1003278, this
|
|
83
|
+
-- same criterion), which folds in shipped_at because a ship path can stamp it
|
|
84
|
+
-- without touching updated_at. This migration adds a THIRD term to that definition
|
|
85
|
+
-- and nothing else. Had the server invented its own two-term clock, the board and
|
|
86
|
+
-- the inbox would disagree about which rows are stale — which is the 6h-vs-24h
|
|
87
|
+
-- failure of ADR 0226 reappearing one tier up, and the reason the indexes below
|
|
88
|
+
-- spell the expression out in full rather than approximating it.
|
|
89
|
+
-- (Goals carry no shipped_at column, so their clock has two terms, not three.)
|
|
90
|
+
ALTER TABLE tasks
|
|
91
|
+
ADD COLUMN IF NOT EXISTS attention_watered_at timestamptz,
|
|
92
|
+
ADD COLUMN IF NOT EXISTS attention_watered_by bigint REFERENCES builders(id);
|
|
93
|
+
|
|
94
|
+
ALTER TABLE goals
|
|
95
|
+
ADD COLUMN IF NOT EXISTS attention_watered_at timestamptz,
|
|
96
|
+
ADD COLUMN IF NOT EXISTS attention_watered_by bigint REFERENCES builders(id);
|
|
97
|
+
|
|
98
|
+
COMMENT ON COLUMN tasks.attention_watered_at IS
|
|
99
|
+
'When a human last said "leave it, it is fine" about this task rotting (task 1003279). Resets the rot clock without touching the work. NULL = never watered.';
|
|
100
|
+
COMMENT ON COLUMN goals.attention_watered_at IS
|
|
101
|
+
'When a human last said "leave it, it is fine" about this goal rotting (task 1003279). Resets the rot clock without touching the work. NULL = never watered.';
|
|
102
|
+
|
|
103
|
+
-- The rot read is "open things whose clock is older than N days", so the index is
|
|
104
|
+
-- on (status, clock) — status first because it is the equality predicate and it
|
|
105
|
+
-- cuts the table to the open rows before the range scan ever runs.
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_attention_rot
|
|
107
|
+
ON tasks (status, (GREATEST(updated_at,
|
|
108
|
+
COALESCE(shipped_at, updated_at),
|
|
109
|
+
COALESCE(attention_watered_at, updated_at))));
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_goals_attention_rot
|
|
111
|
+
ON goals (status, (GREATEST(updated_at, COALESCE(attention_watered_at, updated_at))));
|
|
112
|
+
|
|
113
|
+
-- ---------------------------------------------------------------------------
|
|
114
|
+
-- 3. Rebuild the two `t.*` views so the new columns are projected.
|
|
115
|
+
-- ---------------------------------------------------------------------------
|
|
116
|
+
--
|
|
117
|
+
-- A Postgres view created with `SELECT t.*` pins its column list at CREATE VIEW
|
|
118
|
+
-- time. core_227's header records six columns that silently never reached the claim
|
|
119
|
+
-- feed exactly this way; bodies below are identical to core_227's (the wildcard
|
|
120
|
+
-- re-expands to every current column).
|
|
121
|
+
|
|
122
|
+
DROP VIEW IF EXISTS claimable_tasks;
|
|
123
|
+
CREATE VIEW claimable_tasks AS
|
|
124
|
+
SELECT t.*,
|
|
125
|
+
pms_calibrated_minutes(t.est_minutes, t.kind) AS est_minutes_calibrated
|
|
126
|
+
FROM tasks t
|
|
127
|
+
WHERE t.status = 'ready'
|
|
128
|
+
AND t.title NOT LIKE '\_\_smoke\_\_%' ESCAPE '\'
|
|
129
|
+
AND NOT EXISTS (
|
|
130
|
+
SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id
|
|
131
|
+
)
|
|
132
|
+
AND NOT EXISTS (
|
|
133
|
+
SELECT 1 FROM claims c
|
|
134
|
+
JOIN tasks t2 ON t2.id = c.task_id
|
|
135
|
+
WHERE c.released_at IS NULL
|
|
136
|
+
AND t2.touches && t.touches
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
DROP VIEW IF EXISTS tasks_session_fit;
|
|
140
|
+
CREATE VIEW tasks_session_fit AS
|
|
141
|
+
SELECT t.*,
|
|
142
|
+
CASE
|
|
143
|
+
WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2
|
|
144
|
+
AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
|
|
145
|
+
THEN ARRAY['autonomous','check-in','active']
|
|
146
|
+
WHEN t.automation_tag = 'live-only' OR COALESCE(t.manual_degree, 5) >= 4
|
|
147
|
+
THEN ARRAY['active']
|
|
148
|
+
ELSE ARRAY['check-in','active']
|
|
149
|
+
END AS fits_session_modes,
|
|
150
|
+
pms_calibrated_minutes(t.est_minutes, t.kind) AS est_minutes_calibrated,
|
|
151
|
+
CASE
|
|
152
|
+
WHEN t.credits_reward > 0
|
|
153
|
+
AND pms_calibrated_minutes(t.est_minutes, t.kind) IS NOT NULL
|
|
154
|
+
AND pms_calibrated_minutes(t.est_minutes, t.kind) > 0
|
|
155
|
+
THEN ROUND(t.credits_reward::numeric / pms_calibrated_minutes(t.est_minutes, t.kind), 3)
|
|
156
|
+
ELSE NULL
|
|
157
|
+
END AS credits_per_minute
|
|
158
|
+
FROM tasks t;
|
|
159
|
+
|
|
160
|
+
COMMIT;
|
|
161
|
+
|
|
162
|
+
INSERT INTO schema_migrations (version) VALUES ('core_231_project_settings_and_attention_water')
|
|
163
|
+
ON CONFLICT (version) DO NOTHING;
|
|
@@ -386,6 +386,8 @@ function createClient(opts = {}) {
|
|
|
386
386
|
postGoalsIdTransfer: (args) => request("POST", "/goals/{id}/transfer", { hasBody: true }, args),
|
|
387
387
|
// PATCH /goals/{id}/visibility — rank: any-builder — PATCH /goals/:id/visibility
|
|
388
388
|
patchGoalsIdVisibility: (args) => request("PATCH", "/goals/{id}/visibility", { hasBody: true }, args),
|
|
389
|
+
// POST /goals/{id}/water — rank: any-builder — POST /goals/:id/water
|
|
390
|
+
postGoalsIdWater: (args) => request("POST", "/goals/{id}/water", { hasBody: true }, args),
|
|
389
391
|
// GET /goals/inbox — rank: any-builder — GET /goals/inbox
|
|
390
392
|
getGoalsInbox: (args) => request("GET", "/goals/inbox", { hasBody: false }, args),
|
|
391
393
|
// POST /goals/reorder — rank: metic+archon — POST /goals/reorder
|
|
@@ -466,6 +468,8 @@ function createClient(opts = {}) {
|
|
|
466
468
|
getInboxAwaitingNod: (args) => request("GET", "/inbox/awaiting-nod", { hasBody: false }, args),
|
|
467
469
|
// GET /inbox/by-builder/{id} — rank: any-builder — GET /inbox/by-builder/:id
|
|
468
470
|
getInboxByBuilderId: (args) => request("GET", "/inbox/by-builder/{id}", { hasBody: false }, args),
|
|
471
|
+
// GET /inbox/rotting — rank: any-builder — GET /inbox/rotting
|
|
472
|
+
getInboxRotting: (args) => request("GET", "/inbox/rotting", { hasBody: false }, args),
|
|
469
473
|
// POST /inbox/score — rank: any-builder — POST /inbox/score
|
|
470
474
|
postInboxScore: (args) => request("POST", "/inbox/score", { hasBody: true }, args),
|
|
471
475
|
// GET /inbox/sparks — rank: any-builder — GET /inbox/sparks
|
|
@@ -838,6 +842,8 @@ function createClient(opts = {}) {
|
|
|
838
842
|
postTasksIdVisual: (args) => request("POST", "/tasks/{id}/visual", { hasBody: true }, args),
|
|
839
843
|
// POST /tasks/{id}/vote — rank: metic+archon — POST /tasks/:id/vote
|
|
840
844
|
postTasksIdVote: (args) => request("POST", "/tasks/{id}/vote", { hasBody: true }, args),
|
|
845
|
+
// POST /tasks/{id}/water — rank: any-builder — POST /tasks/:id/water
|
|
846
|
+
postTasksIdWater: (args) => request("POST", "/tasks/{id}/water", { hasBody: true }, args),
|
|
841
847
|
// GET /tasks/claimable — rank: any-builder — GET /tasks/claimable
|
|
842
848
|
getTasksClaimable: (args) => request("GET", "/tasks/claimable", { hasBody: false }, args),
|
|
843
849
|
// GET /tasks/newcomer-floor — rank: any-builder — GET /tasks/newcomer-floor
|
|
@@ -167,6 +167,19 @@
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
function onClick(e) {
|
|
170
|
+
// task 1003279: a rot verb. Handled FIRST because its rows carry no data-goal —
|
|
171
|
+
// the path is server-declared and already tier-correct, so this branch needs to
|
|
172
|
+
// know nothing about which of the four verbs was pressed beyond what to say
|
|
173
|
+
// afterwards. Refreshes the ROT slice only: repainting the goals list would
|
|
174
|
+
// leave the acted-on row sitting there (the task-1003066 lesson).
|
|
175
|
+
const rotBtn = e.target.closest('[data-rot-path]');
|
|
176
|
+
if (rotBtn) {
|
|
177
|
+
const verb = rotBtn.dataset.rotVerb;
|
|
178
|
+
act(() => postJSON(`${API}${rotBtn.dataset.rotPath}`, {}),
|
|
179
|
+
ROT_TOASTS[verb] || 'Done.', loadRot);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
170
183
|
// task 1003066 (BV1.R05): the nod. It reuses the EXISTING promote route —
|
|
171
184
|
// POST /tasks/:id/promote, already Metic+ via task.promote — so giving a nod
|
|
172
185
|
// adds no endpoint and no permission. Handled before the membership branches
|
|
@@ -262,6 +275,104 @@
|
|
|
262
275
|
.catch(() => { /* a non-triager's 403, or a hiccup: never break the hall */ });
|
|
263
276
|
}
|
|
264
277
|
|
|
278
|
+
// ---- the rot queue (task 1003279, goal 1000072) ---------------------------
|
|
279
|
+
//
|
|
280
|
+
// "Untouched-for-X triggers a configured action rather than a silent decay."
|
|
281
|
+
// The owner's decision (2026-08-25): the action is a LINE IN THIS CARD, not an
|
|
282
|
+
// automatic status change and not a nightly digest. Auto-demoting mutates real
|
|
283
|
+
// work with no human in the loop, and a wrong timer then silently freezes it; a
|
|
284
|
+
// digest is a notification, not an action, and gets scrolled past.
|
|
285
|
+
//
|
|
286
|
+
// THE BUTTONS ARE SERVER-DECLARED. Each row arrives carrying its own `verbs`
|
|
287
|
+
// array — label, method and path — and this renders exactly those. It is not
|
|
288
|
+
// decoration: three of the four verbs reuse EXISTING routes (promote, demote,
|
|
289
|
+
// abandon, archive) whose permissions differ by tier, and a hardcoded button list
|
|
290
|
+
// here would be a second copy of that mapping, free to drift into offering a
|
|
291
|
+
// button the server will refuse. A goal legitimately gets two verbs where a task
|
|
292
|
+
// gets four; this file does not need to know why.
|
|
293
|
+
//
|
|
294
|
+
// No client-side rank gate, for the same reason the nod slice has none: the read
|
|
295
|
+
// is scoped server-side, so a builder is only ever handed rows that are theirs to
|
|
296
|
+
// act on and the button list is already narrowed to what they can press.
|
|
297
|
+
const MAX_ROT_ROWS = 5;
|
|
298
|
+
|
|
299
|
+
function untouchedFor(iso) {
|
|
300
|
+
const t = iso ? Date.parse(iso) : NaN;
|
|
301
|
+
if (Number.isNaN(t)) return 'untouched';
|
|
302
|
+
const days = Math.floor((Date.now() - t) / 86400000);
|
|
303
|
+
if (days < 1) return 'untouched today';
|
|
304
|
+
return `untouched ${days} day${days === 1 ? '' : 's'}`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function rotRowHtml(r) {
|
|
308
|
+
const href = r.tier === 'goal'
|
|
309
|
+
? goalHref(r.id)
|
|
310
|
+
: `/task/${encodeURIComponent(r.id)}`;
|
|
311
|
+
const where = r.tier === 'goal'
|
|
312
|
+
? 'goal'
|
|
313
|
+
: (r.goal_title ? escapeHtml(r.goal_title) : `goal ${escapeHtml(String(r.goal_id || '—'))}`);
|
|
314
|
+
// `watered` is worth showing: a row that is back DESPITE having been watered is
|
|
315
|
+
// a different message from one nobody has ever looked at, and hiding that would
|
|
316
|
+
// make the second watering feel like the first one did nothing.
|
|
317
|
+
const again = r.ever_watered ? ' · watered before' : '';
|
|
318
|
+
const buttons = (Array.isArray(r.verbs) ? r.verbs : []).map((v) => {
|
|
319
|
+
const path = String(v.path || '').replace(':id', encodeURIComponent(r.id));
|
|
320
|
+
return `<button class="btn-ghost" type="button" data-rot-path="${escapeHtml(path)}"`
|
|
321
|
+
+ ` data-rot-verb="${escapeHtml(String(v.verb || ''))}"`
|
|
322
|
+
+ ` title="${escapeHtml(String(v.hint || ''))}">${escapeHtml(String(v.label || v.verb || '?'))}</button>`;
|
|
323
|
+
}).join('');
|
|
324
|
+
return `<li class="ginbox__item" data-rot-tier="${escapeHtml(String(r.tier))}">
|
|
325
|
+
<div class="ginbox__main">
|
|
326
|
+
<a class="ginbox__goal" href="${href}">${escapeHtml(r.title || `${r.tier} #${r.id}`)}</a>
|
|
327
|
+
<span class="ginbox__sub">${escapeHtml(String(r.status || ''))} · ${where} · ${escapeHtml(untouchedFor(r.last_touched_at))}${again}</span>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="ginbox__acts">${buttons}</div>
|
|
330
|
+
</li>`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// What each verb says when it lands. Keyed by the SERVER's verb name so a new verb
|
|
334
|
+
// degrades to a neutral confirmation instead of a blank toast.
|
|
335
|
+
const ROT_TOASTS = {
|
|
336
|
+
prioritise: 'Prioritised — it’s claimable now.',
|
|
337
|
+
prune: 'Pruned — back in the backlog.',
|
|
338
|
+
kill: 'Killed — it won’t come back.',
|
|
339
|
+
water: 'Watered — the clock is reset.',
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
function renderRot(data) {
|
|
343
|
+
const box = host && host.querySelector('#attention-rot');
|
|
344
|
+
if (!box) return;
|
|
345
|
+
const rows = (data && Array.isArray(data.rows)) ? data.rows : [];
|
|
346
|
+
if (!rows.length) {
|
|
347
|
+
box.innerHTML = '';
|
|
348
|
+
window.OTBAttention?.refresh();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const shown = rows.slice(0, MAX_ROT_ROWS);
|
|
352
|
+
const more = rows.length > shown.length
|
|
353
|
+
? `<p class="ginbox__pending">…${rows.length - shown.length} more going quiet.</p>`
|
|
354
|
+
: '';
|
|
355
|
+
// The heading names the TIMER, not just the count. A reader who disagrees with
|
|
356
|
+
// the line ("30 days is too short for a backlog") needs to know which number
|
|
357
|
+
// produced it before they can go change it — and when that number came from a
|
|
358
|
+
// value the server could not use, the receipt says so here, on the screen of
|
|
359
|
+
// somebody who can fix it.
|
|
360
|
+
const t = (data && data.timer) || {};
|
|
361
|
+
const timerNote = t.invalid
|
|
362
|
+
? `<span class="ginbox__sub"> — ${escapeHtml(String(t.describe || ''))}</span>`
|
|
363
|
+
: `<span class="ginbox__sub"> — untouched ${escapeHtml(String(t.rot_days ?? '?'))}+ days</span>`;
|
|
364
|
+
box.innerHTML = '<h3 class="ginbox__sec-h">Going quiet '
|
|
365
|
+
+ `<span class="count-pill">${rows.length}</span>${timerNote}</h3>`
|
|
366
|
+
+ `<ul class="ginbox__list">${shown.map(rotRowHtml).join('')}</ul>${more}`;
|
|
367
|
+
window.OTBAttention?.refresh();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function loadRot() {
|
|
371
|
+
return getJSON(`${API}/inbox/rotting`)
|
|
372
|
+
.then((data) => renderRot(data))
|
|
373
|
+
.catch(() => { /* signed out, older server, or a hiccup: never break the hall */ });
|
|
374
|
+
}
|
|
375
|
+
|
|
265
376
|
// task 2169: this widget IS the "Needs your attention" card — ONE capped
|
|
266
377
|
// summary hosting every actionable item class: goal invitations +
|
|
267
378
|
// join-requests (this file) and ideas awaiting triage (builders.js renders
|
|
@@ -294,10 +405,17 @@
|
|
|
294
405
|
// awaiting a nod inside a goal sits nearer the goals slice than idea
|
|
295
406
|
// triage. Its rows carry the shared .ginbox__item class, so refresh()
|
|
296
407
|
// counts them with no change to the selector below.
|
|
297
|
-
'<div id="attention-nod"></div>'
|
|
408
|
+
'<div id="attention-nod"></div>' +
|
|
409
|
+
// task 1003279 (goal 1000072): work going quiet — a goal or task untouched
|
|
410
|
+
// past the project's rot timer, with the owner's four verbs on it. LAST in
|
|
411
|
+
// the card on purpose: every slice above is somebody waiting on YOU right
|
|
412
|
+
// now, while rot is a standing question that has already waited weeks and
|
|
413
|
+
// can wait until they are read.
|
|
414
|
+
'<div id="attention-rot"></div>';
|
|
298
415
|
host.addEventListener('click', onClick);
|
|
299
416
|
load();
|
|
300
417
|
loadNodQueue();
|
|
418
|
+
loadRot();
|
|
301
419
|
// task 1002802 (P06): the profile slice lives in its own file (builders.js is
|
|
302
420
|
// over the size budget); this card owns its div, so it boots it here.
|
|
303
421
|
window.OTBProfileNudge?.load();
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
11
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
+
<!-- The PANEL archetype (task 1003308): this page is its sign-in gate shape. -->
|
|
13
|
+
<link rel="stylesheet" href="/builders/panel.css">
|
|
12
14
|
</head>
|
|
13
15
|
<body data-page="not-ready" data-page-title="Locked">
|
|
14
16
|
|
|
@@ -18,21 +20,23 @@
|
|
|
18
20
|
<div class="app-body">
|
|
19
21
|
<header class="app-topbar" id="app-topbar"></header>
|
|
20
22
|
<div class="app-content">
|
|
23
|
+
<!-- The page's ONE h1 (one-header-main-h1), in the family's voice. -->
|
|
24
|
+
<div class="page-head page-head--room page-head--panel">
|
|
25
|
+
<h1 id="not-ready-h1" class="page-head__h">not yet</h1>
|
|
26
|
+
<p class="page-head__sub">This page unlocks as you advance.</p>
|
|
27
|
+
</div>
|
|
21
28
|
<main id="hall-main">
|
|
22
|
-
<section class="scroll">
|
|
23
|
-
<h2 class="
|
|
24
|
-
<p class="
|
|
25
|
-
<p>
|
|
29
|
+
<section class="scroll gate" aria-labelledby="not-ready-h">
|
|
30
|
+
<h2 id="not-ready-h" class="gate__h">Not yet available</h2>
|
|
31
|
+
<p class="gate__lede">
|
|
26
32
|
Xenos builders start with a focused set of pages — ship your first
|
|
27
33
|
<strong>three tasks</strong> to graduate and open the rest of
|
|
28
34
|
{{worldName}}'s builder hub.
|
|
29
35
|
</p>
|
|
30
|
-
<
|
|
31
|
-
<a href="/ranks">See how ranks work
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<a href="/">← Back to Home</a>
|
|
35
|
-
</p>
|
|
36
|
+
<div class="row-end">
|
|
37
|
+
<a class="pbtn pbtn--save" href="/ranks">See how ranks work</a>
|
|
38
|
+
<a class="pbtn pbtn--quiet" href="/">Back to home</a>
|
|
39
|
+
</div>
|
|
36
40
|
</section>
|
|
37
41
|
</main>
|
|
38
42
|
<footer class="app-foot" id="app-foot"></footer>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The NOT-READY page through modules/ui-design/kit/render.js (task 1003308) — the sign-in gate shape of the panel family, static markup with no feed. Renders under the kit's own stub and under the harness alike.",
|
|
3
|
+
"page": "/not-ready",
|
|
4
|
+
"surface": "hall-ui",
|
|
5
|
+
"stub": { "prefix": "/builders/" },
|
|
6
|
+
"modeQuery": false,
|
|
7
|
+
"ignoreRequests": ["/api/(gds|bongos)/"],
|
|
8
|
+
"ignoreConsole": ["/api/(gds|bongos)/", "\b401\b", "\b404\b", "Unauthorized", "Not Found", "load failed"],
|
|
9
|
+
"states": {
|
|
10
|
+
"locked": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#not-ready-h1", "#not-ready-h"] } },
|
|
11
|
+
"out": { "auth": false, "actions": [["wait", 400]], "expect": { "visible": ["#not-ready-h1"] } }
|
|
12
|
+
},
|
|
13
|
+
"reducedMotion": { "auth": true, "actions": [["wait", 400]] }
|
|
14
|
+
}
|
|
@@ -1,73 +1,23 @@
|
|
|
1
|
-
/* pair.css —
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/* pair.css — what the Dev Box pairing page has and the rest of the panel family
|
|
2
|
+
does not (task 1003308, goal 1000081; the archetype is panel.css). One panel,
|
|
3
|
+
one device, one button. The device is a row of the family with the platform
|
|
4
|
+
glyph as its figure; the approve button is the page's one accent door. */
|
|
5
5
|
|
|
6
|
-
/* Page-local styles for the pairing approve page (#904) — built on the
|
|
7
|
-
shared design-system tokens in style.css. */
|
|
8
6
|
#pair-scroll { max-width: 640px; }
|
|
9
7
|
|
|
10
8
|
.pair-device {
|
|
11
9
|
display: flex;
|
|
12
10
|
align-items: center;
|
|
13
|
-
gap:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
border-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
.pair-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
.pair-
|
|
25
|
-
|
|
26
|
-
font-size: 14px;
|
|
27
|
-
font-weight: 600;
|
|
28
|
-
color: var(--ink);
|
|
29
|
-
}
|
|
30
|
-
.pair-device__meta {
|
|
31
|
-
font-family: var(--font-body);
|
|
32
|
-
font-size: 13px;
|
|
33
|
-
color: var(--ink-soft);
|
|
34
|
-
}
|
|
35
|
-
.pair-code {
|
|
36
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
37
|
-
font-size: 12.5px;
|
|
38
|
-
letter-spacing: 0.08em;
|
|
39
|
-
color: var(--ink-soft);
|
|
40
|
-
}
|
|
41
|
-
.pair-actions {
|
|
42
|
-
display: flex;
|
|
43
|
-
align-items: center;
|
|
44
|
-
gap: 14px;
|
|
45
|
-
flex-wrap: wrap;
|
|
46
|
-
}
|
|
47
|
-
.pair-approve {
|
|
48
|
-
font-family: var(--font-body);
|
|
49
|
-
font-size: 13.5px;
|
|
50
|
-
font-weight: 600;
|
|
51
|
-
padding: 10px 22px;
|
|
52
|
-
background: var(--accent);
|
|
53
|
-
color: #fff;
|
|
54
|
-
border: 1px solid var(--accent);
|
|
55
|
-
border-radius: var(--radius-sm);
|
|
56
|
-
cursor: pointer;
|
|
57
|
-
}
|
|
58
|
-
.pair-approve:hover:not(:disabled) { background: var(--accent-deep); border-color: var(--accent-deep); }
|
|
59
|
-
.pair-approve:disabled { opacity: 0.55; cursor: default; }
|
|
60
|
-
.pair-status {
|
|
61
|
-
font-family: var(--font-body);
|
|
62
|
-
font-size: 13.5px;
|
|
63
|
-
color: var(--ink-soft);
|
|
64
|
-
min-height: 1.2em;
|
|
65
|
-
}
|
|
66
|
-
.pair-status--ok { color: var(--ok); font-weight: 600; }
|
|
67
|
-
.pair-status--err { color: var(--danger); }
|
|
68
|
-
.pair-note {
|
|
69
|
-
font-family: var(--font-body);
|
|
70
|
-
font-size: 13px;
|
|
71
|
-
color: var(--ink-faint);
|
|
72
|
-
margin-top: 16px;
|
|
73
|
-
}
|
|
11
|
+
gap: var(--sp-sm);
|
|
12
|
+
margin: var(--sp-sm) 0 var(--sp-md);
|
|
13
|
+
padding: var(--sp-sm) 0;
|
|
14
|
+
border-top: var(--line) solid var(--rule-soft);
|
|
15
|
+
border-bottom: var(--line) solid var(--rule-soft);
|
|
16
|
+
}
|
|
17
|
+
.pair-device__glyph { width: 40px; font-size: 26px; line-height: 1; text-align: center; color: var(--ink-soft); flex: none; }
|
|
18
|
+
.pair-device__name { font-size: 14.5px; font-weight: 500; color: var(--ink); }
|
|
19
|
+
.pair-device__meta { margin-top: 2px; font-size: 13.5px; color: var(--ink-soft); }
|
|
20
|
+
.pair-code { font-family: var(--font-mono); font-size: 12.5px; letter-spacing: .08em; color: var(--ink); }
|
|
21
|
+
.pair-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-xs); }
|
|
22
|
+
.pair-actions .pair-status { margin: 0; }
|
|
23
|
+
.pair-note { margin: var(--sp-sm) 0 0; max-width: var(--measure); font-size: 13px; color: var(--ink-faint); }
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
11
|
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
+
<!-- The PANEL archetype (task 1003308) before the page's own sheet. -->
|
|
13
|
+
<link rel="stylesheet" href="/builders/panel.css">
|
|
12
14
|
<link rel="stylesheet" href="/builders/pair.css">
|
|
13
15
|
</head>
|
|
14
16
|
<body data-page="pair" data-page-title="Device pairing">
|
|
@@ -19,7 +21,12 @@
|
|
|
19
21
|
<div class="app-body">
|
|
20
22
|
<header class="app-topbar" id="app-topbar"></header>
|
|
21
23
|
<div class="app-content">
|
|
22
|
-
|
|
24
|
+
<!-- The page's ONE h1 (one-header-main-h1), in the family's voice; the
|
|
25
|
+
sub-line is pair.js's — the request, the approval, or the error. -->
|
|
26
|
+
<div class="page-head page-head--room page-head--panel">
|
|
27
|
+
<h1 id="pair-h1" class="page-head__h">pairing</h1>
|
|
28
|
+
<p class="page-head__sub" id="pair-sub">Reading the request…</p>
|
|
29
|
+
</div>
|
|
23
30
|
<main id="hall-main">
|
|
24
31
|
<section class="scroll" id="pair-scroll" aria-labelledby="pair-h">
|
|
25
32
|
<h2 id="pair-h" class="scroll__h">A device is asking to act as you</h2>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The Dev Box PAIRING page through modules/ui-design/kit/render.js (task 1003308). The device row and the approve button only exist once the pairing request has LOADED, so the populated state runs against the hall-preview harness (see settings.states.json for the two commands; the harness serves scripts/hall-preview/fixtures/auth__app-pair__info.json for any code). `nocode` is the page's own error render — the link arrived without its pairing code — and needs nothing served.",
|
|
3
|
+
"page": "/pair",
|
|
4
|
+
"surface": "hall-ui",
|
|
5
|
+
"stub": { "prefix": "/builders/" },
|
|
6
|
+
"modeQuery": false,
|
|
7
|
+
"ignoreRequests": ["/api/(gds|bongos)/"],
|
|
8
|
+
"ignoreConsole": ["/api/(gds|bongos)/", "\b401\b", "\b404\b", "Unauthorized", "Not Found", "load failed"],
|
|
9
|
+
"states": {
|
|
10
|
+
"request": { "auth": true, "url": "/pair?code=K7QD-4M2X", "actions": [["wait", 700]], "expect": { "visible": ["#pair-h1", "#pair-approve-btn"] } },
|
|
11
|
+
"nocode": { "auth": true, "actions": [["wait", 700]], "expect": { "visible": ["#pair-h1", "#pair-scroll .pair-status--err"] } }
|
|
12
|
+
},
|
|
13
|
+
"reducedMotion": { "auth": true, "url": "/pair?code=K7QD-4M2X", "actions": [["wait", 700]] }
|
|
14
|
+
}
|