@bongos/core 1.19.419 → 1.19.421

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.
Files changed (61) hide show
  1. package/.bongos-core.json +108 -58
  2. package/.claude/skills/new-project/SKILL.md +22 -11
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +2 -0
  5. package/clients/bongos-client/index.cjs +2 -0
  6. package/clients/bongos-client/index.d.ts +5 -1
  7. package/clients/bongos-client/index.mjs +2 -0
  8. package/docs/adr/0209-one-budget-for-the-account-existence-oracle.md +79 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +103 -5
  11. package/docs/api-reference.md +4 -3
  12. package/docs/copy-inventory.md +282 -269
  13. package/docs/copy-registry.json +487 -368
  14. package/docs/design/mocks/hub/mine.html +377 -0
  15. package/docs/design/projects-hub-direction-v2.md +25 -1
  16. package/docs/file-map.md +2 -0
  17. package/docs/module-api-changelog.md +4 -0
  18. package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
  19. package/modules/hall-ui/public/settings.html +3 -2
  20. package/modules/hall-ui/public/settings.js +10 -6
  21. package/modules/onboarding/routes/access-requests.js +16 -3
  22. package/modules/platform-identity/account-visibility.js +94 -3
  23. package/modules/platform-identity/platform-identity.js +50 -55
  24. package/modules/platform-identity/routes/connections.js +3 -3
  25. package/modules/platform-identity/tests/platform-identity.mjs +20 -0
  26. package/modules/provisioning/migrations/provisioning_013_project_detail.sql +54 -0
  27. package/modules/provisioning/onboard-plan.js +13 -0
  28. package/modules/provisioning/provisioning.js +113 -5
  29. package/modules/provisioning/routes/provisioning.js +105 -0
  30. package/modules/provisioning/tests/provisioning.mjs +84 -0
  31. package/modules/ui-design/kit/fixtures/featured-hub.json +1 -0
  32. package/modules/ui-design/kit/fixtures/provisioning-instances-empty.json +4 -0
  33. package/modules/ui-design/kit/fixtures/provisioning-instances.json +99 -0
  34. package/modules/ui-design/kit/lib.js +6 -2
  35. package/modules/ui-design/kit/probe.js +1 -1
  36. package/modules/ui-design/kit/render.js +1 -1
  37. package/modules/ui-design/kit/serve.js +63 -2
  38. package/package-lock.json +2 -2
  39. package/package.json +1 -1
  40. package/public-landing/assets/cosmos.css +224 -18
  41. package/public-landing/projects-empty.states.json +49 -0
  42. package/public-landing/projects-floor.states.json +63 -0
  43. package/public-landing/projects.html +273 -93
  44. package/public-landing/projects.probes.json +32 -7
  45. package/public-landing/projects.states.json +10 -6
  46. package/scripts/gds/fitness.js +9 -0
  47. package/scripts/gds/onboard.js +41 -1
  48. package/scripts/gds/provision-net.js +7 -3
  49. package/src/bongos/middleware/rate-limit.js +68 -2
  50. package/src/bongos/routes/auth.js +19 -12
  51. package/src/module-api.js +13 -1
  52. package/tests/account_existence_rate_limit.mjs +375 -0
  53. package/tests/account_privacy_flags.mjs +17 -10
  54. package/tests/effective_visibility_predicate.mjs +891 -0
  55. package/tests/module_api.mjs +1 -0
  56. package/tests/onboard.mjs +56 -0
  57. package/tests/profile_route.mjs +22 -11
  58. package/tests/projects_hub.mjs +102 -5
  59. package/tests/projects_hub_pre_uat.mjs +45 -11
  60. package/tests/provision.mjs +20 -0
  61. package/tests/ui_design_kit.mjs +32 -2
@@ -50,15 +50,10 @@
50
50
  #view-mine .hero,#view-manage .hero{max-width:none;}
51
51
 
52
52
  /* ── the manage view (task 1003041): container rhythm only — every component
53
- it draws (card, chip, addr, empty) is cosmos vocabulary ── */
54
- .backRow{margin:14px 0;font-weight:700;}
53
+ it draws (the head, the ledger, the choice cells, the rail, the doors) is
54
+ cosmos vocabulary, scoped to #view-manage there (task 1003345) ── */
55
55
  #view-manage .empty{text-align:left;}
56
56
  #view-manage .empty p{margin-bottom:0;}
57
- #view-manage .visOpt{display:flex;gap:10px;align-items:flex-start;padding:10px 12px;border:1px solid var(--rule);border-radius:10px;margin-bottom:8px;cursor:pointer;}
58
- #view-manage .visOpt:has(input:checked){border-color:var(--accent);}
59
- #view-manage .visOpt input{margin-top:4px;flex:none;}
60
- #view-manage .visOpt b{display:block;font-size:15px;}
61
- #view-manage .visOpt span{display:block;font-size:13px;color:var(--ink-soft);line-height:1.45;}
62
57
 
63
58
  /* The bongo pair on the New-project view. Component-scoped custom properties,
64
59
  never :root (C1). The drum head is SKIN — using --bg-card would turn it black
@@ -95,14 +90,17 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
95
90
  .wizField{display:block;margin-bottom:18px;}
96
91
  .wizField>span{display:block;font-size:13.5px;font-weight:700;color:var(--ink-soft);margin-bottom:7px;letter-spacing:0.2px;}
97
92
  .wizField small{font-weight:500;color:var(--ink-faint);}
98
- #view-new input[type=text]{
93
+ /* the About step's description is prose, so it is a textarea — it wears the
94
+ same control skin as every other field (task 1002334), never its own */
95
+ #view-new input[type=text], #view-new textarea{
99
96
  width:100%;font-family:var(--font-body);font-size:17px;font-weight:600;color:var(--ink);
100
97
  background:var(--bg-card);border:2px solid var(--ctl-line);border-radius:14px;padding:13px 16px;outline:none;
101
98
  transition:border-color .15s ease, box-shadow .15s ease;
102
99
  }
100
+ #view-new textarea{font-size:15.5px;line-height:1.5;resize:vertical;min-height:84px;display:block;}
103
101
  /* no fade toward the card — a 70% mix washed placeholders to 2.89:1 (task 1002749) */
104
- #view-new input[type=text]::placeholder{color:var(--ink-faint);font-weight:500;}
105
- #view-new input[type=text]:focus{border-color:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 13%,transparent);}
102
+ #view-new input[type=text]::placeholder, #view-new textarea::placeholder{color:var(--ink-faint);font-weight:500;}
103
+ #view-new input[type=text]:focus, #view-new textarea:focus{border-color:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 13%,transparent);}
106
104
  #view-new input[type=text].bad{border-color:var(--err);}
107
105
  .hint{font-size:13px;font-weight:600;color:var(--ink-faint);margin-top:7px;min-height:18px;}
108
106
  .hint.ok{color:var(--ok);}
@@ -146,6 +144,9 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
146
144
  .humanSteps .ghbtn{font-size:14px;padding:9px 16px;margin-top:10px;}
147
145
 
148
146
  .nav{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:26px;}
147
+ /* two controls sharing one end of a nav row — the About step's Skip beside its
148
+ Next (task 1002334). Wraps on a narrow card rather than crushing either. */
149
+ .navPair{display:inline-flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end;}
149
150
  .btn.big{width:100%;justify-content:center;font-size:18px;padding:17px 30px;}
150
151
 
151
152
  /* review list */
@@ -570,8 +571,8 @@ summary{min-height:24px;padding:3px 0;}
570
571
  <div class="wrap viewWrap">
571
572
  <div class="hero" style="padding-left:0;padding-right:0">
572
573
  <h1>Your projects</h1>
573
- <p class="lede">Every project you own — what it is, whether it is answering, and the controls that matter.
574
- Each card says what the platform <b>last verified</b>, and when.</p>
574
+ <p class="lede">Every project you own: what it is, whether it is answering, and the controls that matter.
575
+ Each row says what the platform <b>last verified</b>, and when.</p>
575
576
  </div>
576
577
 
577
578
  <div class="card">
@@ -611,11 +612,12 @@ summary{min-height:24px;padding:3px 0;}
611
612
  <p class="mineEmpty" id="mineEmpty" hidden>Nothing matches that. <button type="button" class="linklike" id="mineClear">Clear the filters</button></p>
612
613
 
613
614
  <section class="empty" id="empty" hidden>
614
- <p>You don't own any projects yet. Name one, pick a home for its code, and it gets an address of its own in this sky.</p>
615
+ <h2>nothing of yours in this sky yet</h2>
616
+ <p>Name a project, pick a home for its code, and it gets an address of its own here. It shows up in this list the moment you file it.</p>
615
617
  <a class="btn" href="?view=new" data-nav-to="new">Create your first project</a>
616
618
  </section>
617
619
  <ul class="projects" id="projectsList"></ul>
618
- <p class="finePrint">Buttons here ask the platform to act; each card reports only what was verified, and when. A standup keeps going even with this tab closed.</p>
620
+ <p class="finePrint">Buttons here ask the platform to act; each row reports only what was verified, and when. A standup keeps going even with this tab closed.</p>
619
621
  </section>
620
622
  </div>
621
623
  </div>
@@ -634,6 +636,19 @@ summary{min-height:24px;padding:3px 0;}
634
636
  <div class="wrap viewWrap">
635
637
  <p class="backRow"><a href="?view=mine" data-nav-to="mine">← Your projects</a></p>
636
638
 
639
+ <!-- the head stands in EVERY state (task 1003345): the page's one h1 lives
640
+ here, outside #mMain, so the signed-out, not-found and loading states
641
+ keep a visible heading (the kit's one-header-main-h1 floor). The planet
642
+ and the chip fill only once a project has rendered; the clause hides
643
+ with #mMain (cosmos, :has). -->
644
+ <div class="hero mHead" style="padding-left:0;padding-right:0">
645
+ <span id="mOrb"></span>
646
+ <div>
647
+ <div class="titleRow"><h1 id="mTitle"><span id="mName">Manage</span></h1><span id="mChip"></span></div>
648
+ <p class="lede">Its address, its repository, what the platform last <b>verified</b> about it, and the controls you have today.</p>
649
+ </div>
650
+ </div>
651
+
637
652
  <!-- signed out: the same identity-only door the mine view uses (ADR 0143) -->
638
653
  <section id="mGate" class="card" hidden>
639
654
  <p class="lede">This is a project’s management page — it answers only to the person who owns it. Sign in with GitHub to open yours.</p>
@@ -654,19 +669,18 @@ summary{min-height:24px;padding:3px 0;}
654
669
  <div class="loading" id="mLoading">Reading the project…</div>
655
670
 
656
671
  <section id="mMain" hidden>
657
- <div class="hero" style="padding-left:0;padding-right:0">
658
- <h1 id="mTitle">Manage</h1>
659
- <p class="lede">Its address, its repository, what the platform last <b>verified</b> about it — and the controls you have today.</p>
660
- </div>
661
-
662
- <div class="card">
663
- <div class="projHead"><span class="projName" id="mName">…</span><span id="mChip"></span></div>
664
- <div class="projMeta" id="mMeta" hidden></div>
665
- <div class="addr" id="mAddr" hidden></div>
666
- <p class="finePrint" id="mCreated" hidden></p>
672
+ <!-- the ledger of facts (task 1003345): each fact keeps its id and fills
673
+ as one label · value pair; a fact with no answer hides — never a dash -->
674
+ <div class="card" id="mFacts">
675
+ <div class="ledger">
676
+ <div id="mMeta" hidden></div>
677
+ <div id="mAddr" hidden></div>
678
+ <div id="mCreated" hidden></div>
679
+ <div id="mPlanet" hidden></div>
680
+ </div>
667
681
  <!-- the card's own story, on the project's own page (task 1003232): the
668
- per-flow strip, the honest snag, and the doors — the same
669
- stripFor/errMarkup/canRetry/canRevive the list card reads -->
682
+ per-flow rail, the honest snag, and the doors — the same
683
+ stripFor/errMarkup/canRetry/canRevive the list row reads -->
670
684
  <div id="mStrip"></div>
671
685
  <div class="projMsg err" id="mErr" hidden></div>
672
686
  <div class="projMsg" id="mMsg" role="status" aria-live="polite"></div>
@@ -678,8 +692,8 @@ summary{min-height:24px;padding:3px 0;}
678
692
  private direction is refused (server + copy) until the private-repo
679
693
  deploy fix lands, because the deploy pull cannot read a private repo
680
694
  (walk finding N4). -->
681
- <div class="card" id="mRepoVisibility">
682
- <h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Repository visibility</h2>
695
+ <div class="card mSec" id="mRepoVisibility">
696
+ <h2>Repository visibility</h2>
683
697
  <div id="mRepoVis"><p class="finePrint">Checking the repository on GitHub…</p></div>
684
698
  <!-- the limitation itself, one click away (grade follow-up task 1003077):
685
699
  the refusal above states it; this fold explains it and links the
@@ -698,8 +712,8 @@ summary{min-height:24px;padding:3px 0;}
698
712
  a change reaches a live project only by the restart the runner
699
713
  performs (ADR 0190). The fold is a SIBLING of the volatile slot (the
700
714
  #mRepoVisWhy shape) so no re-render can drop it. -->
701
- <div class="card" id="mVisibility">
702
- <h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Who can see it</h2>
715
+ <div class="card mSec" id="mVisibility">
716
+ <h2>Who can see it</h2>
703
717
  <div id="mVis"><p class="finePrint">Reading the project’s settings…</p></div>
704
718
  <details class="finePrint" id="mVisWhat">
705
719
  <summary>What “only signed-in members” changes</summary>
@@ -713,8 +727,8 @@ summary{min-height:24px;padding:3px 0;}
713
727
  RUNS is read back from its own public manifest (brand.project.
714
728
  joinability) — never from the save's reply, which is a receipt. The
715
729
  fold is a SIBLING of the volatile slot so no re-render can drop it. -->
716
- <div class="card" id="mJoinability">
717
- <h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">How people join</h2>
730
+ <div class="card mSec" id="mJoinability">
731
+ <h2>How people join</h2>
718
732
  <div id="mJoin"><p class="finePrint">Reading the project’s settings…</p></div>
719
733
  <details class="finePrint" id="mJoinWhat">
720
734
  <summary>What each choice changes</summary>
@@ -730,8 +744,8 @@ summary{min-height:24px;padding:3px 0;}
730
744
  much as for a healthy one, which is why it asks for the project's own
731
745
  name typed back: it is the only control on this page that ends
732
746
  something, and the card's one-click confirm is too light for it. -->
733
- <div class="card" id="mDelete">
734
- <h2 style="font-family:var(--font-display);font-size:19px;margin-bottom:12px;">Take it offline</h2>
747
+ <div class="card mSec" id="mDelete">
748
+ <h2>Take it offline</h2>
735
749
  <div id="mDel"><p class="finePrint">Reading the project…</p></div>
736
750
  <!-- the teardown-vs-delete distinction, stated once as a SIBLING of the
737
751
  volatile slot (the #mRepoVisWhy shape) so no re-render can drop it.
@@ -748,7 +762,7 @@ summary{min-height:24px;padding:3px 0;}
748
762
 
749
763
  <!-- ══════════════════ VIEW · NEW PROJECT ══════════════════════════════════
750
764
  The whole create wizard, absorbed from /create (task 1002763; direction
751
- §4 — that route now 302s here). Seven panels behind the five-beat rail; the
765
+ §4 — that route now 302s here). Eight panels behind the six-beat rail; the
752
766
  wizard block in the script below drives them. Same trust boundary as
753
767
  ever: every button here only ENQUEUES a provisioning request (ADR 0111).
754
768
  The name panel carries a static `on` so a no-script visitor still reads
@@ -759,9 +773,10 @@ summary{min-height:24px;padding:3px 0;}
759
773
  <ol class="steps" aria-label="Creation steps">
760
774
  <li class="now" data-step="1" aria-current="step"><span class="snum">1</span><span class="slbl">Name</span></li>
761
775
  <li data-step="2"><span class="snum">2</span><span class="slbl">Type</span></li>
762
- <li data-step="3"><span class="snum">3</span><span class="slbl">Home</span></li>
763
- <li data-step="4"><span class="snum">4</span><span class="slbl">Address</span></li>
764
- <li data-step="5"><span class="snum">5</span><span class="slbl">Create</span></li>
776
+ <li data-step="3"><span class="snum">3</span><span class="slbl">About</span></li>
777
+ <li data-step="4"><span class="snum">4</span><span class="slbl">Home</span></li>
778
+ <li data-step="5"><span class="snum">5</span><span class="slbl">Address</span></li>
779
+ <li data-step="6"><span class="snum">6</span><span class="slbl">Create</span></li>
765
780
  </ol>
766
781
 
767
782
  <div class="createSplit">
@@ -841,11 +856,45 @@ summary{min-height:24px;padding:3px 0;}
841
856
  </form>
842
857
  </section>
843
858
 
844
- <!-- step 3 · home (repo + mode) -->
859
+ <!-- step 3 · about (task 1002334) — the OPTIONAL project-detail step.
860
+ The one panel in the wizard that can be walked past without an
861
+ answer, so the skip is a real button of its own, sitting beside
862
+ Next rather than hidden as "leave it blank and press Next" — an
863
+ optional step whose escape hatch has to be inferred is not
864
+ optional in practice. What skipping costs is stated ON the panel
865
+ (ADR 0182 D4: no description ⇒ the project is off the public map
866
+ until it has one), because a consequence discovered afterwards is
867
+ not a choice the owner made. -->
868
+ <section class="panel" id="panel-detail" aria-labelledby="detail-h">
869
+ <h1 id="detail-h">Tell us about it</h1>
870
+ <p class="lede">Optional — skip it and nothing about your project changes. Answering shapes what we set up for you, and a description is what puts the project on the public map.</p>
871
+ <form data-next="4">
872
+ <label class="wizField"><span>What are you building? <small>(a sentence or two)</small></span>
873
+ <textarea id="f-desc" rows="3" maxlength="600" placeholder="A co-op farming game where the seasons never repeat."></textarea>
874
+ </label>
875
+ <div class="wizField"><span id="teamLbl">Who’s building it?</span>
876
+ <div class="typeCards" id="teamCards" role="group" aria-labelledby="teamLbl">
877
+ <button type="button" class="tpill" data-team="solo" aria-pressed="false">Just me</button>
878
+ <button type="button" class="tpill" data-team="small-team" aria-pressed="false">A small team</button>
879
+ <button type="button" class="tpill" data-team="community" aria-pressed="false">An open community</button>
880
+ </div>
881
+ </div>
882
+ <div class="reassure" id="detailSkipNote"></div>
883
+ <div class="nav">
884
+ <button type="button" class="btn ghost" data-back="2">← Back</button>
885
+ <span class="navPair">
886
+ <button type="button" class="btn ghost" id="skipDetail">Skip for now</button>
887
+ <button type="submit" class="btn">Next →</button>
888
+ </span>
889
+ </div>
890
+ </form>
891
+ </section>
892
+
893
+ <!-- step 4 · home (repo + mode) -->
845
894
  <section class="panel" id="panel-home" aria-labelledby="home-h">
846
895
  <h1 id="home-h">Where the code lives</h1>
847
896
  <p class="lede">Every project is a real GitHub repository that you own. Start from a clean slate, or bring a repo you already have.</p>
848
- <form data-next="4">
897
+ <form data-next="5">
849
898
  <div class="choices" role="group" aria-label="Starting point">
850
899
  <button type="button" class="choice" id="c-green" aria-pressed="true">
851
900
  <!-- token-built, not platform orange — public-landing/ ships wholesale to
@@ -904,13 +953,13 @@ summary{min-height:24px;padding:3px 0;}
904
953
  </div>
905
954
  <div class="hint" id="h-home">&nbsp;</div>
906
955
  <div class="nav">
907
- <button type="button" class="btn ghost" data-back="2">← Back</button>
956
+ <button type="button" class="btn ghost" data-back="3">← Back</button>
908
957
  <button type="submit" class="btn">Next →</button>
909
958
  </div>
910
959
  </form>
911
960
  </section>
912
961
 
913
- <!-- step 4 · address — a CONFIRMATION, not a form (task 1002704: every project
962
+ <!-- step 5 · address — a CONFIRMATION, not a form (task 1002704: every project
914
963
  gets its slug subdomain assigned up front). The domainless default was a
915
964
  proven dead-end (QA walk, task 1002702).
916
965
  Three choices, all legible without opening anything (task 1002898 N8):
@@ -922,7 +971,7 @@ summary{min-height:24px;padding:3px 0;}
922
971
  <section class="panel" id="panel-address" aria-labelledby="addr-h">
923
972
  <h1 id="addr-h">Its address in the sky</h1>
924
973
  <p class="lede">The handle names your project on the platform — and its address comes with it. Once it stands up, a few GitHub steps (the next screen walks you through them) switch sign-in on.</p>
925
- <form data-next="5">
974
+ <form data-next="6">
926
975
  <label class="wizField"><span>Project handle</span>
927
976
  <input type="text" id="f-slug" placeholder="mercury" autocomplete="off" maxlength="40" spellcheck="false">
928
977
  <div class="hint" id="h-slug">&nbsp;</div>
@@ -944,13 +993,13 @@ summary{min-height:24px;padding:3px 0;}
944
993
  </label>
945
994
  </div>
946
995
  <div class="nav">
947
- <button type="button" class="btn ghost" data-back="3">← Back</button>
996
+ <button type="button" class="btn ghost" data-back="4">← Back</button>
948
997
  <button type="submit" class="btn">Next →</button>
949
998
  </div>
950
999
  </form>
951
1000
  </section>
952
1001
 
953
- <!-- step 5 · review + create -->
1002
+ <!-- step 6 · review + create -->
954
1003
  <section class="panel" id="panel-review" aria-labelledby="rev-h">
955
1004
  <h1 id="rev-h">Ready to roll</h1>
956
1005
  <p class="lede">One tap files the request and the platform starts standing your project up — server, database, address. Here's the beat you're about to drop:</p>
@@ -958,7 +1007,7 @@ summary{min-height:24px;padding:3px 0;}
958
1007
  <div class="alert" id="createAlert" role="alert"></div>
959
1008
  <div class="nav" style="flex-direction:column;align-items:stretch;">
960
1009
  <button type="button" class="btn big" id="doCreate">Create my project</button>
961
- <button type="button" class="btn ghost" data-back="4" style="align-self:flex-start;margin-top:10px;">← Back</button>
1010
+ <button type="button" class="btn ghost" data-back="5" style="align-self:flex-start;margin-top:10px;">← Back</button>
962
1011
  </div>
963
1012
  <p class="finePrint">This files the request; the control plane does the real standup and nothing touches servers from this page. You'll get the few steps only you can do right after.</p>
964
1013
  </section>
@@ -1243,7 +1292,6 @@ summary{min-height:24px;padding:3px 0;}
1243
1292
  were orphans). Keeping the per-project variety is what makes a sky legible;
1244
1293
  deriving it from contract tokens is what makes it survive a re-brand. */
1245
1294
  var ORB_ACCENTS = ['var(--sea)', 'var(--terracotta)', 'var(--gold-soft)', 'var(--sun-lit)', 'var(--accent)'];
1246
- var ORB_GLOWS = ORB_ACCENTS.map(function (t) { return 'color-mix(in srgb,' + t + ' 45%,transparent)'; });
1247
1295
  /* THE PLANET TEMPLATES (task 1003319; the materials, task 1003337). A project's
1248
1296
  orb is one of the world's three whole objects (blob / pair / bead) in one of
1249
1297
  four materials (chrome — the first round, named by omission — frost, gold,
@@ -1968,7 +2016,7 @@ summary{min-height:24px;padding:3px 0;}
1968
2016
  function loadCatalog() {
1969
2017
  fetch(API + '/projects/featured', { credentials: 'same-origin' })
1970
2018
  .then(function (res) { if (!res.ok) throw new Error('catalog'); return res.json(); })
1971
- .then(function (data) { catalog = (data && data.projects) || []; assignAccents(); applyMapFilter(); })
2019
+ .then(function (data) { catalog = (data && data.projects) || []; assignAccents(); applyMapFilter(); repaintPlanets(); })
1972
2020
  .catch(function () {
1973
2021
  /* a child instance has no catalog module at all — say so plainly
1974
2022
  rather than leave an empty sky with no explanation */
@@ -2488,13 +2536,16 @@ summary{min-height:24px;padding:3px 0;}
2488
2536
  var root = $('view-new');
2489
2537
 
2490
2538
  /* ── state ── */
2491
- /* -v2 since task 1002772: the step indices renumbered (Type joined as 2),
2492
- so a draft written under the five-step numbering must be ignored, not
2493
- reinterpreted — a stale step 4 would land on Address and skip the Type
2494
- gate with the rail already marking it done. */
2495
- var DRAFT_KEY = 'cb-create-draft-v2';
2539
+ /* -v3 since task 1002334: the indices renumbered AGAIN (About joined as 3),
2540
+ so a draft written under either older numbering must be ignored, not
2541
+ reinterpreted — a stale step 4 meant Address under v1 and Home under v3,
2542
+ and replaying it would land on the wrong panel with the rail already
2543
+ marking the skipped ones done. Bump this key on EVERY renumbering. */
2544
+ var DRAFT_KEY = 'cb-create-draft-v3';
2496
2545
  var CREATED_KEY = 'cb-created-instance';
2497
- var state = { step: 0, name: '', projType: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
2546
+ /* `description` + `teamShape` are the optional About step (task 1002334) —
2547
+ both legitimately stay empty, and empty is what the create request omits. */
2548
+ var state = { step: 0, name: '', projType: null, description: '', teamShape: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
2498
2549
  try { Object.assign(state, JSON.parse(sessionStorage.getItem(DRAFT_KEY) || '{}')); } catch (e) {}
2499
2550
  var authKnown = false; /* flips true when the boot's /me settles, either way */
2500
2551
 
@@ -2508,7 +2559,7 @@ summary{min-height:24px;padding:3px 0;}
2508
2559
  state.name = handedName.trim().slice(0, 80);
2509
2560
  state.slug = deriveSlug(state.name); state.slugTouched = false;
2510
2561
  state.repo = deriveRepo(state.name); state.repoTouched = false;
2511
- state.step = state.step >= 1 && state.step <= 5 ? state.step : 1;
2562
+ state.step = state.step >= 1 && state.step <= 6 ? state.step : 1;
2512
2563
  try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify(state)); } catch (e) {}
2513
2564
  }
2514
2565
  } catch (e) {}
@@ -2700,16 +2751,17 @@ summary{min-height:24px;padding:3px 0;}
2700
2751
  }
2701
2752
 
2702
2753
  /* ── step navigation ──
2703
- Six steps since task 1002772: gate 0 · name 1 · type 2 · home 3 ·
2704
- address 4 · review 5 · done 6 (the rail shows 1-5; done marks it all). */
2705
- var panels = { 0: 'panel-gate', 1: 'panel-name', 2: 'panel-type', 3: 'panel-home', 4: 'panel-address', 5: 'panel-review', 6: 'panel-done' };
2754
+ Seven steps since task 1002334: gate 0 · name 1 · type 2 · about 3 ·
2755
+ home 4 · address 5 · review 6 · done 7 (the rail shows 1-6; done marks it
2756
+ all). About is the one step Next can pass with nothing filled in. */
2757
+ var panels = { 0: 'panel-gate', 1: 'panel-name', 2: 'panel-type', 3: 'panel-detail', 4: 'panel-home', 5: 'panel-address', 6: 'panel-review', 7: 'panel-done' };
2706
2758
  /* the rail at the done panel (task 1003244): "done" is the Create step
2707
2759
  still running — the marker stays on 5 until the project actually
2708
2760
  answers, and only then does the whole rail read finished. aria-current
2709
2761
  tells a screen reader the same thing. */
2710
2762
  function paintRail(step) {
2711
- var landed = step === 6 && !!(live && live.instStatus === 'active');
2712
- var cur = step === 6 ? 5 : step;
2763
+ var landed = step === 7 && !!(live && live.instStatus === 'active');
2764
+ var cur = step === 7 ? 6 : step;
2713
2765
  root.querySelectorAll('.steps li').forEach(function (li) {
2714
2766
  var n = Number(li.getAttribute('data-step'));
2715
2767
  var now = !landed && n === cur;
@@ -2720,7 +2772,7 @@ summary{min-height:24px;padding:3px 0;}
2720
2772
  the button role, the tab stop and the data-goto the delegated handler
2721
2773
  below reads. The done panel offers none — the project is filed, and
2722
2774
  there is nothing behind it to change. */
2723
- if (step < 6 && n < cur) {
2775
+ if (step < 7 && n < cur) {
2724
2776
  li.setAttribute('role', 'button');
2725
2777
  li.setAttribute('tabindex', '0');
2726
2778
  li.setAttribute('data-goto', String(n));
@@ -2739,7 +2791,7 @@ summary{min-height:24px;padding:3px 0;}
2739
2791
  paintRail(step);
2740
2792
  saveDraft();
2741
2793
  /* the done step takes the whole card — the bongo pair steps aside */
2742
- root.querySelector('.createSplit').classList.toggle('wizDone', step === 6);
2794
+ root.querySelector('.createSplit').classList.toggle('wizDone', step === 7);
2743
2795
  /* focus moves ONLY on a deliberate step (the hub rule setView already
2744
2796
  applies to this view: never on boot, never on a background settle) */
2745
2797
  if (moveFocus) {
@@ -2749,15 +2801,15 @@ summary{min-height:24px;padding:3px 0;}
2749
2801
  /* the repo probe is authenticated — never fire it before /me settles
2750
2802
  (a pre-auth probe 401s for strangers and, worse, could seed the
2751
2803
  draft's owner with myLogin()'s placeholder) */
2752
- if (step === 3) { syncHome(); if (authKnown) ensureRepoAccess(); }
2804
+ if (step === 4) { syncHome(); if (authKnown) ensureRepoAccess(); }
2753
2805
  /* a resumed draft can land PAST the Home step with the probe never run —
2754
2806
  Create would then file a repo it promised to make but never did */
2755
- else if (step > 3 && step < 6 && authKnown && repoAccess === null) ensureRepoAccess();
2807
+ else if (step > 4 && step < 7 && authKnown && repoAccess === null) ensureRepoAccess();
2756
2808
  /* re-check on every entry, not just on typing: a resumed draft arrives here with
2757
2809
  a handle nobody has asked the server about, and a handle free ten minutes ago
2758
2810
  may not be now (task 1002898 N10) */
2759
- if (step === 4) { checkSlugAvailable(); renderAddrPanel(); }
2760
- if (step === 5) renderReview();
2811
+ if (step === 5) { checkSlugAvailable(); renderAddrPanel(); }
2812
+ if (step === 6) renderReview();
2761
2813
  }
2762
2814
 
2763
2815
  /* One delegated pair on the rail, so paintRail can add and drop data-goto
@@ -2850,6 +2902,65 @@ summary{min-height:24px;padding:3px 0;}
2850
2902
  });
2851
2903
  paintType(state.projType || null);
2852
2904
 
2905
+ /* ── the OPTIONAL About step (task 1002334) ──
2906
+ Two answers, both skippable. The description is half the publish gate
2907
+ (ADR 0182 D4) and the team shape is what the setup recommendations read;
2908
+ neither gates the standup, and neither is ever guessed on the owner's
2909
+ behalf. paintDetail keeps the consequence visible while they are still on
2910
+ the panel — it flips between "here's what you'll get" and "here's what
2911
+ skipping costs", so pressing Skip is an informed choice rather than a
2912
+ discovery made three screens later. */
2913
+ var TEAM_META = {
2914
+ 'solo': 'Just you. We keep the setup light and skip the joining machinery.',
2915
+ 'small-team': 'A handful of people you know. We suggest a setup built around invites.',
2916
+ 'community': 'Open to whoever turns up. We suggest a setup built for people applying to join.'
2917
+ };
2918
+ function teamLabel(t) {
2919
+ var b = t && root.querySelector('#teamCards .tpill[data-team="' + t + '"]');
2920
+ return b ? b.textContent : '';
2921
+ }
2922
+ function paintTeam(t) {
2923
+ root.querySelectorAll('#teamCards .tpill').forEach(function (b) {
2924
+ b.setAttribute('aria-pressed', String(b.getAttribute('data-team') === t));
2925
+ });
2926
+ }
2927
+ function paintDetail() {
2928
+ var note = $('detailSkipNote');
2929
+ var told = String(state.description || '').trim();
2930
+ var parts = [];
2931
+ if (!told) {
2932
+ parts.push('No description yet — your project still stands up, it just stays off the public map until it has one. You can add it any time from its page.');
2933
+ } else {
2934
+ parts.push('That description is what puts your project on the public map.');
2935
+ }
2936
+ if (state.teamShape && TEAM_META[state.teamShape]) parts.push(TEAM_META[state.teamShape]);
2937
+ note.textContent = parts.join(' ');
2938
+ note.classList.add('on');
2939
+ }
2940
+ $('f-desc').value = state.description || '';
2941
+ $('f-desc').addEventListener('input', function () {
2942
+ state.description = this.value.slice(0, 600);
2943
+ paintDetail();
2944
+ saveDraft();
2945
+ });
2946
+ root.querySelectorAll('#teamCards .tpill').forEach(function (b) {
2947
+ b.addEventListener('click', function () {
2948
+ /* clicking the chosen pill again UNSAYS it — the only way back to "I did
2949
+ not answer", which an optional question has to keep offering */
2950
+ var pick = b.getAttribute('data-team');
2951
+ state.teamShape = state.teamShape === pick ? null : pick;
2952
+ paintTeam(state.teamShape);
2953
+ paintDetail();
2954
+ saveDraft();
2955
+ });
2956
+ });
2957
+ paintTeam(state.teamShape || null);
2958
+ paintDetail();
2959
+ /* Skip = go on with whatever is (or isn't) filled in. It deliberately does
2960
+ NOT erase an answer already given: a visitor who typed a description and
2961
+ then pressed Skip meant "I'm done here", not "throw that away". */
2962
+ $('skipDetail').addEventListener('click', function () { show(4, true); });
2963
+
2853
2964
  /* ── GitHub-integrated repo picker (adopt) + auto-create (fresh) — task 1002099 / ADR 0143.
2854
2965
  Progressive enhancement: probe GET /github/repos once we reach the Home step.
2855
2966
  200 → we hold repo access → show the picker (adopt) / name field (fresh);
@@ -3046,7 +3157,9 @@ summary{min-height:24px;padding:3px 0;}
3046
3157
  }
3047
3158
  return true;
3048
3159
  }
3049
- if (step === 3) {
3160
+ /* step 3 (About) has no clause: it is the optional one, and Next past an
3161
+ empty answer is the whole point (task 1002334). */
3162
+ if (step === 4) {
3050
3163
  var homeErr = function (msg) { $('h-home').textContent = msg; $('h-home').className = 'hint err'; return false; };
3051
3164
  if (repoAccess === 'need-connect') return homeErr('Allow GitHub repo access to pick or create your repository — or choose “enter it manually”.');
3052
3165
  if (repoAccess === true && state.mode !== 'adopt') {
@@ -3061,7 +3174,7 @@ summary{min-height:24px;padding:3px 0;}
3061
3174
  $('h-home').innerHTML = '&nbsp;'; $('h-home').className = 'hint';
3062
3175
  return true;
3063
3176
  }
3064
- if (step === 4) {
3177
+ if (step === 5) {
3065
3178
  if (!validSlug(state.slug)) {
3066
3179
  $('h-slug').textContent = '2–40 lowercase letters, numbers, or dashes; no leading/trailing/double dash.';
3067
3180
  $('h-slug').className = 'hint err'; $('f-slug').classList.add('bad');
@@ -3127,6 +3240,13 @@ summary{min-height:24px;padding:3px 0;}
3127
3240
  var rows = [
3128
3241
  ['Project', esc(state.name)],
3129
3242
  ['Type', typeLabel(state.projType) || 'Not declared'],
3243
+ /* the About step is optional, so its row states the CONSEQUENCE of the
3244
+ skip rather than a bare em-dash — the review is the last screen before
3245
+ Create, and "off the map" should never be a surprise (task 1002334) */
3246
+ ['About', String(state.description || '').trim()
3247
+ ? esc(String(state.description).trim())
3248
+ : '<em style="font-weight:600;color:var(--warn)">not said — off the public map until you add it</em>'],
3249
+ ['Who’s building it', teamLabel(state.teamShape) || 'Not said'],
3130
3250
  ['Starting from', state.mode === 'adopt' ? 'A repo you already have' : 'A fresh, empty repo'],
3131
3251
  ['Repository', '<code>' + esc(state.owner + '/' + state.repo) + '</code>'],
3132
3252
  ['Handle', '<code>' + esc(state.slug) + '</code>'],
@@ -3213,6 +3333,11 @@ summary{min-height:24px;padding:3px 0;}
3213
3333
  body: JSON.stringify({
3214
3334
  slug: state.slug,
3215
3335
  type: state.projType || undefined,
3336
+ /* the optional About answers (task 1002334) — omitted when skipped,
3337
+ because absent means SKIPPED and an empty string would read as an
3338
+ answered-but-blank description to the publish gate */
3339
+ description: String(state.description || '').trim() || undefined,
3340
+ detail: state.teamShape ? { team_shape: state.teamShape } : undefined,
3216
3341
  hosting_shape: 'standalone',
3217
3342
  /* ONLY a bring-your-own domain travels (task 1002898 N2). The server owns
3218
3343
  the assigned <slug>.<apex> default (task 1002704) and applies it to an
@@ -3257,7 +3382,7 @@ summary{min-height:24px;padding:3px 0;}
3257
3382
  sessionStorage.removeItem(DRAFT_KEY);
3258
3383
  } catch (e) {}
3259
3384
  renderDone();
3260
- show(6, true);
3385
+ show(7, true);
3261
3386
  startProgress(data.instance && data.instance.id);
3262
3387
  }).catch(function (err) {
3263
3388
  if (String(err && err.message) === 'signing in') return;
@@ -3265,7 +3390,7 @@ summary{min-height:24px;padding:3px 0;}
3265
3390
  if (err && err.gotoHome) {
3266
3391
  /* A repo-creation problem (name taken / reconnect) — bounce back to the Home
3267
3392
  step with the reason, so the user fixes it where the repo is chosen. */
3268
- show(3, true);
3393
+ show(4, true);
3269
3394
  $('h-home').textContent = err.homeMsg || 'Please revisit the repository step.';
3270
3395
  $('h-home').className = 'hint err';
3271
3396
  return;
@@ -3799,7 +3924,7 @@ summary{min-height:24px;padding:3px 0;}
3799
3924
  5s tick never clobbers an open dropdown or mid-flight button) */
3800
3925
  if (inst && inst.status !== live.instStatus) {
3801
3926
  live.instStatus = inst.status; renderSteps();
3802
- if (state.step === 6) paintRail(6); /* the rail finishes only when the project answers */
3927
+ if (state.step === 7) paintRail(7); /* the rail finishes only when the project answers */
3803
3928
  }
3804
3929
  var terminal = renderProgress(inst);
3805
3930
  if (!terminal) pollTimer = setTimeout(tick, 5000);
@@ -3836,13 +3961,14 @@ summary{min-height:24px;padding:3px 0;}
3836
3961
  lastStepsHtml = '';
3837
3962
  lastProgHtml = '';
3838
3963
  try { sessionStorage.removeItem(CREATED_KEY); sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
3839
- state = { step: 1, name: '', projType: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
3964
+ state = { step: 1, name: '', projType: null, description: '', teamShape: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
3840
3965
  $('newName').value = ''; $('f-owner').value = ''; $('f-repo').value = ''; $('f-slug').value = ''; $('f-domain').value = '';
3841
- $('f-noaddr').checked = false;
3966
+ $('f-noaddr').checked = false; $('f-desc').value = '';
3842
3967
  /* Re-seed the owner from the signed-in account for the next project. */
3843
3968
  if (me && me.github_login) { state.owner = me.github_login; $('f-owner').value = state.owner; }
3844
3969
  $('h-name').innerHTML = '&nbsp;';
3845
3970
  paintType(null);
3971
+ paintTeam(null);
3846
3972
  setMode('greenfield');
3847
3973
  show(1, true);
3848
3974
  });
@@ -3875,7 +4001,7 @@ summary{min-height:24px;padding:3px 0;}
3875
4001
  manifestState = created.manifest_state; manifestPending = true;
3876
4002
  }
3877
4003
  renderDone();
3878
- show(6, !!focus);
4004
+ show(7, !!focus);
3879
4005
  startProgress(created.id);
3880
4006
  resumePolls();
3881
4007
  return;
@@ -3883,7 +4009,7 @@ summary{min-height:24px;padding:3px 0;}
3883
4009
  /* before identity settles, never render past the name step: deeper steps
3884
4010
  carry authenticated side effects (the Home-step repo probe), and onAuth()
3885
4011
  re-enters to the real draft step the moment /me answers */
3886
- var step = authKnown && state.step >= 1 && state.step <= 5 ? state.step : 1;
4012
+ var step = authKnown && state.step >= 1 && state.step <= 6 ? state.step : 1;
3887
4013
  if (step === 1) {
3888
4014
  show(1, false);
3889
4015
  if (focus) $('newName').focus();
@@ -4130,19 +4256,57 @@ summary{min-height:24px;padding:3px 0;}
4130
4256
  return !!inst.open_intent; /* e.g. a queued teardown or domain-attach on an active instance */
4131
4257
  }
4132
4258
 
4133
- /* ── the orb: the gate's project sphere, glow keyed to verification ────── */
4134
-
4259
+ /* ── the planet on a row (task 1003345): the SAME object the sky shows. A
4260
+ catalog-matched instance takes its catalog row's template and accent
4261
+ (curated, or position-walked by assignAccents); an owned-only one walks
4262
+ by its slug, the rule the map's appended entries use — so one project is
4263
+ one object in both views and never re-colours between them. ── */
4264
+ function planetOf(inst) {
4265
+ var h = inst.domain ? hostOf(inst.domain) : '';
4266
+ for (var i = 0; h && i < catalog.length; i++) {
4267
+ if (hostOf(catalog[i].origin) === h) {
4268
+ return { tpl: templateOf(catalog[i]), acc: accentOf(catalog[i]), curated: ORB_TEMPLATES.indexOf(catalog[i].template) !== -1 };
4269
+ }
4270
+ }
4271
+ return { tpl: slugTemplate(inst.slug), acc: ORB_ACCENTS[slugAccent(inst.slug)], curated: false };
4272
+ }
4273
+ /* shape[-material]-style → the data attributes cosmos.css draws from (the map's rule) */
4274
+ function planetAttrs(pl) {
4275
+ var parts = pl.tpl.split('-');
4276
+ return ' data-template="' + esc(pl.tpl) + '" data-shape="' + esc(parts[0]) +
4277
+ '" data-material="' + esc(parts.length === 3 ? parts[1] : 'chrome') +
4278
+ '" data-style="' + esc(parts[parts.length - 1]) + '"';
4279
+ }
4280
+ var PLANET_SHAPE_WORDS = { blob: 'the blob', pair: 'the bongo pair', bead: 'the bead' };
4281
+ var PLANET_STYLE_WORDS = { tint: 'tinted', wash: 'washed', plain: 'plain', chrome: 'plain' };
4282
+ /* the planet as a clause for the manage ledger: the accent as a dot, then shape, material, style */
4283
+ function planetWords(pl) {
4284
+ var parts = pl.tpl.split('-');
4285
+ var material = parts.length === 3 ? parts[1] : 'chrome';
4286
+ return '<span class="swDot" style="--sw:' + pl.acc + '" aria-hidden="true"></span>' +
4287
+ esc((PLANET_SHAPE_WORDS[parts[0]] || parts[0]) + ' in ' + material + ', ' +
4288
+ (PLANET_STYLE_WORDS[parts[parts.length - 1]] || parts[parts.length - 1]));
4289
+ }
4290
+ /* the catalog can land after a view rendered — the rows and the manage head take its planets then */
4291
+ function repaintPlanets() {
4292
+ Object.keys(byId).forEach(function (id) { renderCard(byId[id]); });
4293
+ if (manageInst) paintManagePlanet(manageInst);
4294
+ }
4295
+
4296
+ /* ── the row's object: the planet, its state on the object (ghost = torn
4297
+ down, ashen = failed its check, dim = a stale probe); only a live
4298
+ project's planet is a door to its site ── */
4135
4299
  function orbFor(inst) {
4136
- var k = slugAccent(inst.slug);
4137
- var cls = 'orb';
4300
+ var pl = planetOf(inst);
4301
+ var cls = 'worldOrb';
4138
4302
  if (inst.status === 'torn_down' || inst.status === 'tearing_down') cls += ' ghost';
4139
4303
  else if (inst.status === 'error' || (inst.status === 'active' && inst.last_verified_up === false)) cls += ' ashen';
4140
4304
  else if (inst.status === 'active' && !freshUp(inst)) cls += ' dim';
4141
- var style = '--acc:' + ORB_ACCENTS[k] + ';--glow:' + ORB_GLOWS[k];
4305
+ var attrs = planetAttrs(pl) + ' style="--acc:' + pl.acc + ';--d:64px"';
4142
4306
  if (inst.status === 'active' && inst.domain) {
4143
- return '<a class="' + cls + '" style="' + style + '" href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener" title="Open ' + esc(inst.slug) + '" aria-label="Open ' + esc(inst.slug) + '"></a>';
4307
+ return '<a class="' + cls + '"' + attrs + ' href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener" title="Open ' + esc(inst.slug) + '" aria-label="Open ' + esc(inst.slug) + '"><span class="disc"></span></a>';
4144
4308
  }
4145
- return '<span class="' + cls + '" style="' + style + '" aria-hidden="true"></span>';
4309
+ return '<span class="' + cls + '"' + attrs + ' aria-hidden="true"><span class="disc"></span></span>';
4146
4310
  }
4147
4311
 
4148
4312
  /* ── progress strips: each flow speaks its own stages (direction §4).
@@ -4628,6 +4792,17 @@ summary{min-height:24px;padding:3px 0;}
4628
4792
  encodeURIComponent('/projects?view=manage&id=' + manageId);
4629
4793
  }
4630
4794
 
4795
+ /* one label · value pair of the manage ledger */
4796
+ function ledgerRow(r) { return '<span class="k">' + r[0] + '</span><span class="v">' + r[1] + '</span>'; }
4797
+ /* the head's planet and the ledger's planet row (task 1003345) — one resolution
4798
+ (planetOf) for both, so the head, the row and the sky can never disagree; the
4799
+ planet row is the slot the picker (task 1003347) fills */
4800
+ function paintManagePlanet(inst) {
4801
+ var pl = planetOf(inst);
4802
+ $('mOrb').innerHTML = orbFor(inst);
4803
+ $('mPlanet').innerHTML = ledgerRow(['planet', planetWords(pl) + (pl.curated ? '' : '<small>chosen for it by the platform.</small>')]);
4804
+ $('mPlanet').hidden = false;
4805
+ }
4631
4806
  function renderManage(inst) {
4632
4807
  manageInst = inst;
4633
4808
  $('mLoading').hidden = true;
@@ -4638,20 +4813,22 @@ summary{min-height:24px;padding:3px 0;}
4638
4813
  /* the ONE chip site of this view — chipFor() or nothing, so the manage
4639
4814
  page can never tell a different liveness story than the card (C6) */
4640
4815
  $('mChip').innerHTML = chipFor(inst);
4641
- var meta = [];
4816
+ paintManagePlanet(inst);
4817
+ var rows = [];
4642
4818
  if (inst.target_ref) {
4643
- meta.push('repo <a href="https://github.com/' + esc(inst.target_ref) +
4644
- '" target="_blank" rel="noopener"><code>' + esc(inst.target_ref) + '</code></a>');
4819
+ rows.push(['repository', '<a href="https://github.com/' + esc(inst.target_ref) +
4820
+ '" target="_blank" rel="noopener">' + esc(inst.target_ref) + '</a>']);
4645
4821
  }
4646
- if (inst.hosting_shape) meta.push(esc(SHAPE_LABEL[inst.hosting_shape] || inst.hosting_shape));
4647
- if (inst.onboard_mode === 'adopt') meta.push('adopted repo');
4648
- $('mMeta').innerHTML = meta.map(function (m) { return '<span>' + m + '</span>'; }).join('');
4649
- $('mMeta').hidden = !meta.length;
4822
+ var server = inst.hosting_shape ? esc(SHAPE_LABEL[inst.hosting_shape] || inst.hosting_shape) : '';
4823
+ if (inst.onboard_mode === 'adopt') server += (server ? ', ' : '') + 'adopted repo';
4824
+ if (server) rows.push(['server', server]);
4825
+ $('mMeta').innerHTML = rows.map(ledgerRow).join('');
4826
+ $('mMeta').hidden = !rows.length;
4650
4827
  var addr = addressRow(inst);
4651
- $('mAddr').innerHTML = addr;
4828
+ $('mAddr').innerHTML = addr ? ledgerRow(['address', addr]) : '';
4652
4829
  $('mAddr').hidden = !addr;
4653
4830
  /* a fact with no answer hides — never a dash (the card rule) */
4654
- $('mCreated').textContent = inst.created_at ? 'Created ' + listedDate(inst.created_at) : '';
4831
+ $('mCreated').innerHTML = inst.created_at ? ledgerRow(['created', esc(listedDate(inst.created_at))]) : '';
4655
4832
  $('mCreated').hidden = !inst.created_at;
4656
4833
  /* fetch-free like everything else here, so the once-a-minute tick may call
4657
4834
  it: the delete section reads only status + open_intent, both already in
@@ -4661,8 +4838,7 @@ summary{min-height:24px;padding:3px 0;}
4661
4838
  only, so a queued settings push shows up on the tick without a reload */
4662
4839
  renderVisState(inst);
4663
4840
  renderJoinState(inst);
4664
- /* the page names its project — in the heading and in the tab */
4665
- $('mTitle').textContent = inst.slug || 'Manage';
4841
+ /* the page names its project — the heading holds #mName; and in the tab */
4666
4842
  document.title = (inst.slug ? inst.slug + ' — ' : '') + BASE_TITLE;
4667
4843
  /* the card's own story, on the project's own page (task 1003232): the
4668
4844
  per-flow strip, the honest snag, and the doors the list card has */
@@ -4761,6 +4937,10 @@ summary{min-height:24px;padding:3px 0;}
4761
4937
  $('mGate').hidden = true;
4762
4938
  $('mMissing').hidden = true;
4763
4939
  $('mLoading').hidden = false;
4940
+ /* the head stands in every state — a fresh load must not wear the last project's name */
4941
+ $('mName').textContent = 'Manage';
4942
+ $('mChip').innerHTML = '';
4943
+ $('mOrb').innerHTML = '';
4764
4944
  }
4765
4945
  var asked = manageId; /* a stale answer must never paint a newer project */
4766
4946
  fetch(API + '/provisioning/instances/' + encodeURIComponent(manageId), { credentials: 'same-origin' })