@bongos/core 1.19.106 → 1.19.107
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 +11 -11
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +3 -1
- package/public-landing/projects.html +169 -22
- package/src/module-api.js +3 -1
- package/tests/projects_hub.mjs +94 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.107",
|
|
6
|
+
"core_contract": "1.19.107",
|
|
7
|
+
"source_commit": "85d496250f3a30925cf27a4601462f516e6de778",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-14T02:08:28.305Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 301,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1558,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "61133ba38baa892b82011a8424092f2ccd2d26b485d9d8043d9c20d0d3abae17",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4147,12 +4147,12 @@
|
|
|
4147
4147
|
{
|
|
4148
4148
|
"path": "package-lock.json",
|
|
4149
4149
|
"mode": "0000644",
|
|
4150
|
-
"sha256": "
|
|
4150
|
+
"sha256": "bd7f6faf78443413a2f0142bd9f72e4fff1eb31fefd2225697f0011599f1a218"
|
|
4151
4151
|
},
|
|
4152
4152
|
{
|
|
4153
4153
|
"path": "package.json",
|
|
4154
4154
|
"mode": "0000644",
|
|
4155
|
-
"sha256": "
|
|
4155
|
+
"sha256": "d14fe248ed9e458a9ebc3a9c7c75101082f44861712860800f341d6218ccb61f"
|
|
4156
4156
|
},
|
|
4157
4157
|
{
|
|
4158
4158
|
"path": "public-docs/index.html",
|
|
@@ -4392,7 +4392,7 @@
|
|
|
4392
4392
|
{
|
|
4393
4393
|
"path": "public-landing/assets/cosmos.css",
|
|
4394
4394
|
"mode": "0000644",
|
|
4395
|
-
"sha256": "
|
|
4395
|
+
"sha256": "e5bfd01db8cca1cb28c2d5c2382a401a2ee882cb1a219ed5553d578f2e3952b6"
|
|
4396
4396
|
},
|
|
4397
4397
|
{
|
|
4398
4398
|
"path": "public-landing/buddha.png",
|
|
@@ -4412,7 +4412,7 @@
|
|
|
4412
4412
|
{
|
|
4413
4413
|
"path": "public-landing/projects.html",
|
|
4414
4414
|
"mode": "0000644",
|
|
4415
|
-
"sha256": "
|
|
4415
|
+
"sha256": "e6e951af6fc202a2d90accef69e32da7d8a3456b80a6fcdbce469274edb49bce"
|
|
4416
4416
|
},
|
|
4417
4417
|
{
|
|
4418
4418
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -5727,7 +5727,7 @@
|
|
|
5727
5727
|
{
|
|
5728
5728
|
"path": "src/module-api.js",
|
|
5729
5729
|
"mode": "0000644",
|
|
5730
|
-
"sha256": "
|
|
5730
|
+
"sha256": "8fb4486e1a47e0c244cec140b06db1175ce7c3b1418b7dd27d71073020a189ca"
|
|
5731
5731
|
},
|
|
5732
5732
|
{
|
|
5733
5733
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7252,7 +7252,7 @@
|
|
|
7252
7252
|
{
|
|
7253
7253
|
"path": "tests/projects_hub.mjs",
|
|
7254
7254
|
"mode": "0000644",
|
|
7255
|
-
"sha256": "
|
|
7255
|
+
"sha256": "6ddec55050ca5864670d02caad6ede76e5e0753afb1e5636036b75d4185e9427"
|
|
7256
7256
|
},
|
|
7257
7257
|
{
|
|
7258
7258
|
"path": "tests/provision.mjs",
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.107",
|
|
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.107",
|
|
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.107",
|
|
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",
|
|
@@ -259,6 +259,8 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
259
259
|
.projects{list-style:none;display:flex;flex-direction:column;gap:18px;}
|
|
260
260
|
.proj{border:2px solid var(--line);border-radius:18px;padding:18px 20px;background:var(--bg-card);display:flex;gap:16px;align-items:flex-start;}
|
|
261
261
|
.proj.gone{opacity:0.78;}
|
|
262
|
+
/* click-to-manage landing glow: the sky's owned orb hands focus to this card */
|
|
263
|
+
.proj.anchored{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 30%,transparent);}
|
|
262
264
|
.projBody{flex:1;min-width:0;}
|
|
263
265
|
.projHead{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
|
|
264
266
|
.projName{font-family:var(--font-display);font-weight:700;font-size:19px;letter-spacing:-0.3px;}
|
|
@@ -503,7 +505,7 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
503
505
|
border:1px solid color-mix(in srgb,var(--accent) 45%,transparent);}
|
|
504
506
|
:root[data-mode="dark"] .worldOrb[data-rel="builder"] .badge{color:var(--sun-lit);}
|
|
505
507
|
@media (prefers-color-scheme: dark){:root:not([data-mode="light"]) .worldOrb[data-rel="builder"] .badge{color:var(--sun-lit);}}
|
|
506
|
-
.worldOrb .chip{transform:scale(.88);}
|
|
508
|
+
.worldOrb .chip{transform:scale(.88);white-space:nowrap;}
|
|
507
509
|
|
|
508
510
|
/* what the only-mine filter shows when you are connected to nothing. Not a
|
|
509
511
|
hypothetical — GET /my-projects returns { owned: [], building: [] } for the
|
|
@@ -453,6 +453,8 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
453
453
|
/* the mine view is the only one that polls; it re-reads on entry so a
|
|
454
454
|
card that changed while you were on the map is not stale */
|
|
455
455
|
if (v === 'mine' && me) refreshList();
|
|
456
|
+
/* owned chips refresh on re-entry, never on a timer — C8 keeps the map quiet */
|
|
457
|
+
if (v === 'map' && me) loadOwnedInstances().then(applyMapFilter);
|
|
456
458
|
/* focus the name field only on a deliberate move into the view — never on
|
|
457
459
|
boot, where it would steal focus from whatever the visitor arrived at */
|
|
458
460
|
if (v === 'new' && focus) $('newName').focus();
|
|
@@ -528,6 +530,8 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
528
530
|
var ORB_GLOWS = ORB_ACCENTS.map(function (t) { return 'color-mix(in srgb,' + t + ' 45%,transparent)'; });
|
|
529
531
|
var catalog = [];
|
|
530
532
|
var mineOrigins = {}; /* normalized host → 'owner' | 'builder' */
|
|
533
|
+
var ownedInstances = []; /* the viewer's OWN provisioning rows (never torn_down) */
|
|
534
|
+
var ownedByHost = {}; /* lower(domain) → instance row — the dedupe join */
|
|
531
535
|
/* What the platform's own entry is FOR SEARCH. It is deliberately not pushed
|
|
532
536
|
into `catalog`: the catalog is the federated directory of other instances,
|
|
533
537
|
and a synthetic member of it would flow into accent assignment, orb slots
|
|
@@ -562,7 +566,24 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
562
566
|
.forEach(function (p, i) { p._acc = ORB_ACCENTS[i % ORB_ACCENTS.length]; });
|
|
563
567
|
}
|
|
564
568
|
function accentOf(p) { return p._acc || ORB_ACCENTS[0]; }
|
|
565
|
-
|
|
569
|
+
/* the manage cards' accent rule — one project, one colour, in the sky AND
|
|
570
|
+
the list. Shared with orbFor(), which used to carry this hash inline. */
|
|
571
|
+
function slugAccent(slug) {
|
|
572
|
+
var h = 0, s = String(slug || '');
|
|
573
|
+
for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
|
574
|
+
return h % ORB_ACCENTS.length;
|
|
575
|
+
}
|
|
576
|
+
function relOf(p) {
|
|
577
|
+
/* an instance the viewer provisioned is a server-resolved authority too —
|
|
578
|
+
own-scoped by owner_builder_id, no client inference (direction §2
|
|
579
|
+
principle 5). It outranks a membership row: owning the instance is the
|
|
580
|
+
stronger claim. The _inst annotation covers owned entries with no
|
|
581
|
+
domain yet; ownedByHost covers any un-annotated caller. */
|
|
582
|
+
if (p._inst) return 'owner';
|
|
583
|
+
var h = hostOf(p.origin);
|
|
584
|
+
if (ownedByHost[h]) return 'owner';
|
|
585
|
+
return mineOrigins[h] || 'none';
|
|
586
|
+
}
|
|
566
587
|
/* The platform's own relationship cannot come from mineOrigins, and not
|
|
567
588
|
because the data is merely missing: platform_identity_project_memberships
|
|
568
589
|
is written ONLY by the server-to-server check-in a FEDERATED instance posts
|
|
@@ -594,7 +615,11 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
594
615
|
}
|
|
595
616
|
function matchesMapQuery(p, q) {
|
|
596
617
|
if (!q) return true;
|
|
597
|
-
|
|
618
|
+
/* an owned entry also answers by slug and address — the same fields the
|
|
619
|
+
manage view's search reads, so the habit learned there works here */
|
|
620
|
+
var hay = String(p.name || '') + ' ' + String(p.tagline || '') +
|
|
621
|
+
(p._inst ? ' ' + String(p._inst.slug || '') + ' ' + String(p._inst.domain || '') : '');
|
|
622
|
+
return hay.toLowerCase().indexOf(q) !== -1;
|
|
598
623
|
}
|
|
599
624
|
|
|
600
625
|
function renderOrbs(list) {
|
|
@@ -608,24 +633,39 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
608
633
|
var a = document.createElement('a');
|
|
609
634
|
a.className = 'worldOrb';
|
|
610
635
|
a.setAttribute('data-generated', '');
|
|
611
|
-
|
|
636
|
+
var rel = relOf(p);
|
|
637
|
+
a.setAttribute('data-rel', rel);
|
|
638
|
+
var inst = p._inst;
|
|
612
639
|
var href = safeHttpUrl(p.origin);
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
640
|
+
if (inst) {
|
|
641
|
+
/* an OWNED entry — catalog-annotated or owned-only, one code path.
|
|
642
|
+
Its job is MANAGE (direction §5): same tab, anchored on that
|
|
643
|
+
project. The public-site door still exists on the manage card; a
|
|
644
|
+
middle-click lands a fresh tab on the manage view, which is this
|
|
645
|
+
orb's honest destination. */
|
|
646
|
+
a.href = '?view=mine';
|
|
647
|
+
a.setAttribute('data-manage', String(inst.id));
|
|
648
|
+
} else {
|
|
649
|
+
/* The href STAYS, and stays honest, even though a plain click now opens
|
|
650
|
+
the card instead of following it: it is what makes the orb a real link
|
|
651
|
+
— focusable, Enter-activatable, and still ctrl/middle-clickable into a
|
|
652
|
+
new tab. The card is layered over that, not substituted for it. */
|
|
653
|
+
a.href = href || '#';
|
|
654
|
+
if (href) { a.target = '_blank'; a.rel = 'noopener'; }
|
|
655
|
+
}
|
|
619
656
|
a.setAttribute('data-origin', String(p.origin || ''));
|
|
620
657
|
a.title = p.tagline || p.name || '';
|
|
621
658
|
a.style.cssText = '--acc:' + accentOf(p) +
|
|
622
659
|
';--d:' + slot[2] + 'px;left:' + slot[0] + '%;top:' + slot[1] + '%';
|
|
623
|
-
var rel = relOf(p);
|
|
624
660
|
a.innerHTML =
|
|
625
661
|
(rel === 'owner' ? '<span class="badge">yours</span>' :
|
|
626
662
|
rel === 'builder' ? '<span class="badge">you build here</span>' : '') +
|
|
627
663
|
'<span class="disc"></span>' +
|
|
628
|
-
'<span class="nm">' + esc(p.name) + '</span>'
|
|
664
|
+
'<span class="nm">' + esc(p.name) + '</span>' +
|
|
665
|
+
/* the ONE chip site, deferring to the manage list's vocabulary — so a
|
|
666
|
+
project can never look alive in the sky and unreachable in the list
|
|
667
|
+
(C6: two views of one truth) */
|
|
668
|
+
(inst ? chipFor(inst) : '');
|
|
629
669
|
field.insertBefore(a, hint);
|
|
630
670
|
});
|
|
631
671
|
}
|
|
@@ -641,7 +681,8 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
641
681
|
var rel = relOf(p);
|
|
642
682
|
var pill = rel === 'owner' ? ' <span class="pill">yours</span>'
|
|
643
683
|
: rel === 'builder' ? ' <span class="pill">you build here</span>' : '';
|
|
644
|
-
return '<tr data-generated data-
|
|
684
|
+
return '<tr data-generated ' + (p._inst ? 'data-manage="' + esc(String(p._inst.id)) + '" ' : '') +
|
|
685
|
+
'data-origin="' + esc(String(p.origin || '')) + '">' +
|
|
645
686
|
'<td class="colName"><span class="pname"><span class="pdot" style="--acc:' +
|
|
646
687
|
accentOf(p) + '"></span>' +
|
|
647
688
|
'<span><span class="ptitle">' + esc(p.name) + '</span>' + pill + '<br>' +
|
|
@@ -653,10 +694,13 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
653
694
|
"we could not ask". */
|
|
654
695
|
'<td class="right num">' + (p.builders == null ? '—' : esc(String(p.builders))) + '</td>' +
|
|
655
696
|
'<td class="colTags">' + tagsHtml(p.tags) + '</td>' +
|
|
656
|
-
/* liveness on a
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
697
|
+
/* liveness on a CATALOG row is NOT probed by this surface — a chip
|
|
698
|
+
saying "live" there would claim more than anything verified (C6).
|
|
699
|
+
An OWNED row is different: the own-scoped feed answers for it, so
|
|
700
|
+
it carries chipFor()'s truth, the same chip its orb and its manage
|
|
701
|
+
card show (task 1002744). */
|
|
702
|
+
'<td class="right colStatus">' + (p._inst ? chipFor(p._inst)
|
|
703
|
+
: '<span class="chip calm"><span class="cdot"></span>—</span>') + '</td>' +
|
|
660
704
|
'<td class="colGo">' + (safeHttpUrl(p.origin)
|
|
661
705
|
? '<a class="btn small ghost" href="' + esc(safeHttpUrl(p.origin)) + '" target="_blank" rel="noopener">Open →</a>'
|
|
662
706
|
: '') + '</td>' +
|
|
@@ -667,7 +711,30 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
667
711
|
function applyMapFilter() {
|
|
668
712
|
var onlyMine = $('onlyMine').checked;
|
|
669
713
|
var q = $('mapQ').value.trim().toLowerCase();
|
|
670
|
-
|
|
714
|
+
/* ONE browse list feeds both sub-views (task 1002744): every catalog row
|
|
715
|
+
is annotated with the viewer's own instance when the hosts join — so a
|
|
716
|
+
project both featured and owned renders once, marked owned — and owned
|
|
717
|
+
instances the catalog does not carry are appended as first-class
|
|
718
|
+
entries. The sky, the table, the search box and the count read the same
|
|
719
|
+
list and can never disagree about what exists. */
|
|
720
|
+
var catalogHosts = {};
|
|
721
|
+
catalog.forEach(function (p) {
|
|
722
|
+
var h = hostOf(p.origin);
|
|
723
|
+
catalogHosts[h] = 1;
|
|
724
|
+
p._inst = ownedByHost[h] || null;
|
|
725
|
+
});
|
|
726
|
+
var browse = catalog.slice();
|
|
727
|
+
ownedInstances.forEach(function (inst) {
|
|
728
|
+
var h = inst.domain ? hostOf(inst.domain) : '';
|
|
729
|
+
if (h && catalogHosts[h]) return; /* annotated onto its catalog row above */
|
|
730
|
+
browse.push({
|
|
731
|
+
name: inst.slug, tagline: inst.domain || '',
|
|
732
|
+
origin: inst.domain ? 'https://' + inst.domain : '',
|
|
733
|
+
builders: null, tags: null,
|
|
734
|
+
_inst: inst, _acc: ORB_ACCENTS[slugAccent(inst.slug)]
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
var list = browse.filter(function (p) {
|
|
671
738
|
return matchesMapQuery(p, q) && (!onlyMine || relOf(p) !== 'none');
|
|
672
739
|
});
|
|
673
740
|
list.sort(function (x, y) {
|
|
@@ -777,16 +844,44 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
777
844
|
if (!a || !isPlainClick(ev)) return;
|
|
778
845
|
ev.preventDefault();
|
|
779
846
|
if (a.id === 'selfOrb') return openCard(SELF_PROJECT, selfRel(), a);
|
|
847
|
+
/* an owned orb's job is MANAGE (direction §5) — before the card path */
|
|
848
|
+
if (a.hasAttribute('data-manage')) return goToManage(a.getAttribute('data-manage'));
|
|
780
849
|
var p = projByOrigin(a.getAttribute('data-origin') || '');
|
|
781
850
|
if (p) openCard(p, relOf(p), a);
|
|
782
851
|
});
|
|
783
852
|
|
|
853
|
+
/* ── Click-to-manage (task 1002744): an owned orb opens YOUR side of the hub,
|
|
854
|
+
anchored on that project's card. The anchor is deferred — setView('mine')
|
|
855
|
+
triggers an async refreshList(), so the target `[data-inst]` node may not
|
|
856
|
+
exist yet; render() and refreshList() retry tryAnchor() once it does. ── */
|
|
857
|
+
var ANCHOR_GLOW_MS = 2200;
|
|
858
|
+
var pendingAnchor = null;
|
|
859
|
+
function goToManage(instId) {
|
|
860
|
+
pendingAnchor = String(instId || '');
|
|
861
|
+
setView('mine');
|
|
862
|
+
tryAnchor(); /* the list may already be rendered from an earlier visit */
|
|
863
|
+
}
|
|
864
|
+
function tryAnchor() {
|
|
865
|
+
if (!pendingAnchor) return;
|
|
866
|
+
var li = document.querySelector('li[data-inst="' + pendingAnchor + '"]');
|
|
867
|
+
if (!li) return;
|
|
868
|
+
pendingAnchor = null;
|
|
869
|
+
/* a search or state filter that hides the target would turn the anchor
|
|
870
|
+
into a silent no-op — clear them the same way the visitor would */
|
|
871
|
+
if (li.hidden) $('mineClear').click();
|
|
872
|
+
li.scrollIntoView({ block: 'center' });
|
|
873
|
+
li.classList.add('anchored');
|
|
874
|
+
setTimeout(function () { li.classList.remove('anchored'); }, ANCHOR_GLOW_MS);
|
|
875
|
+
}
|
|
876
|
+
|
|
784
877
|
$('mapRows').addEventListener('click', function (ev) {
|
|
785
878
|
/* the row's own "Open →" keeps its job — the card is for the rest of the row */
|
|
786
879
|
if (ev.target.closest && ev.target.closest('a')) return;
|
|
787
880
|
var tr = ev.target.closest && ev.target.closest('tr');
|
|
788
881
|
if (!tr || !isPlainClick(ev)) return;
|
|
789
882
|
if (tr.id === 'selfRow') return openCard(SELF_PROJECT, selfRel(), null);
|
|
883
|
+
/* an owned row goes to MANAGE, exactly like its orb — two views, one job */
|
|
884
|
+
if (tr.hasAttribute('data-manage')) return goToManage(tr.getAttribute('data-manage'));
|
|
790
885
|
var p = projByOrigin(tr.getAttribute('data-origin') || '');
|
|
791
886
|
if (p) openCard(p, relOf(p), null);
|
|
792
887
|
});
|
|
@@ -846,6 +941,29 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
846
941
|
.catch(function () { mineOrigins = {}; });
|
|
847
942
|
}
|
|
848
943
|
|
|
944
|
+
/* The owner-scoped half of the sky (task 1002744). The public catalog
|
|
945
|
+
cannot answer "is this mine" — /projects/featured is rank:public and
|
|
946
|
+
varies by nobody — so the map merges the viewer's OWN provisioning rows:
|
|
947
|
+
GET /provisioning/instances is own-scoped by construction and carries the
|
|
948
|
+
same last_verified_* overlay the manage list renders. Signed out (401) and
|
|
949
|
+
a child instance with provisioning off (404) both read as "no owned
|
|
950
|
+
projects here", never as an error. */
|
|
951
|
+
function loadOwnedInstances() {
|
|
952
|
+
if (!me) { ownedInstances = []; ownedByHost = {}; return Promise.resolve(); }
|
|
953
|
+
return fetch(API + '/provisioning/instances', { credentials: 'same-origin' })
|
|
954
|
+
.then(function (res) { return res.ok ? res.json() : { instances: [] }; })
|
|
955
|
+
.then(function (d) {
|
|
956
|
+
/* a torn-down project is filed under "gone" in the manage list — the
|
|
957
|
+
sky does not float what no longer exists */
|
|
958
|
+
ownedInstances = (d.instances || []).filter(function (inst) { return inst.status !== 'torn_down'; });
|
|
959
|
+
ownedByHost = {};
|
|
960
|
+
ownedInstances.forEach(function (inst) {
|
|
961
|
+
if (inst.domain) ownedByHost[hostOf(inst.domain)] = inst;
|
|
962
|
+
});
|
|
963
|
+
})
|
|
964
|
+
.catch(function () { ownedInstances = []; ownedByHost = {}; });
|
|
965
|
+
}
|
|
966
|
+
|
|
849
967
|
$('onlyMine').addEventListener('change', applyMapFilter);
|
|
850
968
|
$('mapQ').addEventListener('input', applyMapFilter);
|
|
851
969
|
document.querySelectorAll('.tbl th[data-sort]').forEach(function (th) {
|
|
@@ -1043,9 +1161,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
1043
1161
|
/* ── the orb: the gate's project sphere, glow keyed to verification ────── */
|
|
1044
1162
|
|
|
1045
1163
|
function orbFor(inst) {
|
|
1046
|
-
var
|
|
1047
|
-
for (var i = 0; i < slug.length; i++) h = (h * 31 + slug.charCodeAt(i)) >>> 0;
|
|
1048
|
-
var k = h % ORB_ACCENTS.length;
|
|
1164
|
+
var k = slugAccent(inst.slug);
|
|
1049
1165
|
var cls = 'orb';
|
|
1050
1166
|
if (inst.status === 'torn_down' || inst.status === 'tearing_down') cls += ' ghost';
|
|
1051
1167
|
else if (inst.status === 'error' || (inst.status === 'active' && inst.last_verified_up === false)) cls += ' ashen';
|
|
@@ -1215,6 +1331,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
1215
1331
|
});
|
|
1216
1332
|
recountMine();
|
|
1217
1333
|
applyMine();
|
|
1334
|
+
tryAnchor(); /* a click-to-manage jump may be waiting on this render */
|
|
1218
1335
|
}
|
|
1219
1336
|
|
|
1220
1337
|
function msg(id, text, cls) {
|
|
@@ -1304,6 +1421,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
1304
1421
|
var sameSet = instances.length === known && instances.every(function (i) { return byId[i.id]; });
|
|
1305
1422
|
if (!sameSet) { render(instances); return; }
|
|
1306
1423
|
instances.forEach(function (inst) { takeUpdate(inst.id, inst); });
|
|
1424
|
+
tryAnchor(); /* same-set refresh renders no shells — retry here too */
|
|
1307
1425
|
})
|
|
1308
1426
|
.catch(function () { /* transient — next tick will try again */ });
|
|
1309
1427
|
}
|
|
@@ -1312,7 +1430,36 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
1312
1430
|
while you browsed the sky would be spending requests on nothing */
|
|
1313
1431
|
function shouldPoll() { return !document.hidden && me && currentView === 'mine'; }
|
|
1314
1432
|
setInterval(function () { if (shouldPoll()) refreshList(); }, REFRESH_MS);
|
|
1315
|
-
document.addEventListener('visibilitychange', function () {
|
|
1433
|
+
document.addEventListener('visibilitychange', function () {
|
|
1434
|
+
if (shouldPoll()) refreshList();
|
|
1435
|
+
/* returning to a MAP tab re-reads the owned overlay once — event-driven,
|
|
1436
|
+
like the mine view's entry refresh, not a timer */
|
|
1437
|
+
if (!document.hidden && me && currentView === 'map') loadOwnedInstances().then(applyMapFilter);
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
/* C6 freshness without C8's cost: once a minute the visible MAP refreshes
|
|
1441
|
+
its owned chips from data already in hand — ZERO requests — so "live ·
|
|
1442
|
+
checked 2m ago" keeps telling the truth and downgrades to live? once the
|
|
1443
|
+
probe ages past FRESH_MS. Chips only: rebuilding every orb and row to
|
|
1444
|
+
re-date a label would be pointless DOM churn. The re-FETCH happens only
|
|
1445
|
+
on view entry and tab return, never on this timer. */
|
|
1446
|
+
function refreshOwnedChips() {
|
|
1447
|
+
var byManageId = {};
|
|
1448
|
+
ownedInstances.forEach(function (inst) { byManageId[String(inst.id)] = inst; });
|
|
1449
|
+
document.querySelectorAll('#view-map [data-manage]').forEach(function (el) {
|
|
1450
|
+
var inst = byManageId[el.getAttribute('data-manage')];
|
|
1451
|
+
var chip = inst && el.querySelector('.chip');
|
|
1452
|
+
if (!chip) return;
|
|
1453
|
+
var wrap = document.createElement('span');
|
|
1454
|
+
wrap.innerHTML = chipFor(inst);
|
|
1455
|
+
chip.replaceWith(wrap.firstChild);
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
function mapChipTick() {
|
|
1459
|
+
if (document.hidden || currentView !== 'map' || !ownedInstances.length) return;
|
|
1460
|
+
refreshOwnedChips();
|
|
1461
|
+
}
|
|
1462
|
+
setInterval(mapChipTick, REFRESH_MS);
|
|
1316
1463
|
|
|
1317
1464
|
/* ── teardown: ask, confirm (the truth, itemized — direction §5.4), then
|
|
1318
1465
|
SAY exactly what the server said ── */
|
|
@@ -1509,7 +1656,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
1509
1656
|
$('gate').hidden = true;
|
|
1510
1657
|
$('mine').hidden = false;
|
|
1511
1658
|
load();
|
|
1512
|
-
return loadMemberships();
|
|
1659
|
+
return Promise.all([loadMemberships(), loadOwnedInstances()]);
|
|
1513
1660
|
})
|
|
1514
1661
|
.catch(function (err) {
|
|
1515
1662
|
me = null;
|
package/src/module-api.js
CHANGED
|
@@ -639,8 +639,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
639
639
|
// landed since 1.19.104 with no explicit bump. run 31758526405. (task 1002620)
|
|
640
640
|
// 1.19.106 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
641
641
|
// landed since 1.19.105 with no explicit bump. run 31759315328. (task 1002620)
|
|
642
|
+
// 1.19.107 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
643
|
+
// landed since 1.19.106 with no explicit bump. run 31762781737. (task 1002620)
|
|
642
644
|
// ---------------------------------------------------------------------------
|
|
643
|
-
const CORE_VERSION = '1.19.
|
|
645
|
+
const CORE_VERSION = '1.19.107'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
644
646
|
|
|
645
647
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
646
648
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -269,6 +269,8 @@ test('the self entry is not pushed into the catalog array', () => {
|
|
|
269
269
|
test('the count includes the platform when it is shown', () => {
|
|
270
270
|
assert.doesNotMatch(SCRIPT, /mapCount'\)\.textContent = list\.length/,
|
|
271
271
|
'the count must not ignore the self entry it is rendering beside');
|
|
272
|
+
// owned entries live INSIDE `list` (the merged browse list) — a separate
|
|
273
|
+
// owned term here meant the table under-rendered what the count claimed
|
|
272
274
|
assert.match(SCRIPT, /var shown = list\.length \+ \(selfShown \? 1 : 0\)/);
|
|
273
275
|
});
|
|
274
276
|
|
|
@@ -541,3 +543,95 @@ test('no anchor in the table ships a placeholder href', () => {
|
|
|
541
543
|
const generated = (SCRIPT.match(/return '<tr data-generated([\s\S]*?)'<\/tr>';/) || [, ''])[1];
|
|
542
544
|
assert.doesNotMatch(generated, /href="#"/, 'generated rows must not ship placeholders either');
|
|
543
545
|
});
|
|
546
|
+
|
|
547
|
+
/* ── The owner-scoped sky (task 1002744) ────────────────────────────────────
|
|
548
|
+
/projects/featured is rank:public, takes _req, and structurally cannot say
|
|
549
|
+
"this one is yours". The sky's owner half comes from the viewer's OWN
|
|
550
|
+
provisioning rows instead: GET /provisioning/instances is own-scoped by
|
|
551
|
+
construction (owner_builder_id = the session's builder) and carries the same
|
|
552
|
+
last_verified_* overlay the manage list renders — one truth, two views (C6).
|
|
553
|
+
These pin the shape: merge client-side, dedupe against the catalog, mark
|
|
554
|
+
owned, chip with the SAME vocabulary, and a plain click goes to MANAGE. ── */
|
|
555
|
+
|
|
556
|
+
test('the sky learns ownership from the viewer\'s own instances, signed-in only', () => {
|
|
557
|
+
const fn = (SCRIPT.match(/function loadOwnedInstances\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
558
|
+
assert.ok(fn, 'loadOwnedInstances() must exist');
|
|
559
|
+
assert.match(fn, /if \(!me\)/, 'a signed-out visitor sees the catalog exactly as today');
|
|
560
|
+
assert.match(fn, /provisioning\/instances/, 'the own-scoped feed is the source');
|
|
561
|
+
assert.match(fn, /torn_down/, 'the sky must not float what no longer exists');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test('a project both featured and owned renders once, marked owned', () => {
|
|
565
|
+
const fn = (SCRIPT.match(/function relOf\(p\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
566
|
+
assert.ok(fn, 'relOf() must exist');
|
|
567
|
+
assert.match(fn, /ownedByHost/,
|
|
568
|
+
'the ownership join lives in the one predicate both browse views already read');
|
|
569
|
+
// the "once" itself: the browse merge ANNOTATES the catalog row and skips
|
|
570
|
+
// appending a duplicate — deleting either half double-renders the project
|
|
571
|
+
const filter = (SCRIPT.match(/function applyMapFilter\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
572
|
+
assert.ok(filter, 'applyMapFilter() must be extractable');
|
|
573
|
+
assert.match(filter, /p\._inst = ownedByHost/, 'catalog rows are annotated with the owned instance');
|
|
574
|
+
assert.match(filter, /if \(h && catalogHosts\[h\]\) return;/, 'an annotated project is never appended twice');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('owned truth reaches the TABLE too — same list, same chip, same manage job', () => {
|
|
578
|
+
// the count, the sky and the table read ONE merged browse list; an owned row
|
|
579
|
+
// carries chipFor()'s truth in Status and routes its click to manage
|
|
580
|
+
const generated = (SCRIPT.match(/return '<tr data-generated([\s\S]*?)'<\/tr>';/) || [, ''])[1];
|
|
581
|
+
assert.ok(generated, 'the generated-row template must keep its extractable shape');
|
|
582
|
+
assert.match(generated, /data-manage/, 'an owned row carries the manage route key');
|
|
583
|
+
assert.match(generated, /p\._inst \? chipFor\(p\._inst\)/, 'an owned row\'s Status is the overlay truth');
|
|
584
|
+
assert.match(generated, /chip calm/, 'an unprobed catalog row keeps the honest calm dash');
|
|
585
|
+
assert.match(SCRIPT, /tr\.hasAttribute\('data-manage'\)\) return goToManage/,
|
|
586
|
+
'an owned row click goes to manage, exactly like its orb');
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test('an owned orb\'s job is manage, not the public site', () => {
|
|
590
|
+
assert.match(SCRIPT, /'\?view=mine'/, 'the owned href is the manage deep-link, real and middle-clickable');
|
|
591
|
+
assert.match(SCRIPT, /hasAttribute\('data-manage'\)\) return goToManage/,
|
|
592
|
+
'a plain click on an owned orb routes to manage before the card path');
|
|
593
|
+
const orbs = (SCRIPT.match(/function renderOrbs\(([\s\S]*?)\n \}/) || [, ''])[1];
|
|
594
|
+
assert.ok(orbs, 'renderOrbs must be extractable');
|
|
595
|
+
assert.match(orbs, /data-manage/, 'owned orbs carry the manage route key');
|
|
596
|
+
// data-inst is the MINE view's card-shell identity; an orb wearing it would
|
|
597
|
+
// shadow every unqualified [data-inst] lookup renderCard/applyMine make
|
|
598
|
+
assert.doesNotMatch(orbs, /data-inst/,
|
|
599
|
+
'the orb route key must not collide with the manage list\'s card identity');
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test('the orb chip is the manage list\'s truth, never a second story (C6)', () => {
|
|
603
|
+
const orbs = (SCRIPT.match(/function renderOrbs\(([\s\S]*?)\n \}/) || [, ''])[1];
|
|
604
|
+
// exactly ONE chip site, and it defers to chipFor() — a second site could
|
|
605
|
+
// hardcode a claim while this assertion still found its token
|
|
606
|
+
assert.equal((orbs.match(/chipFor\(inst\)/g) || []).length, 1,
|
|
607
|
+
'renderOrbs must have exactly one chip call site');
|
|
608
|
+
assert.match(orbs, /\(inst \? chipFor\(inst\) : ''\)/,
|
|
609
|
+
'the chip is chipFor() or nothing — never a literal class');
|
|
610
|
+
assert.doesNotMatch(SCRIPT, /chip live[^']*'\s*\+\s*esc\(inst\./,
|
|
611
|
+
'no instance chip may be literal-classed live — chipMarkup() decides from the overlay');
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('click-to-manage anchors the project and defers until the card exists', () => {
|
|
615
|
+
assert.match(SCRIPT, /function goToManage/);
|
|
616
|
+
assert.match(SCRIPT, /pendingAnchor/);
|
|
617
|
+
assert.match(SCRIPT, /scrollIntoView/);
|
|
618
|
+
const fn = (SCRIPT.match(/function tryAnchor\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
619
|
+
assert.ok(fn, 'tryAnchor() must be extractable');
|
|
620
|
+
assert.match(fn, /mineClear/, 'a filter that hides the anchored card is cleared, not silently no-op\'d');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test('the map does not poll for owned truth — it re-reads on entry (C8)', () => {
|
|
624
|
+
assert.match(SCRIPT, /function shouldPoll\(\)[\s\S]*?currentView === 'mine'/);
|
|
625
|
+
assert.match(SCRIPT, /v === 'map' && me\) loadOwnedInstances/,
|
|
626
|
+
'entering the map refreshes the owned overlay once, never on a timer');
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test('the chip-freshness tick spends no requests — it re-renders what it has (C6+C8)', () => {
|
|
630
|
+
// without this tick "live · checked 2m ago" freezes and outlives FRESH_MS;
|
|
631
|
+
// with a FETCH in it the map would be polling, which C8 forbids
|
|
632
|
+
const fn = (SCRIPT.match(/function mapChipTick\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
633
|
+
assert.ok(fn, 'mapChipTick() must exist');
|
|
634
|
+
assert.match(fn, /currentView !== 'map'/, 'the tick is map-view-guarded');
|
|
635
|
+
assert.match(fn, /document\.hidden/, 'and does nothing in a hidden tab');
|
|
636
|
+
assert.doesNotMatch(fn, /fetch\(/, 'zero requests — the re-fetch belongs to entry and tab return only');
|
|
637
|
+
});
|