@bongos/core 1.19.401 → 1.19.403
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 +50 -40
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0200-re-point-a-bring-your-own-domain.md +105 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +3 -2
- package/docs/copy-inventory.md +141 -138
- package/docs/copy-registry.json +205 -176
- package/docs/design/projects-hub-direction-v2.md +16 -0
- package/modules/platform-identity/project-stats.js +123 -38
- package/modules/platform-identity/routes/my-projects.js +13 -0
- package/modules/platform-identity/routes/projects.js +7 -4
- package/modules/provisioning/migrations/provisioning_012_domain_repoint.sql +35 -0
- package/modules/provisioning/provisioning.js +68 -8
- package/modules/provisioning/routes/provisioning.js +50 -23
- package/modules/ui-design/kit/fixtures/featured.json +1 -1
- package/modules/ui-design/kit/fixtures/my-projects.json +1 -1
- package/modules/ui-design/kit/serve.js +14 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +126 -25
- package/public-landing/projects.html +155 -27
- package/public-landing/projects.probes.json +28 -0
- package/public-landing/projects.states.json +6 -3
- package/scripts/gds/provision-net.js +33 -0
- package/scripts/gds/provision-units.js +18 -0
- package/scripts/gds/provision.js +97 -5
- package/src/module-api.js +5 -1
- package/tests/cosmos_token_contrast.mjs +5 -2
- package/tests/project_stats.mjs +128 -2
- package/tests/projects_hub.mjs +106 -5
- package/tests/projects_hub_pre_uat.mjs +28 -2
- package/tests/provision.mjs +185 -12
- package/tests/provisioning_default_domain.mjs +5 -3
- package/tests/provisioning_domain_attach.mjs +166 -17
- package/tests/public_landing_projects.mjs +5 -1
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.403",
|
|
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.403",
|
|
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.403",
|
|
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",
|
|
@@ -250,6 +250,7 @@
|
|
|
250
250
|
--radius:16px; --radius-sm:10px; --radius-pill:999px;
|
|
251
251
|
--top-h:64px;
|
|
252
252
|
--gutter:clamp(16px,4vw,34px);
|
|
253
|
+
--leader-len:68px; /* the poster card's callout leader (task 1003301) */
|
|
253
254
|
/* the world's object for the hub's view masthead (task 1003332): the apex
|
|
254
255
|
shares the landing's plates because it is the same world (DESIGN.md
|
|
255
256
|
"Materials"); the hub takes the alpha CUTOUT in both modes — the view
|
|
@@ -404,6 +405,8 @@ a.orb{cursor:pointer;}
|
|
|
404
405
|
.attachFold{margin-top:12px;}
|
|
405
406
|
.attachFold summary{cursor:pointer;width:fit-content;font-size:13px;font-weight:700;color:var(--ink-faint);}
|
|
406
407
|
.attachFold summary:hover,.attachFold[open] summary{color:var(--ink-soft);}
|
|
408
|
+
.attachNote{margin:8px 0 0;font-size:13px;line-height:1.45;color:var(--ink-faint);}
|
|
409
|
+
.attachNote code{font-family:var(--font-mono);}
|
|
407
410
|
.projMsg{font-size:13.5px;font-weight:600;margin-top:10px;min-height:18px;color:var(--ink-soft);line-height:1.5;}
|
|
408
411
|
.projMsg.ok{color:var(--ok);}
|
|
409
412
|
.projMsg.err{color:var(--err);}
|
|
@@ -895,32 +898,130 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
895
898
|
dark, which paints a white sheet over the page instead of dimming it. */
|
|
896
899
|
.cardScrim{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;
|
|
897
900
|
padding:24px;background:rgba(0,0,0,.55);backdrop-filter:blur(3px);}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
.projCard
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
.projCard .
|
|
913
|
-
|
|
914
|
-
.projCard .
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
.projCard .
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
901
|
+
/* THE POSTER (task 1003301; the v2 pick, direction §7): a specimen sheet in the
|
|
902
|
+
world, not a panel. min(560px,100%) on bg-card closed by one rule hairline,
|
|
903
|
+
no shadow (the world has no shadow vocabulary for UI). A flex column ordered
|
|
904
|
+
by `order`, so every region stays a flat child of the card whatever the
|
|
905
|
+
layout does with it (the C6 slice rule in tests/projects_hub.mjs).
|
|
906
|
+
container-type gives the disc and the callouts the card's own width (cqw),
|
|
907
|
+
so the banner's 5:2 edge is a number the disc can sit on. Six regions, top
|
|
908
|
+
to bottom: the banner, the two figures (callouts ON the art from 561px,
|
|
909
|
+
the two-up band otherwise and with no art), the accent disc, name / tagline
|
|
910
|
+
/ relationship, the faces, the facts as a mono hairline ledger, the doors. */
|
|
911
|
+
.projCard{position:relative;container-type:inline-size;display:flex;flex-direction:column;
|
|
912
|
+
width:min(560px,100%);max-height:calc(100vh - 48px);overflow:auto;text-align:left;
|
|
913
|
+
background:var(--bg-card);border:1px solid var(--rule);border-radius:var(--radius);box-shadow:none;padding:0 0 26px;}
|
|
914
|
+
.projCard:not(.has-art){padding-top:26px;}
|
|
915
|
+
.projCard .cardArt{order:0;display:block;width:100%;aspect-ratio:5/2;object-fit:cover;
|
|
916
|
+
border-radius:var(--radius) var(--radius) 0 0;}
|
|
917
|
+
.projCard .cardX{position:absolute;top:12px;right:12px;z-index:4;display:inline-flex;align-items:center;
|
|
918
|
+
justify-content:center;width:30px;height:30px;border:0;border-radius:var(--radius-pill);
|
|
919
|
+
background:var(--glass-strong);color:var(--ink);cursor:pointer;font-size:14px;line-height:1;}
|
|
920
|
+
.projCard .cardX:hover{background:var(--glass-edge);}
|
|
921
|
+
/* on art the close control takes the void pill: the one legal literal is black with alpha */
|
|
922
|
+
.projCard.has-art .cardX{background:var(--void-pill);color:var(--void-ink);}
|
|
923
|
+
/* THE DISC SURVIVES: it is how a project is recognised in the sky (task 1003258).
|
|
924
|
+
With art it half-overlaps the banner's lower edge, ringed in the card ground
|
|
925
|
+
so it sits ON the card; without art it renders where it always did. */
|
|
926
|
+
.projCard .cardDot{order:1;align-self:flex-start;display:block;width:54px;height:54px;border-radius:50%;
|
|
927
|
+
margin:0 0 16px 26px;background:var(--acc,var(--accent));box-shadow:none;}
|
|
928
|
+
.projCard.has-art .cardDot{position:absolute;z-index:3;left:24px;top:calc(100cqw / 2.5 - 27px);margin:0;
|
|
929
|
+
border:4px solid var(--bg-card);}
|
|
930
|
+
.projCard h3{order:3;margin:0;padding:0 26px;font-family:var(--font-display);font-size:32px;font-weight:300;
|
|
931
|
+
line-height:1.08;letter-spacing:-.03em;color:var(--ink);}
|
|
932
|
+
.projCard.has-art h3{margin-top:44px;}
|
|
933
|
+
.projCard .cardTag{order:4;margin:9px 0 0;padding:0 26px;font-size:14.5px;font-weight:400;line-height:1.5;
|
|
934
|
+
color:var(--ink-soft);max-width:46ch;}
|
|
935
|
+
/* the relationship is a mono label, not a pill: a fact about you, not a control */
|
|
936
|
+
.projCard .cardRel{order:5;display:block;margin:14px 0 0;padding:0 26px;background:none;border-radius:0;
|
|
937
|
+
font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);
|
|
938
|
+
text-transform:uppercase;color:var(--ink-faint);}
|
|
939
|
+
.projCard .cardFaces{order:6;display:flex;align-items:center;margin:18px 0 0;padding:0 26px;}
|
|
940
|
+
.projCard .cardFaces img{width:28px;height:28px;border-radius:50%;object-fit:cover;
|
|
941
|
+
border:2px solid var(--bg-card);margin-right:-8px;}
|
|
942
|
+
.projCard .facesMore{margin-left:16px;font-family:var(--font-mono);font-size:11.5px;color:var(--ink-faint);
|
|
943
|
+
font-variant-numeric:tabular-nums;}
|
|
944
|
+
/* the two figures in the world's callout grammar (DESIGN.md "Leader-line
|
|
945
|
+
callouts"): a 1px diagonal leader, a 300-weight tabular figure, a caption */
|
|
946
|
+
.projCard .cardFigs{order:7;display:flex;gap:38px;margin:20px 0 0;padding:0 26px;}
|
|
947
|
+
.projCard .cardCall{display:block;}
|
|
948
|
+
.projCard .lead{display:none;width:var(--leader-len);height:26px;margin-bottom:10px;overflow:visible;
|
|
949
|
+
color:color-mix(in srgb,var(--ink) 78%,transparent);}
|
|
950
|
+
.projCard .fig{display:block;position:relative;width:fit-content;font-family:var(--font-display);
|
|
951
|
+
font-size:clamp(21px,5cqw,28px);font-weight:300;letter-spacing:-.025em;line-height:1;
|
|
952
|
+
font-variant-numeric:tabular-nums;color:var(--ink);text-shadow:var(--type-halo);}
|
|
953
|
+
.projCard .cap{display:block;position:relative;width:fit-content;margin-top:7px;font-size:13px;
|
|
954
|
+
color:var(--ink-soft);text-shadow:var(--type-halo);}
|
|
955
|
+
@media (min-width:561px){
|
|
956
|
+
.projCard.has-art .cardFigs{position:absolute;z-index:2;left:0;right:0;top:0;height:calc(100cqw / 2.5);
|
|
957
|
+
margin:0;padding:0;display:block;pointer-events:none;}
|
|
958
|
+
.projCard.has-art .cardCall{position:absolute;width:max-content;max-width:42%;}
|
|
959
|
+
/* 44px in, not the 26px gutter: the patch reaches 44px past the glyph box,
|
|
960
|
+
and a patch that meets the card's edge is clipped flat there */
|
|
961
|
+
.projCard.has-art .cardCall.a{left:44px;top:14px;}
|
|
962
|
+
.projCard.has-art .cardCall.b{right:44px;top:calc(100cqw / 2.5 - 112px);text-align:right;}
|
|
963
|
+
.projCard.has-art .cardCall.b .fig,.projCard.has-art .cardCall.b .cap{margin-left:auto;}
|
|
964
|
+
.projCard.has-art .cardCall .lead{display:block;}
|
|
965
|
+
.projCard.has-art .cardCall.b .lead{transform:scaleX(-1);margin-left:auto;}
|
|
966
|
+
/* each callout carries ONE feathered patch of the CARD ground under its
|
|
967
|
+
figure and caption together: solid to 10px past every edge of the glyph
|
|
968
|
+
boxes (no column of the text ever sits on the fade), then a 34px feather
|
|
969
|
+
to nothing, so the patch reads as haze in the art and never as a slab of
|
|
970
|
+
paint on it. The feather is a mask, not a shadow. It starts under the
|
|
971
|
+
figure, never behind the leader. */
|
|
972
|
+
.projCard.has-art .cardCall::before{content:"";position:absolute;left:-44px;right:-44px;top:18px;bottom:-24px;
|
|
973
|
+
z-index:-1;pointer-events:none;background:var(--bg-card);border-radius:44px/30px;filter:blur(3px);
|
|
974
|
+
-webkit-mask-image:linear-gradient(90deg,transparent,var(--ink) 34px,var(--ink) calc(100% - 34px),transparent),
|
|
975
|
+
linear-gradient(180deg,transparent,var(--ink) 10px,var(--ink) calc(100% - 12px),transparent);
|
|
976
|
+
-webkit-mask-composite:source-in;
|
|
977
|
+
mask-image:linear-gradient(90deg,transparent,var(--ink) 34px,var(--ink) calc(100% - 34px),transparent),
|
|
978
|
+
linear-gradient(180deg,transparent,var(--ink) 10px,var(--ink) calc(100% - 12px),transparent);
|
|
979
|
+
mask-composite:intersect;}
|
|
980
|
+
}
|
|
981
|
+
/* the facts as the ledger (task 1003332's grammar): mono label left, value
|
|
982
|
+
right and tabular, ONE rule-soft hairline per row on both cells so the rule
|
|
983
|
+
never steps at the column boundary. A hidden pair leaves no gap. */
|
|
984
|
+
.projCard .cardMeta{order:8;display:grid;grid-template-columns:auto 1fr;justify-content:normal;gap:0;
|
|
985
|
+
margin:22px 0 0;padding:0 26px;text-align:left;}
|
|
986
|
+
.projCard .cardMeta dt{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);
|
|
987
|
+
text-transform:uppercase;color:var(--ink-faint);padding:11px 20px 11px 0;border-top:1px solid var(--rule-soft);
|
|
988
|
+
line-height:19px;}
|
|
989
|
+
.projCard .cardMeta dd{margin:0;padding:11px 0;text-align:right;font-size:14px;font-weight:400;color:var(--ink);
|
|
990
|
+
border-top:1px solid var(--rule-soft);font-variant-numeric:tabular-nums;}
|
|
991
|
+
.projCard .cardMeta .ownerAv{display:inline-block;width:16px;height:16px;border-radius:50%;object-fit:cover;
|
|
992
|
+
vertical-align:-3px;margin-right:6px;}
|
|
993
|
+
/* ONE accent door (the One Warm Thing Rule), the hall entry beside it as an
|
|
994
|
+
underlined accent-deep text link (the footer-link grammar) */
|
|
995
|
+
.projCard .cardActions{order:9;display:flex;align-items:center;justify-content:flex-start;gap:22px;flex-wrap:wrap;
|
|
996
|
+
margin:24px 0 0;padding:0 26px;}
|
|
997
|
+
.projCard .cardOpen{display:inline-flex;align-items:center;justify-content:center;min-height:48px;padding:0 30px;
|
|
998
|
+
border-radius:var(--radius-pill);background:var(--accent);color:var(--accent-on);font-family:var(--font-display);
|
|
999
|
+
font-size:16px;font-weight:500;text-transform:lowercase;text-decoration:none;
|
|
1000
|
+
transition:background .2s cubic-bezier(.2,.7,.3,1);}
|
|
1001
|
+
.projCard .cardOpen:hover{background:var(--accent-lift);color:var(--accent-on);}
|
|
1002
|
+
.projCard .cardHall{display:inline-flex;align-items:center;gap:8px;min-height:24px;color:var(--accent-deep);
|
|
1003
|
+
font-size:15px;text-transform:lowercase;text-decoration:underline;text-underline-offset:4px;
|
|
1004
|
+
text-decoration-color:color-mix(in srgb,var(--accent) 55%,transparent);
|
|
1005
|
+
transition:text-decoration-color .2s cubic-bezier(.2,.7,.3,1);}
|
|
1006
|
+
.projCard .cardHall:hover{text-decoration-color:var(--accent-deep);}
|
|
1007
|
+
.cardHall svg,.rowHall svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.5;
|
|
1008
|
+
stroke-linecap:round;stroke-linejoin:round;}
|
|
1009
|
+
/* the table row's hall glyph: beside Open, a member's door, only where the hall is elsewhere */
|
|
1010
|
+
.rowHall{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin-left:10px;
|
|
1011
|
+
vertical-align:middle;color:var(--accent-deep);border-radius:var(--radius-pill);}
|
|
1012
|
+
.rowHall:hover{background:var(--glass);}
|
|
1013
|
+
.tbl .colGo{white-space:nowrap;}
|
|
1014
|
+
@media (max-width:640px){
|
|
1015
|
+
.projCard h3{font-size:26px;padding:0 20px;}
|
|
1016
|
+
.projCard .cardTag,.projCard .cardRel,.projCard .cardFaces,.projCard .cardFigs,.projCard .cardMeta,
|
|
1017
|
+
.projCard .cardActions{padding-left:20px;padding-right:20px;}
|
|
1018
|
+
.projCard .cardDot{margin-left:20px;}
|
|
1019
|
+
.projCard.has-art .cardDot{left:18px;}
|
|
1020
|
+
.projCard .cardActions{gap:16px;}
|
|
1021
|
+
}
|
|
921
1022
|
@media (prefers-reduced-motion:no-preference){
|
|
922
|
-
.cardScrim:not([hidden]) .projCard{animation:cardIn .
|
|
923
|
-
@keyframes cardIn{from{opacity:0;transform:translateY(6px)
|
|
1023
|
+
.cardScrim:not([hidden]) .projCard{animation:cardIn 260ms cubic-bezier(.16,1,.3,1) both;}
|
|
1024
|
+
@keyframes cardIn{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}
|
|
924
1025
|
}
|
|
925
1026
|
|
|
926
1027
|
/* ── your projects: search + a state filter whose pills carry live counts, so
|
|
@@ -539,7 +539,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
539
539
|
<td class="right num" id="selfBuilders">—</td>
|
|
540
540
|
<td class="colTags" id="selfTags">—</td>
|
|
541
541
|
<td class="right colStatus"><span class="chip calm"><span class="cdot"></span>—</span></td>
|
|
542
|
-
<td class="colGo"><a class="rowGo" href="/">Open →</a></td>
|
|
542
|
+
<td class="colGo"><a class="rowGo" href="/">Open →</a><a class="rowHall" id="selfHall" href="{{buildersOrigin}}" aria-label="Builders' hall" title="Builders' hall" hidden><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M2.8 7.4 10 3.5l7.2 3.9"/><path d="M5.2 8.8v5.6M10 8.8v5.6M14.8 8.8v5.6"/><path d="M3.4 16.3h13.2"/></svg></a></td>
|
|
543
543
|
</tr>
|
|
544
544
|
<!--apex-only:end-->
|
|
545
545
|
</tbody>
|
|
@@ -563,20 +563,30 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
563
563
|
<div class="cardScrim" id="cardScrim" hidden>
|
|
564
564
|
<div class="projCard" id="projCard" role="dialog" aria-modal="true" aria-labelledby="cardName">
|
|
565
565
|
<button type="button" class="cardX" id="cardClose" aria-label="Close">✕</button>
|
|
566
|
+
<!-- The poster (task 1003301, the v2 pick): every region a DIRECT child of
|
|
567
|
+
the card, on purpose. The C6 card test slices the card non-greedily up
|
|
568
|
+
to the first adjacent pair of closing wrapper tags, and a nested
|
|
569
|
+
wrapper would silently shorten what that test guards. So the banner is
|
|
570
|
+
a bare img, the figures and the faces are paragraphs, the facts a
|
|
571
|
+
definition list. Each region degrades by hiding: no art, no banner;
|
|
572
|
+
no faces, no row; one figure null, the other stands alone; both null,
|
|
573
|
+
no band; a fact with no answer hides label and value together,
|
|
574
|
+
because "Owner: dash" on the most authoritative-looking element of
|
|
575
|
+
the page reads as broken, not honest (task 1002778). Everything null
|
|
576
|
+
is the card that shipped before this one: disc, name, tagline,
|
|
577
|
+
relationship, the address, one door. -->
|
|
578
|
+
<img class="cardArt" id="cardArt" alt="" hidden>
|
|
579
|
+
<p class="cardFigs" id="cardFigs" hidden>
|
|
580
|
+
<span class="cardCall a" id="cardBuildersFig" hidden><svg class="lead" viewBox="0 0 68 26" preserveAspectRatio="none" aria-hidden="true"><line x1="0" y1="25" x2="67" y2="1" stroke="currentColor" stroke-width="1" vector-effect="non-scaling-stroke"/></svg><span class="fig" id="cardBuilders"></span><span class="cap">builders</span></span>
|
|
581
|
+
<span class="cardCall b" id="cardShippedFig" hidden><svg class="lead" viewBox="0 0 68 26" preserveAspectRatio="none" aria-hidden="true"><line x1="0" y1="25" x2="67" y2="1" stroke="currentColor" stroke-width="1" vector-effect="non-scaling-stroke"/></svg><span class="fig" id="cardShipped"></span><span class="cap">shipped in 30 days</span></span>
|
|
582
|
+
</p>
|
|
566
583
|
<span class="cardDot" id="cardDot"></span>
|
|
567
584
|
<h3 id="cardName"></h3>
|
|
568
585
|
<p class="cardTag" id="cardTag"></p>
|
|
569
586
|
<p class="cardRel" id="cardRel" hidden></p>
|
|
570
|
-
|
|
571
|
-
purpose: the C6 card test slices the card non-greedily up to the
|
|
572
|
-
first adjacent pair of closing wrapper tags, and a nested wrapper
|
|
573
|
-
would silently shorten what that test guards. A fact with no answer
|
|
574
|
-
hides, label and value together, because "Owner: dash" on the most
|
|
575
|
-
authoritative-looking element of the page reads as broken, not
|
|
576
|
-
honest (task 1002778). -->
|
|
587
|
+
<p class="cardFaces" id="cardFaces" hidden></p>
|
|
577
588
|
<dl class="cardMeta" id="cardMeta" hidden>
|
|
578
589
|
<dt id="cardTypeL" hidden>Type</dt><dd id="cardType" hidden></dd>
|
|
579
|
-
<dt id="cardBuildersL" hidden>Builders</dt><dd id="cardBuilders" hidden></dd>
|
|
580
590
|
<dt id="cardRunsL" hidden>Runs</dt><dd id="cardRuns" hidden></dd>
|
|
581
591
|
<dt id="cardOwnerL" hidden>Owner</dt><dd id="cardOwner" hidden></dd>
|
|
582
592
|
<dt id="cardListedL" hidden>Listed</dt><dd id="cardListed" hidden></dd>
|
|
@@ -1492,7 +1502,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1492
1502
|
: '<span class="chip calm"><span class="cdot"></span>—</span>') + '</td>' +
|
|
1493
1503
|
'<td class="colGo">' + (safeHttpUrl(p.origin)
|
|
1494
1504
|
? '<a class="rowGo" href="' + esc(safeHttpUrl(p.origin)) + '" target="_blank" rel="noopener">Open →</a>'
|
|
1495
|
-
: '') + '</td>' +
|
|
1505
|
+
: '') + hallGlyphHtml(p, rel) + '</td>' +
|
|
1496
1506
|
'</tr>';
|
|
1497
1507
|
}).join(''));
|
|
1498
1508
|
}
|
|
@@ -1577,6 +1587,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1577
1587
|
if (row) {
|
|
1578
1588
|
row.hidden = !selfShown;
|
|
1579
1589
|
$('selfPill').innerHTML = badge ? ' <span class="pill">' + badge + '</span>' : '';
|
|
1590
|
+
/* the hall glyph: member-gated (the badge says so) and address-gated */
|
|
1591
|
+
var sh = $('selfHall');
|
|
1592
|
+
if (sh) sh.hidden = !badge || !hallUrlOf(SELF_PROJECT);
|
|
1580
1593
|
}
|
|
1581
1594
|
|
|
1582
1595
|
var shown = list.length + (selfShown ? 1 : 0);
|
|
@@ -1639,32 +1652,111 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1639
1652
|
return !!value;
|
|
1640
1653
|
}
|
|
1641
1654
|
|
|
1655
|
+
/* Runs, on the poster, is the project's ADDRESS: where it answers. The table
|
|
1656
|
+
keeps the module list in its Runs column (the v2 pick, direction §7). */
|
|
1657
|
+
function runsOf(p) {
|
|
1658
|
+
return p.isSelf ? hostOf(location.host) : hostOf(p.origin);
|
|
1659
|
+
}
|
|
1660
|
+
/* shipped in the last 30 days, "N+" when the count sat at the feed's row cap;
|
|
1661
|
+
null is "we could not ask" and hides, 0 is a real answer and renders */
|
|
1662
|
+
function shippedFigure(p) {
|
|
1663
|
+
if (p.shipped_30d == null) return '';
|
|
1664
|
+
return String(p.shipped_30d) + (p.shipped_30d_capped ? '+' : '');
|
|
1665
|
+
}
|
|
1666
|
+
/* A figure either has an answer or takes no space: the callout hides whole,
|
|
1667
|
+
leader and caption with it, so the other figure stands alone. */
|
|
1668
|
+
function cardFig(id, value) {
|
|
1669
|
+
$(id).textContent = value || '';
|
|
1670
|
+
$(id + 'Fig').hidden = !value;
|
|
1671
|
+
return !!value;
|
|
1672
|
+
}
|
|
1673
|
+
/* The faces: up to five avatars from the project's own leaderboard, then +N
|
|
1674
|
+
for the builders beyond them. Third-party URLs, scheme-checked before they
|
|
1675
|
+
become a src. No faces, no row. */
|
|
1676
|
+
var FACES_MAX = 5;
|
|
1677
|
+
function cardFaces(p) {
|
|
1678
|
+
var faces = Array.isArray(p.faces) ? p.faces.map(safeHttpUrl).filter(Boolean).slice(0, FACES_MAX) : [];
|
|
1679
|
+
var more = p.builders != null && p.builders > faces.length ? p.builders - faces.length : 0;
|
|
1680
|
+
$('cardFaces').innerHTML = faces.map(function (u) {
|
|
1681
|
+
return '<img src="' + esc(u) + '" alt="" loading="lazy">';
|
|
1682
|
+
}).join('') + (more ? '<span class="facesMore">+' + more + '</span>' : '');
|
|
1683
|
+
$('cardFaces').hidden = !faces.length;
|
|
1684
|
+
}
|
|
1685
|
+
/* The banner. has-art switches the whole layout (the disc over the banner's
|
|
1686
|
+
edge, the figures as callouts on it); without art the card is the card
|
|
1687
|
+
that shipped before this one. A broken image demotes to no-art, so the
|
|
1688
|
+
most authoritative element of the page never shows a broken glyph. */
|
|
1689
|
+
function cardArt(url) {
|
|
1690
|
+
var img = $('cardArt');
|
|
1691
|
+
if (url) img.src = url; else img.removeAttribute('src');
|
|
1692
|
+
img.hidden = !url;
|
|
1693
|
+
$('projCard').classList.toggle('has-art', !!url);
|
|
1694
|
+
}
|
|
1695
|
+
$('cardArt').addEventListener('error', function () { cardArt(''); });
|
|
1696
|
+
/* EXACTLY ONE hall-URL derivation (task 1003257, owner decision a). The hub's
|
|
1697
|
+
own hall comes from the DOM: the server-stamped anchor in its own table row
|
|
1698
|
+
(dropped whole by the stamp when a pack names no hall; never assembled in
|
|
1699
|
+
script). A project's comes from the hall_origin its feed row carries. Both
|
|
1700
|
+
go through safeHttpUrl, and both count only where the hall is a different
|
|
1701
|
+
host from the site: a door that opens onto the room you are in is a dead door. */
|
|
1702
|
+
function hallUrlOf(p) {
|
|
1703
|
+
var hall, site;
|
|
1704
|
+
if (p.isSelf) {
|
|
1705
|
+
var a = $('selfHall');
|
|
1706
|
+
hall = a ? safeHttpUrl(a.getAttribute('href')) : '';
|
|
1707
|
+
site = location.host;
|
|
1708
|
+
} else {
|
|
1709
|
+
hall = safeHttpUrl(p.hall_origin);
|
|
1710
|
+
site = p.origin;
|
|
1711
|
+
}
|
|
1712
|
+
return hall && hostOf(hall) !== hostOf(site) ? hall : '';
|
|
1713
|
+
}
|
|
1714
|
+
var HALL_GLYPH = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M2.8 7.4 10 3.5l7.2 3.9"/><path d="M5.2 8.8v5.6M10 8.8v5.6M14.8 8.8v5.6"/><path d="M3.4 16.3h13.2"/></svg>';
|
|
1715
|
+
function isMember(rel) { return rel === 'owner' || rel === 'builder'; }
|
|
1716
|
+
/* the row's hall glyph: a member's door, only where the hall is elsewhere */
|
|
1717
|
+
function hallGlyphHtml(p, rel) {
|
|
1718
|
+
var hall = isMember(rel) ? hallUrlOf(p) : '';
|
|
1719
|
+
return hall
|
|
1720
|
+
? '<a class="rowHall" href="' + esc(hall) + '" target="_blank" rel="noopener" aria-label="Builders\' hall" title="Builders\' hall">' + HALL_GLYPH + '</a>'
|
|
1721
|
+
: '';
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1642
1724
|
function openCard(p, rel, returnTo) {
|
|
1643
1725
|
var href = p.isSelf ? '/' : safeHttpUrl(p.origin);
|
|
1726
|
+
/* the art: catalog text an Archon typed, scheme-checked server-side and
|
|
1727
|
+
again here before it becomes a src */
|
|
1728
|
+
cardArt(p.isSelf ? '' : safeHttpUrl(p.art_url));
|
|
1644
1729
|
$('cardDot').style.setProperty('--acc', p.isSelf ? 'var(--accent)' : accentOf(p));
|
|
1645
1730
|
$('cardName').textContent = p.name || '';
|
|
1646
1731
|
$('cardTag').textContent = p.tagline || '';
|
|
1647
1732
|
var label = rel === 'owner' ? 'yours' : rel === 'builder' ? 'you build here' : '';
|
|
1648
1733
|
$('cardRel').textContent = label;
|
|
1649
1734
|
$('cardRel').hidden = !label;
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1735
|
+
cardFaces(p);
|
|
1736
|
+
/* the two figures: builders (0 is a real answer, "nobody has joined yet";
|
|
1737
|
+
null is "we could not ask" and hides) and shipped in the last 30 days */
|
|
1738
|
+
var figs = cardFig('cardBuilders', p.builders == null ? '' : String(p.builders));
|
|
1739
|
+
figs = cardFig('cardShipped', shippedFigure(p)) || figs;
|
|
1740
|
+
$('cardFigs').hidden = !figs;
|
|
1653
1741
|
var any = cardFact('cardType', typeLabel(p.type));
|
|
1654
|
-
any = cardFact('
|
|
1655
|
-
any = cardFact('cardRuns', Array.isArray(p.tags) ? tagsHtml(p.tags) : '', true) || any;
|
|
1742
|
+
any = cardFact('cardRuns', runsOf(p)) || any;
|
|
1656
1743
|
/* when the rel chip already says "yours", naming the viewer as Owner is
|
|
1657
1744
|
noise — the fact answers strangers, not the person it describes */
|
|
1658
1745
|
any = cardFact('cardOwner', rel === 'owner' ? '' : ownerHtml(p), true) || any;
|
|
1659
1746
|
any = cardFact('cardListed', p.created_at ? listedDate(p.created_at) : '') || any;
|
|
1660
1747
|
$('cardMeta').hidden = !any;
|
|
1661
1748
|
/* the platform's door is same-tab: / is this site, and a new tab for the
|
|
1662
|
-
page you are already on is a bug, not a courtesy
|
|
1663
|
-
|
|
1664
|
-
|
|
1749
|
+
page you are already on is a bug, not a courtesy. The hall door beside it
|
|
1750
|
+
is member-gated AND address-gated: a member's entry, only where the hall
|
|
1751
|
+
is somewhere other than the site the first door already opens. */
|
|
1752
|
+
var hall = isMember(rel) ? hallUrlOf(p) : '';
|
|
1753
|
+
$('cardActions').innerHTML = (href
|
|
1754
|
+
? '<a class="cardOpen" id="cardOpen" href="' + esc(href) + '"' +
|
|
1665
1755
|
(p.isSelf ? '' : ' target="_blank" rel="noopener"') + '>' +
|
|
1666
1756
|
(p.isSelf ? 'Home →' : 'Open site →') + '</a>'
|
|
1667
|
-
: ''
|
|
1757
|
+
: '') + (hall
|
|
1758
|
+
? '<a class="cardHall" id="cardHall" href="' + esc(hall) + '" target="_blank" rel="noopener">' + HALL_GLYPH + 'Builders\' hall</a>'
|
|
1759
|
+
: '');
|
|
1668
1760
|
closeMenus();
|
|
1669
1761
|
cardReturn = returnTo || null;
|
|
1670
1762
|
$('cardScrim').hidden = false;
|
|
@@ -1864,6 +1956,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1864
1956
|
builder count and its owner — the archon row — by the same rule the
|
|
1865
1957
|
server-side enrichment applies to every other project */
|
|
1866
1958
|
SELF_PROJECT.builders = real.length;
|
|
1959
|
+
SELF_PROJECT.faces = real.map(function (b) { return safeHttpUrl(b.avatar_url); }).filter(Boolean).slice(0, FACES_MAX);
|
|
1867
1960
|
var arch = real.filter(function (b) { return b.rank === 'archon'; })[0];
|
|
1868
1961
|
if (arch && arch.github_login) {
|
|
1869
1962
|
SELF_PROJECT.owner = String(arch.github_login);
|
|
@@ -1872,6 +1965,21 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1872
1965
|
})
|
|
1873
1966
|
.catch(function () { /* unreachable → the dash already in the markup stands */ });
|
|
1874
1967
|
}
|
|
1968
|
+
/* the platform's shipped-in-30-days figure, by the rule the server applies
|
|
1969
|
+
to every other project (project-stats.js): rows inside the window from
|
|
1970
|
+
shipped_at, "N+" when the feed's row cap (200 with ?days honoured) was full */
|
|
1971
|
+
function loadSelfShipped() {
|
|
1972
|
+
return fetch(API + '/public/recent-shipped?days=30', { credentials: 'same-origin' })
|
|
1973
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
1974
|
+
.then(function (d) {
|
|
1975
|
+
if (!d || !Array.isArray(d.tasks)) return;
|
|
1976
|
+
var since = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
1977
|
+
var n = d.tasks.filter(function (t) { return Date.parse(t && t.shipped_at) >= since; }).length;
|
|
1978
|
+
SELF_PROJECT.shipped_30d = n;
|
|
1979
|
+
SELF_PROJECT.shipped_30d_capped = d.tasks.length >= 200 && n === d.tasks.length;
|
|
1980
|
+
})
|
|
1981
|
+
.catch(function () { /* unknown stays unknown: the figure hides */ });
|
|
1982
|
+
}
|
|
1875
1983
|
|
|
1876
1984
|
function loadCatalog() {
|
|
1877
1985
|
fetch(API + '/projects/featured', { credentials: 'same-origin' })
|
|
@@ -4022,8 +4130,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4022
4130
|
(error_note, no intent in flight) can re-send its own domain — the route
|
|
4023
4131
|
re-enqueues the attach for exactly this shape */
|
|
4024
4132
|
if (inst.status === 'active' && inst.domain && inst.error_note && !inst.open_intent) return true;
|
|
4025
|
-
|
|
4026
|
-
|
|
4133
|
+
/* every address here is now correctable (task 1003237): none (give it one),
|
|
4134
|
+
the platform-assigned <slug>.<apex> (upgrade away from it), and — since the
|
|
4135
|
+
owner decision of 2026-08-28 — a bring-your-own address that was mistyped.
|
|
4136
|
+
The server's guards (reserved, in-apex squat, already held by another
|
|
4137
|
+
project) are unchanged and stay the authority; this is only the door. */
|
|
4138
|
+
return inst.status === 'requested' || inst.status === 'error' || inst.status === 'active';
|
|
4027
4139
|
}
|
|
4028
4140
|
/* re-enqueue doors: Try again on snagged, Bring it back on torn down —
|
|
4029
4141
|
both re-POST the slug (the server's PROVISION_ENQUEUE_FROM gate) */
|
|
@@ -4237,13 +4349,30 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4237
4349
|
be refused (intent_conflict), so the fold waits for the run to land */
|
|
4238
4350
|
if (inst.open_intent && inst.open_intent.action === 'domain-attach') return '';
|
|
4239
4351
|
var rekick = inst.status === 'active' && inst.domain && inst.error_note;
|
|
4352
|
+
/* the CORRECTION door (task 1003237): the project already answers on an
|
|
4353
|
+
address the founder brought, and it can be changed. The field carries the
|
|
4354
|
+
current one so a typo is fixed by editing it, not retyped from scratch —
|
|
4355
|
+
the re-kick door's shape, for the same reason. The platform-assigned
|
|
4356
|
+
<slug>.<apex> is NOT a correction: moving off it is the upgrade the fold
|
|
4357
|
+
already offered, and the field stays empty for the domain being brought. */
|
|
4358
|
+
var byo = inst.domain && inst.domain !== assignedDefaultFor(inst);
|
|
4359
|
+
var repoint = !rekick && byo;
|
|
4360
|
+
var prefill = rekick || repoint;
|
|
4361
|
+
var label = rekick ? 'Try the address again'
|
|
4362
|
+
: repoint ? 'Use a different domain'
|
|
4363
|
+
: inst.domain ? 'Use a domain I own' : 'Give it an address';
|
|
4240
4364
|
return '<details class="attachFold"' + (rekick ? ' open' : '') + '>' +
|
|
4241
|
-
'<summary>' +
|
|
4365
|
+
'<summary>' + label + '</summary>' +
|
|
4242
4366
|
'<div class="attach">' +
|
|
4243
4367
|
'<input type="text" id="dom-' + iid + '" placeholder="myproject.example.com" autocomplete="off" spellcheck="false" maxlength="253" aria-label="A domain you own, for ' + esc(inst.slug) + '"' +
|
|
4244
|
-
(
|
|
4368
|
+
(prefill ? ' value="' + esc(inst.domain) + '"' : '') + '>' +
|
|
4245
4369
|
'<button type="button" class="btn small" data-attach="' + iid + '">' +
|
|
4246
|
-
(rekick ? 'Try the address again' : 'Use this domain') + '</button></div
|
|
4370
|
+
(rekick ? 'Try the address again' : repoint ? 'Change the address' : 'Use this domain') + '</button></div>' +
|
|
4371
|
+
/* true in every state the fold renders in: a REQUESTED project never
|
|
4372
|
+
answered anywhere yet, so "stops answering" would be a claim about a
|
|
4373
|
+
thing that never happened (the F16 rule) */
|
|
4374
|
+
(repoint ? '<p class="attachNote">Changing it lets <code>' + esc(inst.domain) + '</code> go — this project won’t use that address any more.</p>' : '') +
|
|
4375
|
+
'</details>';
|
|
4247
4376
|
}
|
|
4248
4377
|
|
|
4249
4378
|
function renderCard(inst) {
|
|
@@ -5427,8 +5556,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5427
5556
|
msg(id, 'That doesn’t look like a usable address — ' + esc(e.details.reason || 'use a real hostname like myproject.example.com') + '.', 'err');
|
|
5428
5557
|
} else if (e.code === 'domain_taken') {
|
|
5429
5558
|
msg(id, esc(e.message || 'That address is already in use by another project.'), 'err');
|
|
5430
|
-
} else if (e.code === 'domain_already_set') {
|
|
5431
|
-
msg(id, esc(e.message || 'This project already has an address — re-pointing isn’t supported here.'), 'err');
|
|
5432
5559
|
} else if (e.code === 'domain_not_attachable') {
|
|
5433
5560
|
msg(id, esc(e.message || 'An address can be attached once the instance settles.'), 'err');
|
|
5434
5561
|
} else if (e.code === 'intent_conflict') {
|
|
@@ -5591,7 +5718,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5591
5718
|
signed-out stranger lands on and it must not depend on identity */
|
|
5592
5719
|
.then(loadCatalog, loadCatalog)
|
|
5593
5720
|
/* the platform's own count is public too, so it loads either way */
|
|
5594
|
-
.then(loadSelfStats, loadSelfStats)
|
|
5721
|
+
.then(loadSelfStats, loadSelfStats)
|
|
5722
|
+
.then(loadSelfShipped, loadSelfShipped);
|
|
5595
5723
|
})();
|
|
5596
5724
|
</script>
|
|
5597
5725
|
</body>
|
|
@@ -51,6 +51,34 @@
|
|
|
51
51
|
{ "name": "Escape closes the card when no band is open", "continue": true,
|
|
52
52
|
"steps": [["press", "Escape"], ["wait", 150]],
|
|
53
53
|
"expect": { "dialog": false } },
|
|
54
|
+
{ "name": "the poster (task 1003301): art, five faces and +N, two figures on the art, the address, a member's hall door", "continue": true,
|
|
55
|
+
"steps": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://amazonprimea.com\"]"], ["wait", 900]],
|
|
56
|
+
"expect": { "dialog": true, "hasClass": [["#projCard", "has-art"]],
|
|
57
|
+
"visible": ["#cardArt", "#cardFaces", "#cardBuildersFig", "#cardShippedFig", "#cardOpen", "#cardHall"],
|
|
58
|
+
"count": [["#cardFaces img", 5], ["#projCard > div", 1]],
|
|
59
|
+
"text": [["#cardBuilders", "11"], ["#cardShipped", "0"], ["#cardRel", "you build here"], ["#cardRuns", "amazonprimea.com"], ["#cardFaces .facesMore", "+6"], ["#cardHall", "Builders' hall"]],
|
|
60
|
+
"href": [["#cardHall", "https://builders.amazonprimea.com/"], ["#cardOpen", "https://amazonprimea.com/"]] } },
|
|
61
|
+
{ "name": "the ladder: a project that answered nothing is the card that shipped before (disc, name, tagline, address, listed, one door)", "continue": true,
|
|
62
|
+
"steps": [["press", "Escape"], ["wait", 150], ["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://demo.cloudbongos.com\"]"], ["wait", 500]],
|
|
63
|
+
"expect": { "dialog": true, "hasClass": [["#projCard", "has-art", false]],
|
|
64
|
+
"hidden": ["#cardArt", "#cardFaces", "#cardFigs", "#cardBuildersFig", "#cardShippedFig", "#cardHall", "#cardRel", "#cardTypeL", "#cardOwnerL"],
|
|
65
|
+
"visible": ["#cardDot", "#cardName", "#cardTag", "#cardRuns", "#cardListed", "#cardOpen"],
|
|
66
|
+
"text": [["#cardRuns", "demo.cloudbongos.com"]] } },
|
|
67
|
+
{ "name": "the hall door is address-gated: a member of a project whose hall is its own site gets no door; the capped figure reads N+", "continue": true,
|
|
68
|
+
"steps": [["press", "Escape"], ["wait", 150], ["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://emersonian-circles.cloudbongos.com\"]"], ["wait", 500]],
|
|
69
|
+
"expect": { "dialog": true, "hidden": ["#cardArt", "#cardFaces", "#cardHall"], "visible": ["#cardFigs", "#cardBuildersFig", "#cardShippedFig", "#cardOpen"],
|
|
70
|
+
"text": [["#cardRel", "you build here"], ["#cardShipped", "30+"], ["#cardBuilders", "1"]] } },
|
|
71
|
+
{ "name": "the platform's own card: figures and faces by the same rule, its hall door from the stamped DOM link", "continue": true,
|
|
72
|
+
"steps": [["press", "Escape"], ["wait", 150], ["dispatchClick", "#selfOrb"], ["wait", 500]],
|
|
73
|
+
"expect": { "dialog": true, "hidden": ["#cardArt"], "visible": ["#cardBuildersFig", "#cardShippedFig", "#cardFaces", "#cardOpen", "#cardHall"],
|
|
74
|
+
"text": [["#cardRel", "yours"]], "href": [["#cardOpen", "/"]] } },
|
|
75
|
+
{ "name": "the table: the hall glyph rides the door cell on the one member row whose hall is elsewhere, and on the platform's row", "continue": true,
|
|
76
|
+
"steps": [["press", "Escape"], ["wait", 150], ["click", "#projTrig"], ["wait", 120], ["click", "a[data-sub=\"table\"]"], ["wait", 300]],
|
|
77
|
+
"expect": { "dialog": false, "visible": ["#selfHall"], "count": [["#mapRows tr[data-generated] .rowHall", 1], ["#mapRows tr[data-generated] td", 18]],
|
|
78
|
+
"href": [["#mapRows tr[data-generated] .rowHall", "https://builders.amazonprimea.com/"]] } },
|
|
79
|
+
{ "name": "back to the sky for the rest of the sequence", "continue": true,
|
|
80
|
+
"steps": [["click", "#projTrig"], ["wait", 120], ["click", "a[data-sub=\"sky\"]"], ["wait", 300]],
|
|
81
|
+
"expect": { "hidden": ["#sub-table-body"] } },
|
|
54
82
|
{ "name": "a solo Shift tap focuses the search on the map", "continue": true,
|
|
55
83
|
"steps": [["clickAt", 700, 600], ["press", "Shift"], ["wait", 100]],
|
|
56
84
|
"expect": { "activeId": "mapQ" } },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_": "The hub's states for modules/ui-design/kit/render.js (task 1003320): name → url + actions + expectations. `auth` picks the signed-in stub. Rendered dark + light at 1440/390/320 and audited against the platform floors.",
|
|
2
|
+
"_": "The hub's states for modules/ui-design/kit/render.js (task 1003320): name → url + actions + expectations. `auth` picks the signed-in stub. Rendered dark + light at 1440/390/320 and audited against the platform floors. The card states (task 1003301) walk the poster's ladder: the full poster, the artless two-up band, the all-null floor, the platform's own card.",
|
|
3
3
|
"page": "/projects",
|
|
4
4
|
"surface": "public-landing",
|
|
5
5
|
"modeQuery": "mode",
|
|
@@ -14,12 +14,15 @@
|
|
|
14
14
|
"in-map-kbd": { "auth": true, "actions": [["focus", "#projTrig"], ["press", "Enter"], ["wait", 350], ["press", "ArrowDown"], ["wait", 120]], "expect": { "expanded": true, "activeText": "Table" } },
|
|
15
15
|
"in-map-search": { "auth": true, "actions": [["clickIfVisible", "#qToggle"], ["wait", 200], ["fill", "#mapQ", "emer"], ["wait", 300]], "expect": { "value": [["#mapQ", "emer"]] } },
|
|
16
16
|
"in-map-mine-on": { "auth": true, "actions": [["check", "#onlyMine"], ["wait", 300]], "expect": { "checked": "#onlyMine" } },
|
|
17
|
-
"in-table": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 250], ["click", "a[data-sub=\"table\"]"], ["wait", 400]], "expect": { "visible": ["#sub-table-body", ".tblMast", ".mastObj"], "closed": "#menuProjects" } },
|
|
17
|
+
"in-table": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 250], ["click", "a[data-sub=\"table\"]"], ["wait", 400]], "expect": { "visible": ["#sub-table-body", ".tblMast", ".mastObj", "#selfHall"], "closed": "#menuProjects", "count": [["#mapRows tr[data-generated] .rowHall", 1]] } },
|
|
18
18
|
"in-mine": { "auth": true, "url": "/projects?view=mine", "expect": { "hidden": ["#qWrap"] } },
|
|
19
19
|
"in-new": { "auth": true, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
|
|
20
20
|
"out-new": { "auth": false, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
|
|
21
21
|
"out-map-projects": { "auth": false, "actions": [["click", "#projTrig"], ["wait", 350]], "expect": { "expanded": true, "hidden": ["#menuProjects [data-nav=\"mine\"]"] } },
|
|
22
|
-
"card": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb
|
|
22
|
+
"card": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://amazonprimea.com\"]"], ["wait", 900]], "expect": { "dialog": true, "hasClass": [["#projCard", "has-art"]], "visible": ["#cardArt", "#cardFaces", "#cardBuildersFig", "#cardShippedFig", "#cardOpen", "#cardHall"], "text": [["#cardBuilders", "11"], ["#cardShipped", "0"], ["#cardRel", "you build here"], ["#cardRuns", "amazonprimea.com"], ["#cardFaces .facesMore", "+6"]], "count": [["#cardFaces img", 5]], "href": [["#cardHall", "https://builders.amazonprimea.com/"]] } },
|
|
23
|
+
"card-noart": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://emersonian-circles.cloudbongos.com\"]"], ["wait", 600]], "expect": { "dialog": true, "hasClass": [["#projCard", "has-art", false]], "hidden": ["#cardArt", "#cardFaces", "#cardHall"], "visible": ["#cardBuildersFig", "#cardShippedFig", "#cardOpen"], "text": [["#cardShipped", "30+"], ["#cardRel", "you build here"]] } },
|
|
24
|
+
"card-floor": { "auth": true, "actions": [["dispatchClick", "#orbField a.worldOrb[data-origin=\"https://demo.cloudbongos.com\"]"], ["wait", 600]], "expect": { "dialog": true, "hidden": ["#cardArt", "#cardFaces", "#cardFigs", "#cardHall", "#cardRel", "#cardTypeL", "#cardOwnerL"], "visible": ["#cardOpen", "#cardRuns", "#cardListed"] } },
|
|
25
|
+
"card-self": { "auth": true, "actions": [["dispatchClick", "#selfOrb"], ["wait", 600]], "expect": { "dialog": true, "hidden": ["#cardArt"], "visible": ["#cardBuildersFig", "#cardShippedFig", "#cardFaces", "#cardOpen", "#cardHall"], "text": [["#cardRel", "yours"]] } }
|
|
23
26
|
},
|
|
24
27
|
"reducedMotion": { "auth": true, "actions": [["click", "#projTrig"], ["wait", 100]] }
|
|
25
28
|
}
|
|
@@ -118,6 +118,37 @@ function dnsUpsertEnv(inst, { publicIp, zone, proxied = false } = {}) {
|
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
// The RFC 5737 TEST-NET-1 sentinel a released hostname is parked at (task
|
|
122
|
+
// 1003237) — the shape scripts/gds/box-infra.js proved for a torn-down box
|
|
123
|
+
// (idea 309 / task 1234). 192.0.2.1 is reserved for documentation and is
|
|
124
|
+
// GUARANTEED never routable, so a name parked here can never dial platform
|
|
125
|
+
// infra or a recycled address.
|
|
126
|
+
const DNS_BLACKHOLE_IP = '192.0.2.1';
|
|
127
|
+
|
|
128
|
+
// The env a RELEASE invocation of the DNS hook needs: the abandoned hostname,
|
|
129
|
+
// parked at the blackhole sentinel. A re-point (task 1003237) leaves the old
|
|
130
|
+
// address UPSERTed at the platform's IP for a project that no longer answers
|
|
131
|
+
// there — the ADR 0181 §2 leak — and this is how the runner lets it go.
|
|
132
|
+
//
|
|
133
|
+
// REPOINT, not delete, for the reason box-infra.js records: deleting a name
|
|
134
|
+
// opens an NXDOMAIN window that resolvers negative-cache for the zone's SOA
|
|
135
|
+
// neg-TTL (~30 min, not editable), so a client that queried mid-gap stays
|
|
136
|
+
// broken long after. A name that always resolves — to a guaranteed-dead host —
|
|
137
|
+
// has nothing to negative-cache. It also needs only the UPSERT contract every
|
|
138
|
+
// BOX_DNS_HOOK implements, so no hook grows a new mode for this.
|
|
139
|
+
//
|
|
140
|
+
// proxied=0: a parked record must never ride the Cloudflare edge, which would
|
|
141
|
+
// answer for it. Takes a bare `hostname` rather than an instance — the whole
|
|
142
|
+
// point is that the row has already moved on. PURE.
|
|
143
|
+
function dnsReleaseEnv(hostname, { zone } = {}) {
|
|
144
|
+
return {
|
|
145
|
+
BOX_HOSTNAME: hostname,
|
|
146
|
+
BOX_IP: DNS_BLACKHOLE_IP,
|
|
147
|
+
CLOUDFLARE_ZONE: zone || zoneForDomain(hostname),
|
|
148
|
+
BOX_PROXIED: '0',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
// ── DNS token-scope PREFLIGHT (task 1980) ────────────────────────────────────
|
|
122
153
|
// Onboarding a NEW project on a domain the control-plane's Cloudflare token does NOT
|
|
123
154
|
// cover fails LATE: the DNS UPSERT (step 5) errors mid-provision (the Emersonian
|
|
@@ -208,10 +239,12 @@ function healthzCmd(inst) {
|
|
|
208
239
|
|
|
209
240
|
module.exports = {
|
|
210
241
|
CF_API_BASE,
|
|
242
|
+
DNS_BLACKHOLE_IP,
|
|
211
243
|
caddyBlock,
|
|
212
244
|
caddySnippetPath,
|
|
213
245
|
checkDnsTokenScope,
|
|
214
246
|
classifyZoneScope,
|
|
247
|
+
dnsReleaseEnv,
|
|
215
248
|
dnsUpsertEnv,
|
|
216
249
|
federateInstance,
|
|
217
250
|
federationHubOrigin,
|