@bongos/core 1.19.826 → 1.19.828
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 +58 -23
- package/docs/adr/0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md +17 -0
- package/docs/copy-inventory.md +20 -14
- package/docs/copy-registry.json +83 -19
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/blockers-lib.js +110 -0
- package/modules/hall-ui/public/blockers.html +65 -0
- package/modules/hall-ui/public/blockers.js +88 -0
- package/modules/hall-ui/public/goal-inbox.js +74 -0
- package/modules/hall-ui/public/index.html +4 -0
- package/modules/hall-ui/public/shell.js +9 -0
- package/modules/platform-identity/routes/my-projects.js +27 -1
- package/modules/provisioning/module.json +1 -1
- package/modules/provisioning/routes/provisioning.js +2 -22
- package/modules/provisioning/seams.js +54 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/blockers_hall.mjs +107 -0
- package/tests/blockers_lib.mjs +102 -0
- package/tests/hall_nav.mjs +4 -1
- package/tests/hall_page_gate_map.mjs +8 -0
- package/tests/join_dark_matter.mjs +208 -0
- package/tests/projects_hub.mjs +2 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// blockers-lib.js — the ONE blocker-row renderer (task 1003455, goal 1000089).
|
|
2
|
+
//
|
|
3
|
+
// GET /blockers (modules/ideas/routes/blockers.js) had zero consumers — not the
|
|
4
|
+
// hall, not status-ui — and the only blocker a builder could ever see was
|
|
5
|
+
// GET /blockers/:id, reachable only by typing #/blocker/N into the address bar.
|
|
6
|
+
// Owner decision 2026-08-30: BOTH surfaces, from ONE code path — the open-blocker
|
|
7
|
+
// queue gets a real page (modules/hall-ui/public/blockers.js), and open blockers
|
|
8
|
+
// also become line items in the existing Needs-attention inbox
|
|
9
|
+
// (modules/hall-ui/public/goal-inbox.js), because hall-ui/CLAUDE.md's placement
|
|
10
|
+
// rule says an action awaiting a human is an inbox line, never a new Home card.
|
|
11
|
+
//
|
|
12
|
+
// This file is the shared middle: the goals-lib.js / goalRenderFacts precedent —
|
|
13
|
+
// ONE facts source that both callers render from their own DOM shape, so the two
|
|
14
|
+
// surfaces cannot drift apart the way ~10 hand-rolled pagers did before the kit.
|
|
15
|
+
// The two callers do not share a visual family (the inbox nests inside
|
|
16
|
+
// .ginbox__item, the page's rows are the oversight family's .ov-row), so — unlike
|
|
17
|
+
// goalRenderFacts, whose two callers share one family and one fixed class — the
|
|
18
|
+
// class names are the caller's own, passed in `opts`; only the FACTS (title, the
|
|
19
|
+
// filed date, the note, which tasks it blocks) are shared.
|
|
20
|
+
//
|
|
21
|
+
// Pure: no window.OTB, no DOM, no fetch — Node-require-safe like goals-lib.js /
|
|
22
|
+
// hall-kit.js, so a test can run the real arithmetic instead of agreeing with a
|
|
23
|
+
// copy of it.
|
|
24
|
+
(function (root, factory) {
|
|
25
|
+
var mod = factory();
|
|
26
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = mod;
|
|
27
|
+
else root.OTBBlockersLib = mod;
|
|
28
|
+
}(typeof self !== 'undefined' ? self : this, function () {
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
function esc(s) {
|
|
32
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
33
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fmtDate(iso) {
|
|
38
|
+
if (!iso) return '';
|
|
39
|
+
var d = new Date(iso);
|
|
40
|
+
if (isNaN(d.getTime())) return '';
|
|
41
|
+
return d.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A blocker's body_md is the owner's own words, quoted the way goal-inbox.js's
|
|
45
|
+
// invitation/join rows already quote a note (the SAME convention, not a new
|
|
46
|
+
// one). It can run to a paragraph, and the row is a summary, so it is clipped —
|
|
47
|
+
// CSS line-clamp handles visual overflow on both surfaces, this just keeps one
|
|
48
|
+
// enormous blocker from making the row's own text node absurd.
|
|
49
|
+
function noteHtml(bodyMd, noteClass) {
|
|
50
|
+
var t = String(bodyMd || '').trim();
|
|
51
|
+
if (!t) return '';
|
|
52
|
+
var clipped = t.length > 240 ? t.slice(0, 239).replace(/\s+$/, '') + '…' : t;
|
|
53
|
+
return '<p class="' + esc(noteClass) + '">“' + esc(clipped) + '”</p>';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// `taskHref(id)` is the caller's own link builder (window.OTBTaskDetail.href on
|
|
57
|
+
// a live page) — kept OUT of this pure module the way hall-kit.js stays
|
|
58
|
+
// date-agnostic: the lib never learns what a task URL looks like.
|
|
59
|
+
function blockingLine(b, taskHref) {
|
|
60
|
+
var ids = Array.isArray(b.blocking_task_ids) ? b.blocking_task_ids : [];
|
|
61
|
+
if (!ids.length) return 'Blocking nothing yet';
|
|
62
|
+
var links = ids.map(function (id) {
|
|
63
|
+
var label = '#' + id;
|
|
64
|
+
return taskHref ? '<a href="' + esc(taskHref(id)) + '">' + esc(label) + '</a>' : esc(label);
|
|
65
|
+
}).join(', ');
|
|
66
|
+
var noun = ids.length === 1 ? 'task' : 'tasks';
|
|
67
|
+
return 'Blocking ' + ids.length + ' ' + noun + ': ' + links;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// blockerFacts(b, opts) — the shared render. `opts`:
|
|
71
|
+
// titleClass (default 'ov-row__title')
|
|
72
|
+
// noteClass (default 'ov-row__sub')
|
|
73
|
+
// whenClass (default 'ov-row__when')
|
|
74
|
+
// blockerHref(id) — the title's own link (both surfaces point at the SAME
|
|
75
|
+
// #/blocker/N overlay task-detail.js already renders —
|
|
76
|
+
// the "reachable only by typing the URL" bug this task
|
|
77
|
+
// fixes is exactly that overlay having no door INTO it).
|
|
78
|
+
// taskHref(id) — see blockingLine above.
|
|
79
|
+
// Returns { titleHtml, noteHtml, factsHtml } — fragments, not a wrapped row;
|
|
80
|
+
// each caller nests them in its OWN <li> (goal-inbox.js's .ginbox__item,
|
|
81
|
+
// blockers.js's .ov-row), the goalRenderFacts idiom.
|
|
82
|
+
function blockerFacts(b, opts) {
|
|
83
|
+
opts = opts || {};
|
|
84
|
+
var titleClass = opts.titleClass || 'ov-row__title';
|
|
85
|
+
var noteClass = opts.noteClass || 'ov-row__sub';
|
|
86
|
+
var whenClass = opts.whenClass || 'ov-row__when';
|
|
87
|
+
var blockerHref = opts.blockerHref || null;
|
|
88
|
+
var taskHref = opts.taskHref || null;
|
|
89
|
+
|
|
90
|
+
var titleText = esc(b.title || ('Blocker #' + b.id));
|
|
91
|
+
var titleHtml = blockerHref
|
|
92
|
+
? '<a class="' + esc(titleClass) + '" href="' + esc(blockerHref(b.id)) + '">' + titleText + '</a>'
|
|
93
|
+
: '<span class="' + esc(titleClass) + '">' + titleText + '</span>';
|
|
94
|
+
|
|
95
|
+
// ONE fact line, `·`-joined — the watch.js convention (its box/report rows
|
|
96
|
+
// join multiple facts into a single .ov-row__when span the same way) rather
|
|
97
|
+
// than several sibling spans, which need a flex-gapped parent to space
|
|
98
|
+
// themselves and the attention-inbox context (.ginbox__sub) is not one.
|
|
99
|
+
var facts = 'Filed ' + esc(fmtDate(b.created_at)) + ' · ' + blockingLine(b, taskHref);
|
|
100
|
+
var factsHtml = '<span class="' + esc(whenClass) + '">' + facts + '</span>';
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
titleHtml: titleHtml,
|
|
104
|
+
noteHtml: noteHtml(b.body_md, noteClass),
|
|
105
|
+
factsHtml: factsHtml,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { esc: esc, fmtDate: fmtDate, blockingLine: blockingLine, blockerFacts: blockerFacts };
|
|
110
|
+
}));
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Blockers — {{worldName}}</title>
|
|
7
|
+
<meta name="description" content="What is stuck on the owner right now: open blockers and the work they hold up.">
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text x='16' y='24' font-size='24' text-anchor='middle' font-family='sans-serif'>⌂</text></svg>">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
11
|
+
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
12
|
+
<!-- task 1003455: the row shape (title + facts) is the oversight family's,
|
|
13
|
+
the same archetype collab/copy-desk/watch/gate already ride. -->
|
|
14
|
+
<link rel="stylesheet" href="/builders/oversight.css">
|
|
15
|
+
</head>
|
|
16
|
+
<body data-page="blockers" data-page-title="Blockers">
|
|
17
|
+
|
|
18
|
+
<div class="app">
|
|
19
|
+
<aside class="app-sidebar" id="app-sidebar" aria-label="Primary"></aside>
|
|
20
|
+
<div class="app-scrim" id="app-scrim" hidden></div>
|
|
21
|
+
<div class="app-body">
|
|
22
|
+
<header class="app-topbar" id="app-topbar"></header>
|
|
23
|
+
<div class="app-content">
|
|
24
|
+
<div class="page-head page-head--board">
|
|
25
|
+
<h1 class="page-head__h">Blockers</h1>
|
|
26
|
+
<p class="page-head__sub" id="blockers-sub">Things stuck on the owner — a credential, a purchase, a product call only they can make. Resolving one un-sticks every task waiting on it automatically.</p>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<main id="hall-main">
|
|
30
|
+
<!-- task 1003455 (goal 1000089): the queue GET /blockers had no page for
|
|
31
|
+
— the only door in used to be typing #/blocker/N into the address
|
|
32
|
+
bar. This is the read half; filing/resolving/linking is task 1003456. -->
|
|
33
|
+
<section class="scroll" id="blockers-scroll" aria-labelledby="blockers-h">
|
|
34
|
+
<h2 id="blockers-h" class="scroll__h">Open blockers<span class="count-pill" id="blockers-count" hidden></span></h2>
|
|
35
|
+
<div id="blockers-body">
|
|
36
|
+
<p class="profile__placeholder">Loading…</p>
|
|
37
|
+
</div>
|
|
38
|
+
</section>
|
|
39
|
+
</main>
|
|
40
|
+
|
|
41
|
+
<footer class="app-foot" id="app-foot"></footer>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
|
|
47
|
+
|
|
48
|
+
<!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
|
|
49
|
+
chrome exists before the page script runs. -->
|
|
50
|
+
<script src="/builders/shell.js?v=2026-07-02-shell1"></script>
|
|
51
|
+
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
52
|
+
<script src="/builders/palette.js" defer></script>
|
|
53
|
+
<!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
|
|
54
|
+
branding pack's currency label, loaded BEFORE blockers.js. -->
|
|
55
|
+
<script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
|
|
56
|
+
<script src="/builders/brand-apply.js"></script>
|
|
57
|
+
<script src="/builders/hall-kit.js"></script>
|
|
58
|
+
<!-- task-detail.js still owns the #/blocker/N deep-link overlay every row's
|
|
59
|
+
title opens into — this page needs its link-builder (OTBTaskDetail.href)
|
|
60
|
+
for the "blocking N tasks" facts line, but not the overlay's own render. -->
|
|
61
|
+
<script src="/builders/task-detail.js?v=2026-07-03-taskdetail"></script>
|
|
62
|
+
<script src="/builders/blockers-lib.js"></script>
|
|
63
|
+
<script src="/builders/blockers.js"></script>
|
|
64
|
+
</body>
|
|
65
|
+
</html>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// blockers.js — the Blockers page: the full open-blocker queue (task 1003455,
|
|
2
|
+
// goal 1000089).
|
|
3
|
+
//
|
|
4
|
+
// GET /blockers (modules/ideas/routes/blockers.js) is PUBLIC and had ZERO hall
|
|
5
|
+
// consumers before this task — the only blocker a builder could ever see was the
|
|
6
|
+
// #/blocker/N overlay task-detail.js already renders, reachable only by typing
|
|
7
|
+
// the id into the address bar. Owner decision 2026-08-30: BOTH surfaces, from
|
|
8
|
+
// ONE code path — this page is the full queue (a long list has nowhere else to
|
|
9
|
+
// live), and goal-inbox.js's attention-inbox slice is the capped summary. Both
|
|
10
|
+
// render from blockers-lib.js's blockerFacts(), so the two cannot drift apart
|
|
11
|
+
// the way ~10 hand-rolled pagers did before the kit (hall-ui/CLAUDE.md).
|
|
12
|
+
//
|
|
13
|
+
// Read-only: filing, resolving and linking a blocker to a task are task 1003456.
|
|
14
|
+
// Deliberately UNGATED (tests/hall_page_gate_map.mjs) — its one data source is
|
|
15
|
+
// public, the roster/drachmae precedent: a page must not be gated tighter than
|
|
16
|
+
// the API it fronts.
|
|
17
|
+
(function () {
|
|
18
|
+
'use strict';
|
|
19
|
+
var kit = window.OTBKit;
|
|
20
|
+
var lib = window.OTBBlockersLib;
|
|
21
|
+
var API = '/api/bongos';
|
|
22
|
+
var body = document.getElementById('blockers-body');
|
|
23
|
+
var countPill = document.getElementById('blockers-count');
|
|
24
|
+
var sub = document.getElementById('blockers-sub');
|
|
25
|
+
|
|
26
|
+
var taskHref = function (id) {
|
|
27
|
+
return window.OTBTaskDetail ? window.OTBTaskDetail.href(id) : ('/task/' + encodeURIComponent(id));
|
|
28
|
+
};
|
|
29
|
+
var blockerHref = function (id) { return '#/blocker/' + encodeURIComponent(id); };
|
|
30
|
+
|
|
31
|
+
var all = [];
|
|
32
|
+
|
|
33
|
+
function rowHtml(b) {
|
|
34
|
+
var facts = lib.blockerFacts(b, {
|
|
35
|
+
titleClass: 'ov-row__title',
|
|
36
|
+
noteClass: 'ov-row__sub',
|
|
37
|
+
whenClass: 'ov-row__when',
|
|
38
|
+
blockerHref: blockerHref,
|
|
39
|
+
taskHref: taskHref,
|
|
40
|
+
});
|
|
41
|
+
return '<li class="ov-row" data-id="' + lib.esc(String(b.id)) + '">'
|
|
42
|
+
+ '<div class="ov-row__main">'
|
|
43
|
+
+ facts.titleHtml
|
|
44
|
+
+ facts.noteHtml
|
|
45
|
+
+ '<div class="ov-row__facts">' + facts.factsHtml + '</div>'
|
|
46
|
+
+ '</div>'
|
|
47
|
+
+ '</li>';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var pager = kit.makePager('blockers', 'hall-blockers-per-page', { defaultPerPage: 25 });
|
|
51
|
+
var listEl = null;
|
|
52
|
+
|
|
53
|
+
function rerender() {
|
|
54
|
+
var slice = kit.paginate(all, pager);
|
|
55
|
+
listEl.innerHTML = slice.length
|
|
56
|
+
? slice.map(rowHtml).join('')
|
|
57
|
+
: kit.emptyStateHtml('Nothing is stuck on the owner right now.');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mountBlockers(rows) {
|
|
61
|
+
all = Array.isArray(rows) ? rows : [];
|
|
62
|
+
if (countPill) { countPill.textContent = String(all.length); countPill.hidden = all.length === 0; }
|
|
63
|
+
if (sub) {
|
|
64
|
+
sub.textContent = all.length
|
|
65
|
+
? (all.length === 1 ? 'One thing' : all.length + ' things') + ' stuck on the owner — a credential, a '
|
|
66
|
+
+ 'purchase, a product call only they can make. Resolving one un-sticks every task waiting on it automatically.'
|
|
67
|
+
: 'Nothing is stuck on the owner right now.';
|
|
68
|
+
}
|
|
69
|
+
body.innerHTML = '<ul class="ov-rows" id="blockers-list"></ul>' + kit.pagerBarHtml(pager.prefix, { label: 'blockers' });
|
|
70
|
+
listEl = document.getElementById('blockers-list');
|
|
71
|
+
kit.wirePager(pager, rerender);
|
|
72
|
+
rerender();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function notice(msg) {
|
|
76
|
+
body.innerHTML = '';
|
|
77
|
+
body.className = 'note';
|
|
78
|
+
body.textContent = msg;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fetch(API + '/blockers?limit=200', { headers: { Accept: 'application/json' } })
|
|
82
|
+
.then(function (r) {
|
|
83
|
+
if (!r.ok) throw new Error('http ' + r.status);
|
|
84
|
+
return r.json();
|
|
85
|
+
})
|
|
86
|
+
.then(function (data) { mountBlockers(data && data.blockers); })
|
|
87
|
+
.catch(function () { notice('Couldn’t load the blocker queue right now.'); });
|
|
88
|
+
}());
|
|
@@ -384,6 +384,73 @@
|
|
|
384
384
|
});
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
// ---- open blockers (task 1003455, goal 1000089) ---------------------------
|
|
388
|
+
//
|
|
389
|
+
// GET /blockers is public and had ZERO hall consumers — the only blocker a
|
|
390
|
+
// builder could ever see was the #/blocker/N overlay task-detail.js already
|
|
391
|
+
// renders, reachable only by typing the id into the address bar. This slice
|
|
392
|
+
// + the /blockers page (blockers.js) are ONE code path: both render from
|
|
393
|
+
// blockers-lib.js's blockerFacts(), loaded before this file (index.html).
|
|
394
|
+
const MAX_BLOCKER_ROWS = 5;
|
|
395
|
+
|
|
396
|
+
function blockerRowHtml(b) {
|
|
397
|
+
const lib = window.OTBBlockersLib;
|
|
398
|
+
const facts = lib.blockerFacts(b, {
|
|
399
|
+
titleClass: 'ginbox__goal',
|
|
400
|
+
noteClass: 'ginbox__note',
|
|
401
|
+
whenClass: 'ginbox__sub',
|
|
402
|
+
// The SAME overlay task-detail.js has rendered since task 1003066's
|
|
403
|
+
// predecessor — this slice is what gives it its first door in.
|
|
404
|
+
blockerHref: (id) => `#/blocker/${encodeURIComponent(id)}`,
|
|
405
|
+
taskHref: (id) => (window.OTBTaskDetail ? window.OTBTaskDetail.href(id) : `/task/${encodeURIComponent(id)}`),
|
|
406
|
+
});
|
|
407
|
+
return `<li class="ginbox__item">
|
|
408
|
+
<div class="ginbox__body">
|
|
409
|
+
${facts.titleHtml}
|
|
410
|
+
${facts.factsHtml}
|
|
411
|
+
${facts.noteHtml}
|
|
412
|
+
</div>
|
|
413
|
+
</li>`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function renderBlockers(data) {
|
|
417
|
+
const box = host && host.querySelector('#attention-blockers');
|
|
418
|
+
if (!box) return;
|
|
419
|
+
const rows = (data && Array.isArray(data.blockers)) ? data.blockers : [];
|
|
420
|
+
if (!rows.length) {
|
|
421
|
+
box.innerHTML = '';
|
|
422
|
+
window.OTBAttention?.refresh();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const total = (data && data.page && Number.isFinite(data.page.total)) ? data.page.total : rows.length;
|
|
426
|
+
const more = total > rows.length
|
|
427
|
+
? `<p class="ginbox__pending">…${total - rows.length} more — <a href="/blockers">open the queue</a>.</p>`
|
|
428
|
+
: '';
|
|
429
|
+
box.innerHTML = '<h3 class="ginbox__sec-h">Stuck on the owner '
|
|
430
|
+
+ `<span class="count-pill">${total}</span></h3>`
|
|
431
|
+
+ `<ul class="ginbox">${rows.map(blockerRowHtml).join('')}</ul>${more}`;
|
|
432
|
+
window.OTBAttention?.refresh();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// task 1003448's "Home bounded reads" rule: ask the server for exactly the
|
|
436
|
+
// rows the card will show (limit=MAX_BLOCKER_ROWS) and read `page.total` back,
|
|
437
|
+
// rather than fetching every open blocker and slicing it client-side — the
|
|
438
|
+
// Next-up / triage-card bug this file's own CLAUDE.md names as the shape to
|
|
439
|
+
// avoid.
|
|
440
|
+
function loadBlockers() {
|
|
441
|
+
return getJSON(`${API}/blockers?limit=${MAX_BLOCKER_ROWS}`)
|
|
442
|
+
.then((data) => renderBlockers(data))
|
|
443
|
+
.catch((err) => {
|
|
444
|
+
const box = host && host.querySelector('#attention-blockers');
|
|
445
|
+
if (box) box.innerHTML = '';
|
|
446
|
+
window.OTBAttention?.refresh();
|
|
447
|
+
if (err.code !== 'auth-needed' && err.code !== 'not-found') {
|
|
448
|
+
console.warn('[hall] blockers feed unavailable — the "Stuck on the owner" section is empty '
|
|
449
|
+
+ 'because the request failed, NOT because nothing is open:', err);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
387
454
|
// task 2169: this widget IS the "Needs your attention" card — ONE capped
|
|
388
455
|
// summary hosting every actionable item class: goal invitations +
|
|
389
456
|
// join-requests (this file) and ideas awaiting triage (builders.js renders
|
|
@@ -417,6 +484,12 @@
|
|
|
417
484
|
// triage. Its rows carry the shared .ginbox__item class, so refresh()
|
|
418
485
|
// counts them with no change to the selector below.
|
|
419
486
|
'<div id="attention-nod"></div>' +
|
|
487
|
+
// task 1003455 (goal 1000089): open blockers — things stuck on the owner
|
|
488
|
+
// (modules/ideas/blockers.js). AFTER nod, BEFORE rot: like every slice
|
|
489
|
+
// above it, a blocker is genuinely waiting on a human, but unlike them it
|
|
490
|
+
// is project-wide rather than addressed to this viewer, so it sits with
|
|
491
|
+
// rot's standing-question tail rather than the personal asks at the top.
|
|
492
|
+
'<div id="attention-blockers"></div>' +
|
|
420
493
|
// task 1003279 (goal 1000072): work going quiet — a goal or task untouched
|
|
421
494
|
// past the project's rot timer, with the owner's four verbs on it. LAST in
|
|
422
495
|
// the card on purpose: every slice above is somebody waiting on YOU right
|
|
@@ -426,6 +499,7 @@
|
|
|
426
499
|
host.addEventListener('click', onClick);
|
|
427
500
|
load();
|
|
428
501
|
loadNodQueue();
|
|
502
|
+
loadBlockers();
|
|
429
503
|
loadRot();
|
|
430
504
|
// task 1002802 (P06): the profile slice lives in its own file (builders.js is
|
|
431
505
|
// over the size budget); this card owns its div, so it boots it here.
|
|
@@ -121,6 +121,10 @@
|
|
|
121
121
|
<script src="/builders/hall-widgets.js?v=2026-06-27-r75" defer></script>
|
|
122
122
|
<!-- BV1.R78 (task 1694): the Goals widget registers into the same registry. -->
|
|
123
123
|
<script src="/builders/goals.js?v=2026-07-01-goals" defer></script>
|
|
124
|
+
<!-- task 1003455: the shared blocker-row renderer (window.OTBBlockersLib), read by
|
|
125
|
+
both goal-inbox.js's attention-inbox slice below and the /blockers page.
|
|
126
|
+
Loaded BEFORE goal-inbox.js, which calls it while rendering. -->
|
|
127
|
+
<script src="/builders/blockers-lib.js" defer></script>
|
|
124
128
|
<!-- ADR 0112 (task 1954): the home-page goal-membership inbox — invitations to
|
|
125
129
|
accept + join-requests to approve. Self-registers; hidden when empty. -->
|
|
126
130
|
<script src="/builders/goal-inbox.js?v=2026-07-10-goal-inbox" defer></script>
|
|
@@ -165,6 +165,15 @@
|
|
|
165
165
|
// active-nav detection (#nav-work), the 'work' gate case, and the server
|
|
166
166
|
// page-gate all keep working — only the visible label and order change.
|
|
167
167
|
{ id: 'work', label: 'Tasks', icon: 'work', href: '/work', gate: 'signed' },
|
|
168
|
+
// task 1003455 (goal 1000089): the open-blocker queue — what's stuck on the
|
|
169
|
+
// owner. Lives with the project's own state (Roadmap/Goals/Tasks), not You
|
|
170
|
+
// or Community, because its subject is the project, not a person. Its one
|
|
171
|
+
// data source (GET /blockers) is deliberately public, like Roadmap's — but
|
|
172
|
+
// the NAV FLAG stays 'signed' on the roster/drachmae precedent: an ungated
|
|
173
|
+
// page can still gate cosmetically wider in the sidebar than the API it
|
|
174
|
+
// fronts, showing the link to signed-in builders while a stranger who
|
|
175
|
+
// already has the URL can still open it.
|
|
176
|
+
{ id: 'blockers', label: 'Blockers', icon: 'admin', href: '/blockers', gate: 'signed' },
|
|
168
177
|
// task 1003744 (goal 1000111): the Board Room lives HERE, in the project's
|
|
169
178
|
// own group, and gates on the atom its page gates on.
|
|
170
179
|
//
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// asked to join and when (ADR 0201 D1.3)
|
|
7
7
|
// POST /my-projects/join — apply to a project: one click plus an optional
|
|
8
8
|
// note in the applicant's own words (task 1002330).
|
|
9
|
-
// Relayed; the project executes and decides
|
|
9
|
+
// Relayed; the project executes and decides —
|
|
10
|
+
// unless the platform's record says it is unpublished.
|
|
10
11
|
// POST /my-projects/:clientId/leave — self-service offboard (reflected to the hub)
|
|
11
12
|
//
|
|
12
13
|
// All gated on a live HUB session (requireBuilder). Authority stays LOCAL: JOIN only
|
|
@@ -24,9 +25,30 @@ const api = require('../../../src/module-api');
|
|
|
24
25
|
const pi = require('../platform-identity');
|
|
25
26
|
const projectStats = require('../project-stats');
|
|
26
27
|
const applicationEcho = require('../application-echo');
|
|
28
|
+
const { projectPublishability } = require('../publish-gate');
|
|
27
29
|
|
|
28
30
|
const { pool, requireBuilder, validateOrRespond, userAgent } = api;
|
|
29
31
|
|
|
32
|
+
// Dark matter, refused before the relay (task 1003578, ADR 0247 D4). The project's own
|
|
33
|
+
// door cannot lock itself — its instance never holds its description — so the hub asks the
|
|
34
|
+
// provisioning row, the one live copy, through a port resolved by name (ADR 0083; optional,
|
|
35
|
+
// the `provisioning.projectOwnedBy` precedent). Only a POSITIVE verdict refuses. No port, no
|
|
36
|
+
// row (a self-hosted or hand-seeded project) or a failed read is no verdict, and the request
|
|
37
|
+
// is relayed as before: relaying grants nothing, and refusing on a guess would tell an
|
|
38
|
+
// applicant something untrue about someone else's project.
|
|
39
|
+
const PUBLISH_IDENTITY_PORT = 'provisioning.projectPublishIdentity';
|
|
40
|
+
async function isDarkMatter(client) {
|
|
41
|
+
const identityOf = api.resolveOptional(PUBLISH_IDENTITY_PORT);
|
|
42
|
+
if (typeof identityOf !== 'function') return false;
|
|
43
|
+
try {
|
|
44
|
+
const identity = await identityOf({ slug: client.client_id, host: new URL(client.origin).hostname });
|
|
45
|
+
return !!identity && projectPublishability(identity).publishable === false;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
api.logger('platform-identity').warn(`publish verdict read failed, relaying (non-fatal): ${e && e.message}`);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
// joinRelayOutcome — what the project's own admission surface answered, said in
|
|
31
53
|
// the HUB's words (task 1003044). The project decides; the hub relays and must
|
|
32
54
|
// never paint a refusal as "requested" — nor as "unreachable". A 2xx is a filed
|
|
@@ -172,6 +194,10 @@ module.exports = function myProjectsRoutes() {
|
|
|
172
194
|
if (!client || client.status !== 'active' || !client.origin) {
|
|
173
195
|
return res.fail('unknown_project', 404);
|
|
174
196
|
}
|
|
197
|
+
if (await isDarkMatter(client)) {
|
|
198
|
+
return res.json({ ok: true, status: 'not_published', project: { name: client.name, origin: client.origin },
|
|
199
|
+
message: RELAY_SENTENCE.not_published });
|
|
200
|
+
}
|
|
175
201
|
// Relay an access request to the project's OWN (public) admission endpoint —
|
|
176
202
|
// the project decides (open-join, or request/invite per ADR 0112). We post to
|
|
177
203
|
// the registry-trusted origin, never the caller's raw input.
|
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
"migrations": true,
|
|
15
15
|
"pollers": ["liveness-sweep", "catalog-backfill"]
|
|
16
16
|
},
|
|
17
|
-
"provides": ["provisioning.instanceState", "provisioning.projectPlanets", "provisioning.projectOwnedBy"],
|
|
17
|
+
"provides": ["provisioning.instanceState", "provisioning.projectPlanets", "provisioning.projectOwnedBy", "provisioning.projectPublishIdentity"],
|
|
18
18
|
"consumes": []
|
|
19
19
|
}
|
|
@@ -58,6 +58,8 @@ const capacity = require('../capacity'); // the box's slot ceiling (ADR 0285,
|
|
|
58
58
|
const { callbackPage } = require('./callback-page');
|
|
59
59
|
// the LOUD body validators (task 1003504) — lifted out when this file hit the size ratchet
|
|
60
60
|
const { badProjectDetail, badModuleSelection, badProjectType } = require('./body-validators');
|
|
61
|
+
// the kernel ports this module provides (moved out at the size ratchet, task 1003578)
|
|
62
|
+
const { registerProvisioningSeams } = require('../seams');
|
|
61
63
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
62
64
|
const log = api.logger('provisioning');
|
|
63
65
|
|
|
@@ -110,28 +112,6 @@ function ownerFromTargetRef(ref) {
|
|
|
110
112
|
return m ? m[1] : null;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
// Register this module's kernel seam (ADR 0083 / the registerBoxSeams precedent).
|
|
114
|
-
// Idempotent — the loader calls this factory once at boot, but tests may
|
|
115
|
-
// instantiate the router repeatedly; gating on hasProvider keeps the port
|
|
116
|
-
// single-registered.
|
|
117
|
-
// • PROVIDES port `provisioning.instanceState` (slug) → { status, … } | null.
|
|
118
|
-
// Lets other code read an instance's provisioning status via
|
|
119
|
-
// resolveOptional without importing this module (box.hasEverConnected pattern).
|
|
120
|
-
// • PROVIDES port `provisioning.projectPlanets` (hosts[]) → { host: { planet_template,
|
|
121
|
-
// planet_accent } } — the owners' planet choices (task 1003347), so the hub's
|
|
122
|
-
// public featured feed can carry them without importing this module.
|
|
123
|
-
// • PROVIDES port `provisioning.projectOwnedBy` ({ slug, host, builderId }) → boolean —
|
|
124
|
-
// the OWNERSHIP fact behind a hub catalog row (task 1003580), so a hub surface can
|
|
125
|
-
// own-scope a project without importing this module and without treating the hub's
|
|
126
|
-
// `owner_github_id` columns as authority (ADR 0238). See projectOwnedBy for why the
|
|
127
|
-
// question names both halves of the ADR 0246 projection.
|
|
128
|
-
function registerProvisioningSeams() {
|
|
129
|
-
if (api.hasProvider('provisioning.instanceState')) return;
|
|
130
|
-
api.registerProvider('provisioning.instanceState', (slug) => provisioning.instanceStateForSlug(pool, slug));
|
|
131
|
-
api.registerProvider('provisioning.projectPlanets', (hosts) => provisioning.planetChoicesForHosts(pool, hosts));
|
|
132
|
-
api.registerProvider('provisioning.projectOwnedBy', (target) => provisioning.projectOwnedBy(pool, target));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
115
|
// Is this project on the public map, and if not, what is it still missing?
|
|
136
116
|
// (R16, task 1002335 — the surface R15 pointed at by name.)
|
|
137
117
|
//
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// modules/provisioning/seams.js — the kernel ports this module provides (ADR 0083 / the
|
|
2
|
+
// registerBoxSeams precedent). Moved out of routes/provisioning.js under task 1003578,
|
|
3
|
+
// because that file sits at the 1500-line fitness ratchet and a new port needed a line.
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const api = require('../../src/module-api');
|
|
7
|
+
const { pool } = api;
|
|
8
|
+
const provisioning = require('./provisioning');
|
|
9
|
+
|
|
10
|
+
// The `provisioning.projectPublishIdentity` port (task 1003578, ADR 0247 D4): the name and
|
|
11
|
+
// description the publish gate judges, for the control-plane project a hub catalog row was
|
|
12
|
+
// projected from. This row is the one LIVE copy of a control-plane project's description;
|
|
13
|
+
// the hub's catalog column is a cache (ADR 0182 D5), so the hub's join relay asks here.
|
|
14
|
+
//
|
|
15
|
+
// Keyed on BOTH halves of the ADR 0246 projection (`client_id = slug` at `https://<domain>`),
|
|
16
|
+
// the `projectOwnedBy` rule: a hand-seeded catalog row's name is squattable in this table,
|
|
17
|
+
// and a stranger's instance must not answer for it. The slug IS the name on this plane, so
|
|
18
|
+
// the mapping stays here and the rule stays in the hub. `null` = no row = NO verdict.
|
|
19
|
+
async function projectPublishIdentity(db, { slug, host } = {}) {
|
|
20
|
+
if (typeof slug !== 'string' || !slug) return null;
|
|
21
|
+
const { rows } = await db.query(
|
|
22
|
+
`SELECT slug, description FROM provisioning_instances
|
|
23
|
+
WHERE slug = $1 AND lower(domain) = $2
|
|
24
|
+
LIMIT 1`,
|
|
25
|
+
[slug, typeof host === 'string' ? host.toLowerCase() : ''],
|
|
26
|
+
);
|
|
27
|
+
return rows[0] ? { name: rows[0].slug, description: rows[0].description } : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Idempotent — the loader calls the routes factory once at boot, but tests may
|
|
31
|
+
// instantiate the router repeatedly; gating on hasProvider keeps each port
|
|
32
|
+
// single-registered.
|
|
33
|
+
// • PROVIDES port `provisioning.instanceState` (slug) → { status, … } | null.
|
|
34
|
+
// Lets other code read an instance's provisioning status via
|
|
35
|
+
// resolveOptional without importing this module (box.hasEverConnected pattern).
|
|
36
|
+
// • PROVIDES port `provisioning.projectPlanets` (hosts[]) → { host: { planet_template,
|
|
37
|
+
// planet_accent } } — the owners' planet choices (task 1003347), so the hub's
|
|
38
|
+
// public featured feed can carry them without importing this module.
|
|
39
|
+
// • PROVIDES port `provisioning.projectOwnedBy` ({ slug, host, builderId }) → boolean —
|
|
40
|
+
// the OWNERSHIP fact behind a hub catalog row (task 1003580), so a hub surface can
|
|
41
|
+
// own-scope a project without importing this module and without treating the hub's
|
|
42
|
+
// `owner_github_id` columns as authority (ADR 0238). See projectOwnedBy for why the
|
|
43
|
+
// question names both halves of the ADR 0246 projection.
|
|
44
|
+
// • PROVIDES port `provisioning.projectPublishIdentity` ({ slug, host }) → { name,
|
|
45
|
+
// description } | null — see projectPublishIdentity above.
|
|
46
|
+
function registerProvisioningSeams() {
|
|
47
|
+
if (api.hasProvider('provisioning.instanceState')) return;
|
|
48
|
+
api.registerProvider('provisioning.instanceState', (slug) => provisioning.instanceStateForSlug(pool, slug));
|
|
49
|
+
api.registerProvider('provisioning.projectPlanets', (hosts) => provisioning.planetChoicesForHosts(pool, hosts));
|
|
50
|
+
api.registerProvider('provisioning.projectOwnedBy', (target) => provisioning.projectOwnedBy(pool, target));
|
|
51
|
+
api.registerProvider('provisioning.projectPublishIdentity', (target) => projectPublishIdentity(pool, target));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { registerProvisioningSeams, projectPublishIdentity };
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.828",
|
|
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.828",
|
|
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.828",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.828'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|