@bongos/core 1.21.105 → 1.21.106

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 (36) hide show
  1. package/.bongos-core.json +53 -43
  2. package/docs/copy-inventory.md +368 -366
  3. package/docs/copy-registry.json +475 -457
  4. package/docs/design/project-startup-direction.md +14 -0
  5. package/docs/module-api-changelog.md +2 -0
  6. package/docs/page-inventory.json +10 -3
  7. package/docs/page-readings.json +1023 -977
  8. package/modules/public-landing/public/projects-hall-live.states.json +3 -2
  9. package/modules/public-landing/public/projects-setup.states.json +91 -0
  10. package/modules/public-landing/public/projects.html +535 -804
  11. package/modules/public-landing/public/projects.probes.json +3 -3
  12. package/modules/public-landing/public/projects.states.json +1 -1
  13. package/modules/ui-design/kit/fixtures/provisioning-instances-hall-live.json +22 -2
  14. package/modules/ui-design/kit/fixtures/provisioning-instances-setup.json +195 -0
  15. package/modules/ui-design/kit/serve.js +22 -4
  16. package/package-lock.json +2 -2
  17. package/package.json +1 -1
  18. package/release-notes.json +6 -0
  19. package/src/module-api.js +1 -1
  20. package/tests/cosmos_token_contrast.mjs +10 -18
  21. package/tests/github_handback_any_device.mjs +4 -3
  22. package/tests/github_one_trip.mjs +45 -47
  23. package/tests/github_org_account.mjs +2 -2
  24. package/tests/github_pick_up.mjs +40 -29
  25. package/tests/onboard.mjs +13 -7
  26. package/tests/project_invite_ui.mjs +6 -3
  27. package/tests/project_modules_ui.mjs +3 -3
  28. package/tests/projects_hub.mjs +8 -9
  29. package/tests/projects_hub_cloud_hop.mjs +77 -125
  30. package/tests/projects_hub_dns_ready.mjs +42 -51
  31. package/tests/projects_hub_pre_uat.mjs +68 -69
  32. package/tests/public_landing_projects.mjs +45 -102
  33. package/tests/setup_status_facts.mjs +400 -0
  34. package/tests/wizard_github_feedback.mjs +90 -87
  35. package/tests/wizard_one_page.mjs +11 -9
  36. package/tests/wizard_auto_open.mjs +0 -94
@@ -78,11 +78,11 @@
78
78
  by scope (#view-new: no card ground, no progress rail since task 1004504,
79
79
  the doors as pills); what lands here is the wizard’s own structure —
80
80
  fields on one bottom hairline, the choices as cells of one band, the review
81
- as a ledger, the human steps as a numbered ledger, the standup strip as the
82
- same strip. No literal color anywhere: every surface is a cosmos token or a
81
+ as a ledger, part 03's checked facts as one list (.setFact). No literal color
82
+ anywhere: every surface is a cosmos token or a
83
83
  color-mix over one (C1/C2/C4); no plate, no box shadow, no glow — halos are
84
84
  outlines. Naming: the wizard's field wrapper is .wizField — cosmos owns
85
- .field (the orb map) — and its done checklist is .humanSteps. Its inputs are
85
+ .field (the orb map). Its inputs are
86
86
  scoped to the view so the glass
87
87
  search pills (#mapQ, #mineQ) keep their own look. */
88
88
  .panel{display:none;}
@@ -486,21 +486,7 @@ body:has(#view-new:not([hidden])) footer.foot{position:relative;z-index:1;backgr
486
486
  .reassure.on{display:block;}
487
487
  .reassure p{margin:0 0 10px;}
488
488
 
489
- /* the GitHub doors: one-click deep links to GitHub's own pages (the visitor
490
- acts in GitHub's UI; we fill in the blanks). GitHub's brand black used to
491
- be pinned here; in the world every GitHub door is the INVERTED pill the
492
- gate's sign-in wears — ink on the ground — one grammar, still not the
493
- accent, so a re-brand never paints it. */
494
- .ghbtn{
495
- display:inline-flex;align-items:center;justify-content:center;gap:10px;text-decoration:none;cursor:pointer;
496
- min-height:44px;background:var(--ink);color:var(--bg);border:1px solid var(--ink);border-radius:999px;
497
- font-family:var(--font-body);font-weight:500;font-size:15px;padding:0 22px;
498
- box-shadow:none;transition:background .15s ease;
499
- }
500
- .ghbtn:hover{background:color-mix(in srgb,var(--ink) 88%,var(--bg));transform:none;}
501
- .ghbtn:active{transform:none;}
502
- .ghbtn .gh{width:17px;height:17px;fill:currentColor;}
503
- .humanSteps .ghbtn{min-height:40px;font-size:14.5px;padding:0 18px;margin-top:12px;}
489
+
504
490
 
505
491
  /* the doors under a step: back on the left, the primary on the right; the
506
492
  About step's skip beside its next (task 1002334) wraps rather than crushes */
@@ -510,81 +496,61 @@ body:has(#view-new:not([hidden])) footer.foot{position:relative;z-index:1;backgr
510
496
  /* the review: a ledger of what is about to be filed — label · value rows on
511
497
  soft hairlines, the last row closed */
512
498
 
513
- /* ── done / human steps: the tick leaves (the rail says filed), the title
514
- stands in the display voice, the steps only you can play are a numbered
515
- ledger on soft hairlines — the finished one reads DONE in the ok ink, a
516
- locked one keeps muted TOKENS, never subtree opacity (task 1002751) ── */
517
- .doneHead{display:block;margin-bottom:8px;}
518
- .doneHead .tick{display:none;}
519
- .stepsHead{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;font-weight:400;color:var(--ink-faint);margin:40px 0 0;}
520
- .humanSteps{list-style:none;counter-reset:hs;margin-top:16px;max-width:680px;}
521
- .humanSteps>li{
522
- display:grid;grid-template-columns:44px 1fr;gap:0 16px;position:static;background:none;border:0;border-top:1px solid var(--rule-soft);border-radius:0;
523
- padding:16px 0 18px;margin:0;counter-increment:hs;
524
- }
525
- .humanSteps>li:last-child{border-bottom:1px solid var(--rule-soft);}
526
- .humanSteps>li>*{grid-column:2;min-width:0;}
527
- .humanSteps>li::before{
528
- content:counter(hs,decimal-leading-zero);grid-column:1;grid-row:1;position:static;width:auto;height:auto;border-radius:0;
529
- background:none;color:var(--ink-faint);font-family:var(--label-face);font-weight:400;font-size:var(--label-size);letter-spacing:var(--label-space);
530
- display:block;padding-top:4px;
499
+ /* ── part 03 after Create (task 1004683; the design of record
500
+ docs/design/mocks/day-night-rooms/setup-status.html): one sentence saying what is
501
+ happening now, then two groups of facts the server checked, Its home and GitHub.
502
+ Each fact is a dot, the fact itself, and on the right when it was checked or what
503
+ it found. No bar, no stage dots, nothing said twice; a fact that is not done keeps
504
+ muted TOKENS, never subtree opacity (task 1002751). The cloud hop opens only when
505
+ asked, below the facts. ── */
506
+ #panel-done:not(.on) .doneBody,#panel-done:not(.on) .again{display:none;}
507
+ #panel-done.on .partSub{display:none;}
508
+ .setNow{margin:10px 0 0;font-size:16px;font-weight:400;line-height:1.5;color:var(--ink-soft);max-width:62ch;min-height:24px;}
509
+ .setNow b{font-weight:500;color:var(--ink);}
510
+ .setNow a{display:inline-block;min-height:24px;line-height:24px;color:var(--link-ink);text-decoration:underline;text-underline-offset:3px;}
511
+ .setFactGrp{margin-top:22px;max-width:680px;}
512
+ .setFactGrp>h3{margin:0 0 4px;font-family:var(--label-face);font-size:var(--label-size);font-weight:400;letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
513
+ .setFact{display:grid;grid-template-columns:24px minmax(0,1fr) auto;gap:2px 12px;align-items:center;padding:11px 0;border-top:1px solid var(--rule);}
514
+ .setFact:first-child{border-top:0;}
515
+ .setFact .d{box-sizing:border-box;width:24px;height:24px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;border:1px solid var(--glass-edge);color:var(--ink-soft);}
516
+ .setFact .t{font-size:15px;font-weight:400;line-height:1.4;color:var(--ink-soft);min-width:0;overflow-wrap:anywhere;}
517
+ .setFact .t small{display:block;margin-top:2px;font-size:12.5px;line-height:1.45;color:var(--ink-faint);}
518
+ .setFact .t small a{display:inline-block;min-height:24px;padding:3px 0;line-height:18px;color:var(--link-ink);text-decoration:underline;text-underline-offset:3px;}
519
+ .setFact .r{font-size:12.5px;color:var(--ink-faint);text-align:right;white-space:nowrap;}
520
+ .setFact .optional{margin-left:8px;font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
521
+ .setFact.done .d{background:color-mix(in srgb,var(--ok) 12%,transparent);color:var(--ok);border-color:var(--ok);}
522
+ .setFact.done .t,.setFact.now .t,.setFact.you .t{color:var(--ink);}
523
+ .setFact.now .d{border:2px solid var(--accent);}
524
+ .setFact.you .d{background:color-mix(in srgb,var(--accent) 16%,transparent);border-color:color-mix(in srgb,var(--accent) 60%,transparent);color:var(--ink);}
525
+ .setFact.bad .d{background:color-mix(in srgb,var(--err) 10%,transparent);color:var(--err);border-color:var(--err);}
526
+ .setFact.bad .t{color:var(--err);}
527
+ .setFact .acts{grid-column:2 / 4;display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:8px;}
528
+ .setFact .acts .btn{min-height:44px;}
529
+ .setFact .acts .linklike{min-height:24px;padding:3px 0;font-weight:400;}
530
+ .setFact .acts details{flex-basis:100%;font-size:13px;color:var(--ink-soft);}
531
+ .setFact .acts details p{margin:6px 0 0;overflow-wrap:anywhere;}
532
+ /* the account choice sits beside Connect GitHub, inside the Approve fact (task 1004651) */
533
+ .setFact .acctPick{flex:1 1 220px;max-width:none;}
534
+ #view-new .part .setFact .acctPick .wizField{margin:0;}
535
+ .setFact .acctPick .acctTyped{margin-top:10px;}
536
+ .setFact .acctPick .hint{min-height:0;}
537
+ @media (prefers-reduced-motion:no-preference){
538
+ .setFact.now .d{animation:factBreathe 1.4s ease-in-out infinite;}
539
+ @keyframes factBreathe{0%,100%{border-color:var(--accent)}50%{border-color:color-mix(in srgb,var(--accent) 35%,transparent)}}
531
540
  }
532
- .humanSteps b{display:block;font-size:16px;font-weight:500;color:var(--ink);}
533
- .humanSteps p{font-size:14px;font-weight:400;color:var(--ink-soft);line-height:1.5;margin-top:4px;max-width:56ch;overflow-wrap:anywhere;}
534
- /* a repo the platform already handled is named as a plain inline link (task 1002855) —
535
- the .ghbtn treatment reads as an action to take, and there is nothing here to do */
536
- .humanSteps p a{color:var(--ink);font-weight:400;text-decoration:underline;text-underline-offset:3px;display:inline-block;padding:3px 0;min-height:24px;line-height:18px;}
537
- .humanSteps p a:hover{color:var(--ink);}
538
- /* live checklist states (task 1002694): a step the platform has verified done, and a
539
- step that can't be performed yet (its prerequisite — usually the domain — is missing) */
540
- .humanSteps>li.sdone{border-color:var(--rule-soft);background:none;}
541
- .humanSteps>li.sdone::before{content:"done";text-transform:uppercase;background:none;color:var(--ok);}
542
- .humanSteps>li.sdone b{color:var(--ink-soft);}
543
- .humanSteps>li.slocked{border-color:var(--rule-soft);}
544
- .humanSteps>li.slocked b{color:var(--ink-soft);}
545
- .humanSteps>li.slocked p{color:var(--ink-faint);}
546
- .humanSteps>li.slocked::before{color:var(--ink-faint);}
547
- .humanSteps details{margin-top:10px;}
548
- .humanSteps summary{cursor:pointer;font-weight:400;font-size:13.5px;color:var(--ink-soft);}
549
- .humanSteps summary:hover{color:var(--ink);}
550
- .humanSteps details p{margin-top:8px;}
551
- .kv{display:flex;align-items:center;gap:10px;margin-top:8px;background:none;border:0;border-top:1px solid var(--rule-soft);border-radius:0;padding:8px 0;font-size:12.5px;}
552
- .kv .kk{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;font-weight:400;color:var(--ink-faint);flex:0 0 auto;}
553
- .kv code{font-family:var(--font-mono);font-size:12.5px;color:var(--ink-soft);word-break:break-all;flex:1;}
554
- .copy{
555
- flex:0 0 auto;cursor:pointer;min-height:28px;border-radius:999px;background:none;border:1px solid var(--rule);
556
- font-family:var(--font-body);font-size:12.5px;font-weight:400;color:var(--ink-soft);padding:0 12px;
557
- transition:border-color .12s ease,color .12s ease;
541
+ @media (max-width:520px){
542
+ .setFact{grid-template-columns:24px minmax(0,1fr);}
543
+ .setFact .r{grid-column:2;text-align:left;white-space:normal;}
544
+ .setFact .acts{grid-column:2;}
558
545
  }
559
- .copy:hover{border-color:var(--ink-faint);color:var(--ink);}
560
- .copy.did{background:none;border-color:var(--ok);color:var(--ok);}
546
+ .hopLink{display:inline-block;margin-top:16px;min-height:24px;padding:3px 0;background:none;border:0;cursor:pointer;font-family:var(--font-body);font-size:13px;color:var(--link-ink);text-decoration:underline;text-underline-offset:3px;}
547
+ .hopLink:hover{color:var(--ink);}
548
+ .hopLink[hidden]{display:none;}
561
549
  .samePath{font-size:13px;font-weight:400;color:var(--ink-faint);margin-top:18px;}
562
550
  .samePath a{display:inline-block;padding:3px 0;min-height:24px;line-height:18px;}
563
- #liveProgress{margin-top:4px;}
564
- /* the sign-in wait inside its step + the "open it when ready" tick (task 1004548) */
565
- .humanSteps .prog{margin-top:14px;}
566
- .autoOpen{display:flex;align-items:flex-start;gap:10px;margin-top:14px;font-size:14px;line-height:1.45;color:var(--ink-soft);cursor:pointer;min-height:24px;}
567
- .autoOpen input{width:18px;height:18px;margin:1px 0 0;flex:0 0 auto;accent-color:var(--accent);cursor:pointer;}
568
-
569
- /* ── the done panel's two moments (task 1004578): WAIT — one status line, a
570
- thin bar and the cloud hop — then ARRIVE — the steps one at a time. The
571
- panel's data-moment says which; "plain" (a snag, a teardown, a stopped
572
- watch) is the panel as it always was. While waiting the strip, the locked
573
- checklist and the later-lines stand down; the sign-in step can be opened
574
- in place (the one step a founder can do before the project answers). ── */
575
- .hop{display:none;margin-top:22px;max-width:680px;}
576
- #panel-done[data-moment="wait"] .hop{display:block;}
577
- #panel-done[data-moment="wait"] #liveProgress,
578
- #panel-done[data-moment="wait"] #wizInvite,
579
- #panel-done[data-moment="wait"] #wizInviteLater,
580
- #panel-done[data-moment="wait"] #doneModules,
581
- #panel-done[data-moment="wait"] .samePath,
582
- #panel-done[data-moment="wait"] .again{display:none;}
583
- /* part 03 before Create: the heading, its state and the sentence; the rest waits */
584
- #panel-done:not(.on) .doneBody,#panel-done:not(.on) .again{display:none;}
585
- .hopStatus{font-size:14px;font-weight:400;line-height:1.45;color:var(--ink-soft);margin:0;min-height:20px;}
586
- .hopBar{height:3px;background:var(--rule-soft);margin:10px 0 12px;overflow:hidden;}
587
- .hopBar span{display:block;height:100%;width:0;background:var(--accent);transition:width .8s ease;}
551
+ /* the cloud hop (task 1004578), opened in place by "Play cloud hop while you wait" */
552
+ .hop{margin-top:14px;max-width:680px;}
553
+ .hop[hidden]{display:none;}
588
554
  .hopStage{position:relative;border-radius:14px;overflow:hidden;background:var(--space-deep);}
589
555
  .hopStage{transition:opacity .5s ease;}
590
556
  .hop.hopFading .hopStage{opacity:0;}
@@ -597,66 +563,13 @@ body:has(#view-new:not([hidden])) footer.foot{position:relative;z-index:1;backgr
597
563
  .hopScore{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-soft);}
598
564
  /* reduced motion: the sky stands still and there is nothing to steer */
599
565
  .hop.isStill .hopRow{display:none;}
600
- /* ARRIVE — the strip's three dots stand down (its note stays: it carries the address) */
601
- #panel-done[data-moment="arrive"] #liveProgress .stages{display:none;}
602
- /* the checklist (task 1004648, the mockup's steps): one dot per step — an empty ring,
603
- a tick in the ok ink once the server has verified it — and every step on show */
604
- #panel-done .humanSteps{margin-top:18px;}
605
- #panel-done .humanSteps>li{grid-template-columns:24px 1fr;gap:0 12px;padding:12px 0 14px;border-top:0;}
606
- #panel-done .humanSteps>li:last-child{border-bottom:0;}
607
- #panel-done .humanSteps>li::before{content:"";box-sizing:border-box;width:24px;height:24px;padding:0;border-radius:50%;border:1px solid var(--glass-edge);
608
- display:inline-flex;align-items:center;justify-content:center;font-family:var(--font-body);font-size:13px;letter-spacing:0;}
609
- #panel-done .humanSteps>li.sdone::before{content:"✓";text-transform:none;color:var(--ok);background:color-mix(in srgb,var(--ok) 12%,transparent);border:1px solid var(--ok);}
610
- #panel-done .humanSteps>li.slocked::before{border:1px solid var(--rule-soft);}
611
- /* a stopped step (task 1004652, the mockup's "!" dot): what stopped, and the button that fixes it */
612
- #panel-done .humanSteps>li.sstopped::before{content:"!";color:var(--err);background:color-mix(in srgb,var(--err) 10%,transparent);border:1px solid var(--err);}
613
- .humanSteps .linklike{display:block;margin-top:10px;min-height:24px;padding:3px 0;background:none;border:0;cursor:pointer;font-family:var(--font-body);font-size:13.5px;color:var(--link-ink);text-decoration:underline;text-underline-offset:3px;text-align:left;}
614
- .humanSteps .linklike:hover{color:var(--ink);}
615
- /* the optional step (command-line sign-in, task 1004649): a dashed ring until it ticks */
616
- #panel-done .humanSteps>li.soptional:not(.sdone)::before{border-style:dashed;}
617
- #panel-done .humanSteps b{font-size:15px;line-height:24px;}
566
+ /* every required fact done: "<name> is alive.", the invite, and the Enter Genesis door */
618
567
  .alive{margin-top:18px;padding-top:18px;border-top:1px solid var(--rule);}
568
+ .alive[hidden]{display:none;}
619
569
  #view-new .aliveH{margin:0;font-family:var(--font-display);font-size:28px;font-weight:200;letter-spacing:-.02em;line-height:1.2;text-transform:none;color:var(--ink);}
620
- #view-new #panel-done .lede{margin:8px 0 0;font-size:14.5px;line-height:1.5;color:var(--ink-soft);}
621
- #view-new #panel-done .lede a{color:var(--link-ink);text-decoration:underline;text-underline-offset:3px;}
622
-
623
- /* ── the standup strip (task 2083), drawn as the lifecycle rail (DESIGN.md):
624
- three hairlines, a 7px dot, one lit step — no plate, no connector, no
625
- pulse. The vocabulary is the page's fixed one (F22): filed / standing it
626
- up / live; isLive in the ok ink, isErr in the error ink, flowDown neutral.
627
- The marks mirror cosmos's .strip on the derived tokens (task 1002751). ── */
628
- .prog{background:none;border:0;border-radius:0;padding:0;margin-top:22px;max-width:520px;}
629
- .prog .stages{display:grid;grid-template-columns:repeat(3,1fr);gap:0 10px;list-style:none;margin:0;}
630
- .prog .stages li{display:block;position:relative;border-top:1px solid var(--rule);padding:12px 0 0;}
631
- .prog .stages li:not(:first-child)::before{content:none;}
632
- .prog .sdot{position:absolute;top:-4px;left:0;width:7px;height:7px;border:1px solid var(--rule);border-radius:50%;background:var(--bg);display:block;}
633
- .prog .stages li.done{border-top-color:var(--ink-soft);}
634
- .prog .stages li.done .sdot{border-color:var(--ink-soft);background:var(--ink-soft);}
635
- .prog .stages li.done .sdot::after{content:none;}
636
- .prog .stages li.now{border-top-color:var(--accent);}
637
- .prog .stages li.now .sdot{border-color:var(--accent);background:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
638
- .prog .stages li.now .sdot::after{content:none;animation:none;}
639
- .prog .slbl{display:block;font-family:var(--label-face);font-size:var(--label-size);font-weight:400;letter-spacing:var(--label-space);text-transform:uppercase;line-height:1.3;color:var(--ink-faint);}
640
- .prog .stages li.now .slbl{color:var(--ink);}
641
- .prog .stages li.done .slbl{color:var(--ink-soft);}
642
- .prog .pnote{text-align:left;font-size:13px;font-weight:400;line-height:1.45;color:var(--ink-soft);margin:8px 0 0;min-height:0;}
643
- .prog .pnote a{color:var(--ink);font-weight:400;text-decoration:underline;text-underline-offset:3px;display:inline-block;padding:3px 0;min-height:24px;line-height:18px;}
644
- .prog.isLive{background:none;border-color:transparent;}
645
- .prog.isLive .stages li.done{border-top-color:var(--ok);}
646
- .prog.isLive .stages li.done .sdot{border-color:var(--ok);background:var(--ok);}
647
- .prog.isLive .slbl{color:var(--ok);}
648
- .prog.isLive .stages li.done .slbl{color:var(--ok);}
649
- .prog.isLive .pnote{color:var(--ok);font-size:13.5px;}
650
- .prog.isErr{background:none;border-color:transparent;}
651
- .prog.isErr .stages li.now{border-top-color:var(--err);}
652
- .prog.isErr .stages li.now .sdot{border-color:var(--err);background:var(--err);outline:4px solid color-mix(in srgb,var(--err) 22%,transparent);}
653
- .prog.isErr .pnote{color:var(--err);}
654
- /* teardown's calm neutral (F22), the same vocabulary cosmos's .strip.flowDown speaks */
655
- .prog.flowDown .stages li.done{border-top-color:var(--ink-soft);}
656
- .prog.flowDown .stages li.now{border-top-color:var(--ink-soft);}
657
- .prog.flowDown .stages li.now .sdot{border-color:var(--ink-soft);background:var(--ink-soft);outline:none;}
658
- .prog.flowDown .slbl{color:var(--ink-faint);}
659
- .prog.flowDown .stages li.now .slbl,.prog.flowDown .stages li.done .slbl{color:var(--ink-soft);}
570
+ .aliveDoor{display:flex;margin-top:16px;}
571
+ .aliveDoor .btn{min-height:48px;padding:0 28px;}
572
+
660
573
  .again{
661
574
  display:inline-block;margin-top:14px;font-size:13.5px;font-weight:400;color:var(--ink-soft);
662
575
  background:none;border:none;cursor:pointer;font-family:var(--font-body);text-decoration:underline;
@@ -1815,7 +1728,7 @@ summary{min-height:24px;padding:3px 0;}
1815
1728
  <!-- visibility stated on EVERY repo path (task 1002721) -->
1816
1729
  <div class="hint">We never change it: public stays public, private stays private.</div>
1817
1730
  </div>
1818
- <div id="ghFresh" class="wizField" hidden>
1731
+ <div id="ghFresh" class="wizField" data-onboard-step="github-repo" hidden>
1819
1732
  <span>New repository name</span>
1820
1733
  <input type="text" id="ghNewName" placeholder="moonbase" autocomplete="off" maxlength="100" aria-label="New repository name">
1821
1734
  <!-- visibility is stated on EVERY repo path (task 1002721); updateFreshPreview()
@@ -2120,21 +2033,30 @@ summary{min-height:24px;padding:3px 0;}
2120
2033
  <p class="hint createWhy" id="createWhy" aria-live="polite">&nbsp;</p>
2121
2034
  <div class="alert" id="createAlert" role="alert"></div>
2122
2035
 
2123
- <!-- 03 · Connect GitHub: waits until the project exists, then drives the steps only
2124
- the founder can do (today's onboard-plan steps, task 1002694), drawn as one
2125
- checklist that ticks itself from what the server has verified. -->
2036
+ <!-- 03 · Connect GitHub until Create; then "Getting <name> ready" (task 1004683, the
2037
+ design of record docs/design/mocks/day-night-rooms/setup-status.html): one sentence
2038
+ saying what is happening now, then one list of facts the server checked, in two
2039
+ groups. Every fact's state is read off a server field (paintFacts); the facts are
2040
+ drawn from nodes, never markup from strings. -->
2126
2041
  <div class="part waiting" id="panel-done" aria-labelledby="done-part-h">
2127
- <h2 id="done-part-h"><span class="n">03</span>Connect GitHub <span class="state" id="ghState">opens once your project exists</span></h2>
2042
+ <!-- "Connect GitHub" and "opens once your project exists" are the before-Create words;
2043
+ paintFacts() names it "Getting <name> ready" with its count after Create -->
2044
+ <h2 id="done-part-h"><span class="n">03</span><span id="donePartT">Connect GitHub</span> <span class="state" id="ghState">opens once your project exists</span></h2>
2128
2045
  <p class="partSub">One trip to GitHub. Approve the app, pick your repo, and you land back here. We tick each step as it happens, from any tab or device.</p>
2129
2046
  <div class="doneBody" id="doneBody">
2047
+ <!-- the ONE live region of part 03: written only when the sentence changes -->
2048
+ <p class="setNow" id="setupNow" role="status" aria-live="polite"></p>
2130
2049
  <p class="hint" id="doneRender" role="status" hidden></p>
2131
2050
  <p class="hint" id="doneLook" role="status" hidden></p>
2132
2051
  <div id="doneApp" hidden></div>
2133
- <!-- whose account GitHub makes the app on (task 1004651, the mockup's
2134
- account select beside Connect GitHub); shown while Connect is offered -->
2052
+ <div class="setFactGrp"><h3 id="homeFactsH">Its home</h3><div class="setFacts" id="homeFacts" role="list" aria-labelledby="homeFactsH"></div></div>
2053
+ <div class="setFactGrp"><h3 id="ghFactsH">GitHub</h3><div class="setFacts" id="ghFacts" role="list" aria-labelledby="ghFactsH"></div></div>
2054
+ <!-- whose account GitHub makes the app on (task 1004651, the mockup's account
2055
+ select beside Connect GitHub): paintFacts moves it into the Approve fact
2056
+ while Connect is offered, and back here otherwise -->
2135
2057
  <div class="acctPick" id="aaPick" hidden>
2136
2058
  <div class="row2">
2137
- <label class="wizField"><span>Make the app on</span>
2059
+ <label class="wizField"><span class="vh">Make the app on</span>
2138
2060
  <select id="aaSel" aria-label="Which GitHub account"><option value="personal">On my account</option><option value="org">On an organisation</option></select></label>
2139
2061
  <label class="wizField" id="aaOrgField" hidden><span>Organisation</span><select id="aaOrg"></select></label>
2140
2062
  </div>
@@ -2144,17 +2066,11 @@ summary{min-height:24px;padding:3px 0;}
2144
2066
  </div>
2145
2067
  <p class="hint" id="aaNote" aria-live="polite"></p>
2146
2068
  </div>
2147
- <ol class="humanSteps" id="humanSteps"></ol>
2148
- <!-- the STABLE live region: the strip inside is rebuilt every 5s tick,
2149
- and a live region that is itself replaced never announces -->
2150
- <div id="liveProgress" role="status" aria-live="polite"></div>
2151
- <!-- the wait (task 1004578): one status line, a thin bar, and the cloud
2152
- hop — one cloud per public project on the platform, in its planet's
2153
- colour. Shown only while data-moment="wait"; under reduced motion
2154
- the sky is drawn once and stands still. -->
2155
- <div class="hop" id="hop">
2156
- <p class="hopStatus" id="hopStatus" role="status" aria-live="polite"></p>
2157
- <div class="hopBar" aria-hidden="true"><span id="hopBarFill"></span></div>
2069
+ <!-- the cloud hop (task 1004578): one cloud per public project on the platform,
2070
+ in its planet's colour. It opens in place only when asked, and the wait never
2071
+ depends on it; under reduced motion the sky is drawn once and stands still. -->
2072
+ <button type="button" class="hopLink" id="hopLink" aria-expanded="false" aria-controls="hop">Play cloud hop while you wait</button>
2073
+ <div class="hop" id="hop" hidden>
2158
2074
  <div class="hopStage">
2159
2075
  <canvas id="hopCanvas" tabindex="0" role="img" aria-label="A cloud-hopping game to pass the wait: each cloud is a public project on the platform. Arrow keys steer, space jumps."></canvas>
2160
2076
  </div>
@@ -2163,18 +2079,20 @@ summary{min-height:24px;padding:3px 0;}
2163
2079
  <button type="button" class="btn small ghost" id="hopJump">Jump</button>
2164
2080
  <button type="button" class="btn small ghost" id="hopRight" aria-label="Move right">→</button>
2165
2081
  <span class="hopHint">Arrows or A and D steer, space jumps.</span>
2166
- <span class="hopScore" id="hopScore" aria-live="polite"></span>
2082
+ <span class="hopScore" id="hopScore"></span>
2167
2083
  </div>
2168
2084
  </div>
2169
- <div class="alive">
2170
- <h2 class="aliveH" id="done-h">Setting up your project</h2>
2171
- <p class="lede" id="doneLede"></p>
2085
+ <!-- every required fact done (paintFacts): "<name> is alive.", the invite box once
2086
+ the hall answers (PS15), and the door into Genesis, the hall's founding mode -->
2087
+ <div class="alive" id="doneAlive" hidden>
2088
+ <!-- shown only when every required fact is done; paintAlive() puts the name in -->
2089
+ <h2 class="aliveH" id="done-h">Your project is alive.</h2>
2172
2090
  <!-- the invite step (task 1002346, BV1.R27; ADR 0249) — OPTIONAL, and it stands
2173
2091
  HERE because an invite needs a project that already exists. Hidden until the
2174
2092
  created record carries an id and a slug; the script is the only thing that
2175
2093
  shows it. -->
2176
2094
  <div class="inviteBox" id="wizInvite" hidden>
2177
- <p class="finePrint">Invite your first builders. They become founding builders. Optional: the invite comes from your project’s hall, and you can invite anyone later.</p>
2095
+ <p class="finePrint">Invite your first builders. They become founding builders. Optional: the invite comes from your project’s hall.</p>
2178
2096
  <div class="attach">
2179
2097
  <label class="vh" for="wizInviteWho">GitHub handle</label>
2180
2098
  <input type="text" id="wizInviteWho" placeholder="GitHub handle" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="200">
@@ -2187,13 +2105,13 @@ summary{min-height:24px;padding:3px 0;}
2187
2105
  <ul class="inviteList" id="wizInviteList"></ul>
2188
2106
  <div class="projMsg" id="wizInviteMsg" role="status" aria-live="polite"></div>
2189
2107
  </div>
2108
+ <div class="aliveDoor"><a class="btn" id="enterGenesis" data-onboard-step="enter-hall" target="_blank" rel="noopener">Enter Genesis</a></div>
2190
2109
  <!-- the way back, the doneModules idiom: an optional step is a default
2191
2110
  rather than a dead end only if the panel names where it lives after -->
2192
2111
  <p class="samePath" id="wizInviteLater"></p>
2193
2112
  <!-- the extras row was a default (task 1002342, BV1.R23) — one line saying
2194
2113
  where they live afterwards, so accepting them is never a dead end -->
2195
2114
  <p class="samePath" id="doneModules"></p>
2196
- <p class="samePath">These steps are the same however a project is created.</p>
2197
2115
  </div>
2198
2116
  </div>
2199
2117
  </div>
@@ -4805,35 +4723,6 @@ summary{min-height:24px;padding:3px 0;}
4805
4723
  $('addrCustom').open = true;
4806
4724
  }
4807
4725
  }
4808
- /* Pinned GitHub OAuth-app values for a domain (auth.js WEB_REDIRECT_ORIGIN —
4809
- callback is the apex /api/gds/auth/web/callback). Mirrors onboard.oauthAppInstructions. */
4810
- function oauthAppValues(domain) {
4811
- var origin = 'https://' + domain;
4812
- return { homepageUrl: origin, callbackUrl: origin + '/api/gds/auth/web/callback' };
4813
- }
4814
- /* GitHub's own "New repository" page, pre-filled with the owner + name. No auth or
4815
- permissions on our side — the visitor creates the empty repo in GitHub's UI, which
4816
- is exactly what the scaffold then clones. `owner` selects the account/org; omitting
4817
- a blank param just leaves that field empty on GitHub. */
4818
- function githubNewRepoUrl(owner, repo) {
4819
- var u = new URL('https://github.com/new');
4820
- if (repo) u.searchParams.set('name', repo);
4821
- if (owner) u.searchParams.set('owner', owner);
4822
- return u.toString();
4823
- }
4824
- /* github.com address of the repo this draft points at, or '' when we can't name one
4825
- honestly. Adopt: owner/repo IS the address the owner picked. Auto-create: the
4826
- html_url the create call returned — a draft predating task 1002855 has none, and
4827
- gets no link rather than a guess that could 404. The scheme is hard-coded and both
4828
- segments are encoded, so nothing here can become an off-site or javascript: href. */
4829
- function repoLinkFor() {
4830
- if (!state.owner || !state.repo) return '';
4831
- if (state.mode === 'adopt') {
4832
- return 'https://github.com/' + encodeURIComponent(state.owner) + '/' + encodeURIComponent(state.repo);
4833
- }
4834
- var stored = String(state.repoUrl || '');
4835
- return stored.indexOf('https://github.com/') === 0 ? stored : '';
4836
- }
4837
4726
  function saveDraft() {
4838
4727
  try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify(state)); } catch (e) {}
4839
4728
  }
@@ -4871,12 +4760,12 @@ summary{min-height:24px;padding:3px 0;}
4871
4760
  var h = created ? $('done-part-h') : $(panels[step]).querySelector('h1');
4872
4761
  if (h) { h.setAttribute('tabindex', '-1'); h.focus({ preventScroll: false }); }
4873
4762
  }
4874
- /* the cloud hop's motion runs only while part 03 is live (task 1004578) — a
4875
- start/stop, not a paint: renderDone paints the part */
4876
- if (typeof hopStart === 'function') { if (created) hopStart(); else hopStop(); }
4763
+ /* the cloud hop runs only on part 03, and only once asked (tasks 1004578, 1004683) —
4764
+ a stop, not a paint: renderDone paints the part */
4765
+ if (!created && typeof hopOpen === 'function') hopOpen(false);
4877
4766
  if (step === 1) paintPage();
4878
4767
  else paintCreate();
4879
- if (typeof stepsPaint === 'function') stepsPaint();
4768
+ if (typeof paintFacts === 'function') paintFacts();
4880
4769
  }
4881
4770
  /* The page hosts every control at once, so it paints them all on entry. The repo
4882
4771
  probe is authenticated — never fire it before /me settles (a pre-auth probe 401s
@@ -5202,7 +5091,7 @@ summary{min-height:24px;padding:3px 0;}
5202
5091
  files — the server applies the bundle. A picker that could block a
5203
5092
  project from being created would be worse than no picker. */
5204
5093
  var starterBundles = null; /* the payload once it lands; null until then */
5205
- var bundlesPromise = null; /* memoised like fetchPlan below — entering the step twice asks once */
5094
+ var bundlesPromise = null; /* memoised — entering the step twice asks once */
5206
5095
  var bundlesFailed = false; /* asked and did not get it; distinct from not yet asked */
5207
5096
  /* Lazy, not at boot: /projects serves four views from one file, and the map
5208
5097
  has no business paying for the picker's read. */
@@ -6806,325 +6695,400 @@ summary{min-height:24px;padding:3px 0;}
6806
6695
  });
6807
6696
  });
6808
6697
 
6809
- /* ── done panel: rendered FROM the canonical onboard plan (task 1002694, gap 2 /
6810
- idea 1000675). GET /provisioning/onboard-plan is the single source the
6811
- /new-project skill + `bongos onboard` read too, so the checklist can't
6812
- drift. A per-key renderer adds this page's rich UI; an unknown human step
6813
- falls back to the plan's own label, so a new canonical step surfaces here
6814
- by default instead of silently vanishing. ── */
6815
- function copyBtn(text) {
6816
- return '<button type="button" class="copy" data-copy="' + esc(text) + '">Copy</button>';
6817
- }
6818
- var GH_ICON = '<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>';
6819
-
6698
+ /* ── part 03 after Create (task 1004683; the design of record
6699
+ docs/design/mocks/day-night-rooms/setup-status.html, owner 2026-10-09): ONE list of
6700
+ facts the server checked, in two groups. Its home reads the owner's instance read
6701
+ (GET /provisioning/instances/:id: status, open_intent, domain, dns_resolves,
6702
+ last_verified_up); GitHub reads the github-setup checklist and its pick-up word
6703
+ (tasks 1004649–1004652). homeFacts() and ghFacts() are PURE over what those reads
6704
+ last said: every fact's state is one of todo · now · you · done · bad, and each
6705
+ comes from a server field — never a timer, a stage count or a click this page
6706
+ assumed. setupSentence() says what is happening now, once; paintFacts() draws it
6707
+ all from nodes. Nothing is said twice. ── */
6820
6708
  var instanceId = null; /* set when a create lands / a resumed watch starts */
6821
6709
  /* where a project is ACTED on since task 1003355: its own page. A list row
6822
6710
  carries only view and manage now, so every pointer that names a control —
6823
6711
  attach an address, retry a snagged standup, bring a torn-down project
6824
6712
  back — must land there, not on the list, which has no such button. */
6825
6713
  function projectPageLink(label) {
6826
- return '<a href="' + (instanceId ? '?view=manage&id=' + encodeURIComponent(instanceId) : '?view=mine') +
6827
- '">' + label + '</a>';
6828
- }
6829
- var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null, pickUp: null, verifying: false, askedPlatform: false };
6830
- var planPromises = {}; /* mode|domain → Promise<steps[]> */
6831
- var lastStepsHtml = '';
6832
- var lastProgHtml = ''; /* same change-only contract, for the progress strip */
6833
- var FALLBACK_HUMAN_KEYS = ['github-repo', 'oauth-app', 'device-flow', 'app-install', 'first-sign-in', 'enter-hall'];
6834
-
6835
- function fetchPlan() {
6836
- var domain = resolvedDomain() || '';
6837
- var key = state.mode + '|' + domain;
6838
- if (!planPromises[key]) {
6839
- /* shape=cloud-host: this wizard only creates control-plane cloud-host
6840
- instances, and the plan is shape-filtered server-side (direction §7) */
6841
- planPromises[key] = fetch(API + '/provisioning/onboard-plan?mode=' + encodeURIComponent(state.mode) + '&shape=cloud-host' + (domain ? '&domain=' + encodeURIComponent(domain) : ''))
6842
- .then(function (res) { if (!res.ok) throw new Error('plan'); return res.json(); })
6843
- .then(function (d) { return (d && d.steps) || []; })
6844
- .catch(function () {
6845
- delete planPromises[key]; /* transient — retried on the next render */
6846
- return FALLBACK_HUMAN_KEYS.map(function (k) { return { key: k, kind: 'human', label: '' }; });
6847
- });
6848
- }
6849
- return planPromises[key];
6850
- }
6851
-
6852
- /* github-repo — adopt / auto-created / manual-create, each aware of whether the
6853
- scaffold has landed (instance active ⇒ the build already pushed). The wizard
6854
- files onboard_mode to the server, so for adopt the RUNNER clones + layers
6855
- (task 1002562) — the owner no longer runs `bongos init --adopt` here. */
6856
- function renderStepGithubRepo() {
6857
- var landed = live.instStatus === 'active';
6858
- /* the one-page checklist (task 1004649) shows only the GitHub steps the founder
6859
- takes; a repo the platform made or adopted needs nothing, so it is not a row */
6860
- if (state.mode === 'adopt' || state.repoAutoCreated || landed) return '';
6861
- var repoLabel = state.owner && state.repo ? '<strong>' + esc(state.owner + '/' + state.repo) + '</strong>' : 'your new repository';
6862
- /* Naming a repo without linking it stranded the owner on exactly the path where the
6863
- platform did the work (task 1002855). Adopt links the repo the owner picked;
6864
- auto-create links the html_url GitHub itself returned. A draft written before this
6865
- change carries no stored URL and degrades to the bare name — never a guessed link.
6866
- Only these two branches link: the manual branch below still has a real .ghbtn
6867
- action, and nesting an <a> inside it would be malformed. */
6868
- var repoUrl = repoLinkFor();
6869
- var repoLink = repoUrl ? '<a href="' + esc(repoUrl) + '" target="_blank" rel="noopener">' + repoLabel + '</a>' : repoLabel;
6870
- if (state.mode === 'adopt') {
6871
- return landed
6872
- ? '<li class="sdone" data-onboard-step="github-repo"><b>Your repo carries the platform</b><p>The build layered the platform onto ' + repoLink + ' — existing files, history, and backlog kept. Pull to see it.</p></li>'
6873
- : '<li data-onboard-step="github-repo"><b>Your existing repo</b><p>The build clones ' + repoLink + ' and layers the platform onto its existing history — nothing is created or emptied, and your backlog is imported. Nothing for you to do here.</p></li>';
6874
- }
6875
- if (state.repoAutoCreated) {
6876
- return landed
6877
- ? '<li class="sdone" data-onboard-step="github-repo"><b>Repo ready — scaffold landed</b><p>We created ' + repoLink + ' (public, by design) and the build pushed your project’s starting files to it.</p></li>'
6878
- : '<li data-onboard-step="github-repo"><b>Your empty repo is ready</b><p>We created ' + repoLink + ' on GitHub for you — public (projects here are open by design), empty, and waiting. The scaffold lands there when the build runs; nothing for you to do here.</p></li>';
6879
- }
6880
- var ghNewUrl = githubNewRepoUrl(state.owner, state.repo);
6881
- return '<li' + (landed ? ' class="sdone"' : '') + ' data-onboard-step="github-repo"><b>Create the empty GitHub repo</b><p>Do this first — the build needs the repo to exist. Click below (GitHub opens with the owner and name already filled in), press <strong>Create repository</strong>, and leave it completely empty. It must land at exactly ' + repoLabel + ' — leave GitHub’s Owner box as prefilled, because the build watches that exact address; a repo created under a different owner is never found. GitHub will ask public or private — your choice; both work here, and we never change it. Seeing “repository not found” above? That just means the repo isn’t created yet — make it here.</p>' +
6882
- '<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + GH_ICON + 'Create ' + repoLabel + ' on GitHub</a></li>';
6883
- }
6884
-
6885
- /* The manual OAuth-app path stays as a documented fallback (ADR 0133), but the
6886
- control-plane placement is named as the operator's, never handed to the owner
6887
- as a command to run (gap 4). */
6888
- function manualOauthDetails() {
6889
- var oa = oauthAppValues(resolvedDomain());
6890
- return '<details><summary>Prefer to create an OAuth app by hand?</summary>' +
6891
- '<p>GitHub → <em>your account’s</em> Settings → Developer settings → OAuth Apps → New OAuth App. (Developer settings live under your account, not the repo.)</p>' +
6892
- '<div class="kv"><span class="kk">Homepage URL</span><code>' + esc(oa.homepageUrl) + '</code>' + copyBtn(oa.homepageUrl) + '</div>' +
6893
- '<div class="kv"><span class="kk">Callback URL</span><code>' + esc(oa.callbackUrl) + '</code>' + copyBtn(oa.callbackUrl) + '</div>' +
6894
- '<p>The platform team places the generated credentials; this step ticks itself once they land.</p></details>';
6895
- }
6896
-
6897
- /* "Open it when it's ready" (task 1004548). The founder now waits for sign-in on
6898
- this checklist instead of on the callback page, so the auto-open lives here: a
6899
- tick, on unless they untick it (remembered per browser), that opens the project
6900
- the moment it answers AND sign-in is wired. Armed only by a wait this page has
6901
- actually watched, so revisiting a long-finished checklist never jumps away. */
6902
- var AUTO_OPEN_KEY = 'cb-open-when-ready';
6903
- var autoOpenArmed = false;
6904
- var autoOpened = false;
6905
- function autoOpenPref() {
6906
- try { return localStorage.getItem(AUTO_OPEN_KEY) !== '0'; } catch (e) { return true; }
6907
- }
6908
- function autoOpenTick() {
6909
- return '<label class="autoOpen"><input type="checkbox" data-action="auto-open"' + (autoOpenPref() ? ' checked' : '') + '>' +
6910
- '<span>Open my project automatically when it’s ready</span></label>';
6911
- }
6912
- function maybeAutoOpen() {
6913
- var domain = resolvedDomain();
6914
- if (autoOpened || !autoOpenArmed || !domain || !autoOpenPref()) return;
6915
- if (live.manifest !== 'done' || live.instStatus !== 'active' || !live.addrReady) return;
6916
- autoOpened = true;
6917
- var url = 'https://' + domain;
6918
- /* a new tab keeps this checklist (two steps follow sign-in); a browser that
6919
- blocks an unprompted tab gets this one sent instead — the founder asked */
6920
- var w = null;
6921
- try { w = window.open(url, '_blank'); } catch (e) {}
6922
- if (w) { try { w.opener = null; } catch (e) {} }
6923
- else window.location.assign(url);
6924
- }
6925
-
6926
- /* The four GitHub steps of part 03, in the mockup's words and order (task 1004649,
6927
- docs/design/mocks/day-night-rooms/): Approve the <name> app on GitHub · Install it
6928
- on your repo · Builders can sign in · Command-line sign-in is on. One click starts
6929
- ONE trip — approve, straight on to installing, back here — and every tick is what
6930
- the server verified (live.setup, the status poll's `checklist`). Every value that
6931
- reaches the markup goes through esc(). */
6714
+ return '<a href="' + manageHref() + '">' + label + '</a>';
6715
+ }
6716
+ function manageHref() {
6717
+ return instanceId ? '?view=manage&id=' + encodeURIComponent(instanceId) : '?view=mine';
6718
+ }
6719
+ var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null, pickUp: null, verifying: false, askedPlatform: false, inst: null, readAt: 0, gone: '', repo: null };
6720
+
6932
6721
  function ghStep(key) { return live.setup && live.setup[key]; }
6933
6722
  /* "Pick up where it stopped" (task 1004652): a stopped step says what stopped in one plain
6934
6723
  sentence and carries the action that fixes it, with the app GitHub already made, never a
6935
6724
  second one. live.pickUp is the server's word for what that action does (github-setup.js
6936
- pickUpFor); live.verifying is whether the runner is still checking. Static words only. */
6725
+ pickUpFor); live.verifying is whether the runner is still checking. */
6937
6726
  var ghTabTrouble = null; /* 'behind' | 'closed': what this page saw of the GitHub tab it opened */
6938
6727
  var NO_SECOND_APP = 'Nothing is lost, and no second app will be made.';
6939
- function pickUpBtn() { return '<button type="button" class="ghbtn" data-action="pick-up">Pick up where it stopped</button>'; }
6940
- function askPlatformBtn() {
6941
- if (live.askedPlatform === true) return '<p>Sent. The platform team has it and finishes it from here; there is nothing else for you to do.</p>';
6942
- return (live.askedPlatform === 'failed' ? '<p>That didn’t send. Try again.</p>' : '') +
6943
- '<button type="button" class="linklike" data-action="ask-platform">Send it to the platform team</button>';
6728
+ function projectName() { return state.name || state.slug || 'your project'; }
6729
+ /* a host name as the server stores it; anything else is no address. PURE. */
6730
+ function hostOk(d) { return typeof d === 'string' && d.length < 254 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/i.test(d); }
6731
+ /* the project's address: the server's once it has read, the page's own until then */
6732
+ function setupDomain() {
6733
+ if (live.inst) return hostOk(live.inst.domain) ? live.inst.domain : '';
6734
+ var d = resolvedDomain();
6735
+ return hostOk(d) ? d : '';
6736
+ }
6737
+ /* owner/repo: the server's (github-setup.js targetRepo, strict), else the page's own */
6738
+ function repoName() {
6739
+ if (live.repo) return live.repo;
6740
+ return state.owner && state.repo ? state.owner + '/' + state.repo : null;
6944
6741
  }
6945
- function appTitle() { return 'Approve the ' + esc(state.name || state.slug || 'project') + ' app on GitHub'; }
6946
6742
  /* the organisation that owns the app, when the founder made it on one (task 1004651) */
6947
6743
  function appOrgLogin() {
6948
6744
  return typeof live.appOrg === 'string' && /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/.test(live.appOrg) ? live.appOrg : null;
6949
6745
  }
6950
6746
  /* the app's own settings on GitHub: under its organisation, or under the founder's
6951
- account. A raw URL; callers esc() it into an attribute. */
6747
+ account. A raw URL, set as an attribute, never parsed as markup. */
6952
6748
  function appSettingsUrl() {
6953
6749
  if (!live.appSlug) return null;
6954
6750
  var org = appOrgLogin();
6955
6751
  return 'https://github.com' + (org ? '/organizations/' + encodeURIComponent(org) : '') + '/settings/apps/' + encodeURIComponent(live.appSlug);
6956
6752
  }
6957
6753
 
6958
- /* oauth-app — the ADR 0133 one-click GitHub-App manifest flow is the primary path
6959
- (gap 3 / idea 1000676); locked until a domain exists (the App needs the real
6960
- callback host — the server refuses domainless with domain_required). */
6961
- function renderStepOauthApp() {
6962
- var domain = resolvedDomain();
6963
- if (!domain) {
6964
- return '<li class="slocked" data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>Unlocks once your project has an address — sign-in needs a real domain. Attach one any time from ' + projectPageLink('this project’s own page') + '.</p></li>';
6754
+ /* When a fact was seen, said plainly, from a server timestamp (or the moment this page's
6755
+ read answered): "just now" / "2:14 pm" for when it happened, "checked 6s ago" /
6756
+ "checked 3m ago" / "checked 2:14 pm" for when it was looked at. '' when there is none. PURE. */
6757
+ function whenSaid(at, now, checked) {
6758
+ var t = typeof at === 'number' ? at : Date.parse(at || '');
6759
+ if (!t || isNaN(t)) return '';
6760
+ var s = Math.max(0, Math.round((now - t) / 1000));
6761
+ if (checked && s < 60) return 'checked ' + s + 's ago';
6762
+ if (checked && s < 3600) return 'checked ' + Math.floor(s / 60) + 'm ago';
6763
+ if (!checked && s < 60) return 'just now';
6764
+ var d = new Date(t), h = d.getHours(), m = d.getMinutes();
6765
+ var said = s < 86400 ? ((h % 12) || 12) + ':' + (m < 10 ? '0' : '') + m + ' ' + (h < 12 ? 'am' : 'pm')
6766
+ : d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
6767
+ return (checked ? 'checked ' : '') + said;
6768
+ }
6769
+
6770
+ /* Its home: three facts from the owner's instance read. PURE over (inst, now) and the
6771
+ time that read answered. A domain is NAMED only once the platform's own lookup has
6772
+ seen it answer (addressReady, task 1004505): a name typed before it resolves is
6773
+ cached as not found for up to half an hour. */
6774
+ function homeFacts(inst, now) {
6775
+ var checked = live.readAt ? whenSaid(live.readAt, now, true) : '';
6776
+ if (!inst) {
6777
+ return [{ key: 'address', state: 'todo', title: 'Address kept for you' },
6778
+ { key: 'server', state: 'todo', title: 'Server and database running' },
6779
+ { key: 'answers', state: 'todo', title: 'Address answers' }];
6780
+ }
6781
+ var s = inst.status, oi = inst.open_intent;
6782
+ /* an intent speaks only where the status does not already (F22): a requested project
6783
+ always has its provision intent open */
6784
+ var act = oi && s !== 'provisioning' && s !== 'tearing_down' && s !== 'requested' ? oi.action : '';
6785
+ var dom = hostOk(inst.domain) ? inst.domain : '', ready = addressReady(inst);
6786
+ var address = dom
6787
+ ? { key: 'address', state: 'done', title: 'Address kept for you', sub: ready ? dom : 'Its name shows here once it reaches everyone.', right: whenSaid(inst.created_at, now) }
6788
+ : { key: 'address', state: 'todo', title: 'Address kept for you' };
6789
+ var server;
6790
+ if (act === 'teardown' || s === 'tearing_down') server = { state: 'todo', title: 'Server and database switching off', sub: 'The repository stays yours, and the database is kept.' };
6791
+ else if (s === 'torn_down' && act !== 'provision') server = { state: 'todo', title: 'Server and database switched off' };
6792
+ else if (s === 'requested' || s === 'provisioning' || act === 'provision') server = { state: 'now', title: 'Server and database starting', sub: 'Usually a few minutes.', right: checked };
6793
+ /* a settings change restarts the project (task 1003140): said by its state, queued or running */
6794
+ else if (s === 'active' && act === 'settings-apply') {
6795
+ server = oi.state === 'running'
6796
+ ? { state: 'now', title: 'Server and database restarting', sub: 'To apply a settings change.', right: checked }
6797
+ : { state: 'now', title: 'Server and database running', sub: 'A settings change is queued. It restarts to apply it.', right: checked };
6965
6798
  }
6799
+ else if (s === 'active') server = { state: 'done', title: 'Server and database running', right: whenSaid(inst.provisioned_at, now) };
6800
+ else if (s === 'error') server = { state: 'bad', title: 'Server and database stopped', sub: 'It stopped partway and won’t carry on by itself.', right: checked, act: 'manage', actText: 'Retry it on its own page' };
6801
+ else server = { state: 'todo', title: 'Server and database running' };
6802
+ server.key = 'server';
6803
+ var answers = { key: 'answers', state: 'todo', title: 'Address answers' };
6804
+ if (dom && s === 'active' && act !== 'teardown') {
6805
+ if (!ready) { answers.state = 'now'; answers.sub = 'New addresses can take a few minutes to reach everyone.'; answers.right = checked; }
6806
+ else if (inst.last_verified_up === true) { answers.state = 'done'; answers.right = whenSaid(inst.last_verified_at, now, true); }
6807
+ else if (inst.last_verified_up === false) { answers.state = 'now'; answers.sub = 'Not answering yet. We keep checking.'; answers.right = whenSaid(inst.last_verified_at, now, true); }
6808
+ else { answers.state = 'now'; answers.sub = 'Checking that it answers.'; answers.right = checked; }
6809
+ }
6810
+ return [address, server, answers];
6811
+ }
6812
+
6813
+ /* GitHub: the four facts of the one trip (task 1004649), each from the checklist the
6814
+ server derived (live.setup), the attempt's effective status (live.manifest and how far
6815
+ a failure got, task 1004572) and what picking it up does (live.pickUp, task 1004652).
6816
+ PURE over those and `now`. A stopped fact carries the one action that fixes it; only
6817
+ the FIRST stopped fact carries "Pick up where it stopped". */
6818
+ function ghFacts(now) {
6819
+ var checked = live.readAt ? whenSaid(live.readAt, now, true) : '';
6820
+ var dom = setupDomain(), name = projectName(), repo = repoName();
6821
+ var approve = { key: 'approve', title: 'Approve the ' + name + ' app on GitHub' };
6966
6822
  /* GitHub made the app and the platform placed its keys: the slug is recorded only
6967
6823
  after the placement (the runner), so a known slug outside a failure is the tick —
6968
- a fact, whatever the venue says about starting a NEW trip */
6969
- /* …including when a LATER step stopped (sign-in unconfirmed): the app and its keys are
6970
- real, so this step stays ticked and the stopped one says so (task 1004652) */
6824
+ including when a LATER step stopped (sign-in unconfirmed) */
6971
6825
  var appMade = (ghStep('approve') === 'done' || live.appSlug) && live.manifest !== 'expired' &&
6972
6826
  (live.manifest !== 'error' || live.manifestFailure === 'sign_in_unconfirmed');
6973
- if (appMade) {
6974
- return '<li class="sdone" data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>GitHub made the app, and its keys are on your project.</p></li>';
6827
+ if (appMade) { approve.state = 'done'; approve.sub = name + ' app on ' + (appOrgLogin() || 'your account'); }
6828
+ else if (!dom) { approve.state = 'todo'; approve.sub = 'Opens once it has an address.'; }
6829
+ /* GitHub refuses app manifests whose hook URL it cannot reach (owner walk, 2026-08-12) */
6830
+ else if (!apexBase()) { approve.state = 'todo'; approve.sub = 'GitHub can’t reach a platform on a local address, so this works only on a public one.'; }
6831
+ else if (live.manifest === 'error' || live.manifest === 'expired') {
6832
+ approve.state = 'bad'; approve.right = checked; approve.act = 'connect';
6833
+ if (live.manifest === 'expired') approve.sub = 'That setup link expired before GitHub was approved, so nothing was made. Start a fresh one.';
6834
+ else if (live.manifestCode === 'domain_required') { approve.sub = 'It needs an address first. Attach one on its own page.'; approve.act = 'manage'; approve.actText = 'Open its own page'; }
6835
+ else if (live.pickUp === 'convert') { approve.sub = 'GitHub approved the app, and the platform stopped before it finished collecting the app’s keys.'; approve.act = 'pick-up'; }
6836
+ else if (live.pickUp === 'operator' || live.manifestFailure === 'finish_failed') { approve.sub = 'GitHub made the app, but its keys never reached your project. Making another would leave you with two, so the platform team finishes this one.'; approve.act = 'ask-platform'; }
6837
+ else { approve.sub = 'GitHub setup didn’t start. Nothing was changed on GitHub.'; approve.tech = live.manifestMsg || ''; }
6975
6838
  }
6976
- /* honesty about the venue (owner walk, 2026-08-12): GitHub rejects app
6977
- manifests whose hook URL isn't publicly reachable, so a locally-hosted
6978
- platform can't complete the one-click flow — say it BEFORE the bounce. */
6979
- if (!apexBase()) {
6980
- return '<li class="slocked" data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>This platform is running on a local address, and GitHub refuses app setups it can’t reach over the public internet — so the one-click flow can’t complete here. On a publicly-addressed platform this is one click.</p>' + manualOauthDetails() + '</li>';
6839
+ else if (live.manifest === 'code_received' || live.manifest === 'converting') { approve.state = 'now'; approve.sub = 'Approved. The platform is collecting the app’s keys.'; }
6840
+ else if (live.manifest) {
6841
+ /* waiting on GitHub: "Reopen GitHub" posts the SAME attempt again, never a second app */
6842
+ approve.state = 'you'; approve.act = 'reopen';
6843
+ approve.sub = ghTabTrouble === 'closed' ? 'The GitHub tab closed before you approved, so nothing was made. Reopen it to carry on.'
6844
+ : ghTabTrouble === 'behind' ? 'Your GitHub tab opened behind this window.' : 'Waiting for you in the GitHub tab.';
6981
6845
  }
6982
- if (live.manifest === 'error' || live.manifest === 'expired') {
6983
- /* a plain sentence first; the raw route text folds under Technical
6984
- details (task 1003244). The one refusal a founder can act on — no
6985
- address yet — says where to go instead of what the server said. */
6986
- var snag, raw = '';
6987
- /* a failure AFTER GitHub's approval left an app on the founder's account, so
6988
- "nothing was changed" would be false, and a new link would make a second app —
6989
- the server says how far it got (live.manifestFailure, task 1004572) and what
6990
- picking it up does (live.pickUp, task 1004652); the words and the action follow */
6991
- var act = '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Connect GitHub ↗</button>' + manualOauthDetails();
6992
- if (live.manifest === 'expired') snag = 'That setup link expired (they last 30 minutes) before GitHub was approved, so nothing was made on GitHub. Start a fresh one.';
6993
- else if (live.manifestCode === 'domain_required') snag = 'This project needs an address first — attach one from ' + projectPageLink('its own page') + ', then come back to this step.';
6994
- else if (live.pickUp === 'convert') {
6995
- snag = 'GitHub approved the app, and the platform stopped before it finished collecting the app’s keys. ' + NO_SECOND_APP;
6996
- act = pickUpBtn();
6997
- }
6998
- else if (live.manifestFailure === 'finish_failed') {
6999
- snag = 'GitHub made the app, but its keys never reached your project, so it can’t be used yet. Making another would leave you with two, so we won’t: the platform team finishes this one. Send it to them from here.';
7000
- act = askPlatformBtn();
6846
+ else { approve.state = 'you'; approve.act = 'connect'; approve.sub = 'One trip: approve, install on your repo, and you are back here.'; }
6847
+
6848
+ var install = { key: 'install', title: 'Install it on your repo', sub: repo || '' };
6849
+ var installUrl = live.installUrl || (live.appSlug ? 'https://github.com/apps/' + encodeURIComponent(live.appSlug) + '/installations/new' : null);
6850
+ if (ghStep('install') === 'done') install.state = 'done';
6851
+ else if (!live.appSlug) install.state = 'todo';
6852
+ /* the runner stopped checking and GitHub never confirmed the install: the link opens the
6853
+ SAME app's install page, and the click asks the runner to look again */
6854
+ else if (live.pickUp === 'check') { install.state = 'bad'; install.sub = 'GitHub stopped before the app was on ' + (repo || 'your repo') + '.'; install.right = checked; install.act = 'pick-up-link'; install.href = installUrl; }
6855
+ else { install.state = 'you'; install.right = 'not yet seen'; install.act = 'install-link'; install.href = installUrl; }
6856
+
6857
+ var signIn = { key: 'sign_in', title: 'Builders can sign in' };
6858
+ if (live.manifest === 'done' || ghStep('sign_in') === 'done') { signIn.state = 'done'; signIn.sub = dom && live.addrReady ? 'Tested on ' + dom : ''; }
6859
+ else if (live.manifest === 'error' && live.manifestFailure === 'sign_in_unconfirmed') {
6860
+ /* the keys are placed but the project never confirmed sign-in: the runner re-asks it
6861
+ with the keys already there; a project that still will not answer goes to the
6862
+ platform team */
6863
+ if (live.verifying) { signIn.state = 'now'; signIn.sub = 'Asking your project again, with the app GitHub already made.'; }
6864
+ else {
6865
+ signIn.state = 'bad'; signIn.sub = 'Your project didn’t confirm that sign-in works.'; signIn.right = checked;
6866
+ signIn.act = live.pickUp === 'check' && install.state !== 'bad' ? ['pick-up', 'ask-platform'] : ['ask-platform'];
7001
6867
  }
7002
- else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
7003
- return '<li class="sstopped" data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>' + snag + '</p>' +
7004
- (raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') + act + '</li>';
7005
6868
  }
7006
- if (live.manifest === 'code_received' || live.manifest === 'converting') {
7007
- /* GitHub's half of the approval is done — say so, rather than keep telling the
7008
- founder to approve an app they just approved */
7009
- autoOpenArmed = true;
7010
- return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>Approved on GitHub. The platform is placing the app’s keys on your project, and the GitHub tab goes on to installing it on your repo. This step ticks itself.</p>' +
7011
- progressMarkup(1, 1, 'Placing the app’s keys on your project…', '', 'signin') + autoOpenTick() + '</li>';
7012
- }
7013
- if (live.manifest) { /* pending — GitHub holds the manifest, nobody has approved it yet */
7014
- autoOpenArmed = true;
7015
- /* the tab this page opened, as seen from here (task 1004652): behind this window, or
7016
- closed before the approval. "Reopen GitHub" posts the SAME attempt again (the mint
7017
- route hands back a link still waiting), so it can never make a second app. */
7018
- var wait = ghTabTrouble === 'closed'
7019
- ? 'The GitHub tab closed before the app was approved, so nothing was made on GitHub. Reopen it to carry on with this same setup.'
7020
- : ghTabTrouble === 'behind'
7021
- ? 'Your GitHub tab opened behind this window. Reopen it to bring it to the front; it is this same setup, so no second app will be made.'
7022
- : 'Waiting on GitHub — approve the app in the tab that opened. No GitHub tab? It may have opened behind this window or in another one. Once approved, GitHub takes you straight on to installing it on your repo, then back here; every step ticks itself.';
7023
- return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>' + wait + '</p>' +
7024
- progressMarkup(0, 0, 'Waiting for your approval on GitHub…', '', 'signin') + autoOpenTick() +
7025
- (ghTabTrouble
7026
- ? '<button type="button" class="ghbtn" data-action="reopen-github">' + GH_ICON + 'Reopen GitHub ↗</button>'
7027
- : '<button type="button" class="linklike" data-action="reopen-github">No GitHub tab? Reopen GitHub</button>') + '</li>';
7028
- }
7029
- return '<li data-onboard-step="oauth-app"><b>' + appTitle() + '</b><p>One trip to GitHub: approve the app, install it on your repo, and you land back here. No keys to copy, and the secret never leaves the server side.</p>' +
7030
- '<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Connect GitHub ↗</button>' + manualOauthDetails() + '</li>';
7031
- }
7032
-
7033
- /* device-flow — "Command-line sign-in is on", the OPTIONAL fourth step (task 1004649).
7034
- GitHub creates a manifest-born App with Device Flow OFF and offers no manifest
7035
- field and no API to switch it on (the spike in task 1004649), so only the founder's
7036
- own tick in the app's settings can — and the runner ticks this step when GitHub
7037
- starts a device sign-in for the app. It never holds the project back: web sign-in
7038
- works without it, and the CLI falls back to a web token meanwhile. */
7039
- function renderStepDeviceFlow() {
7040
- var title = '<b>Command-line sign-in is on</b>';
7041
- if (ghStep('device_flow') === 'done') {
7042
- return '<li class="sdone soptional" data-onboard-step="device-flow">' + title + '<p>Builders can sign in from a terminal too — the <code style="font-family:var(--font-mono)">bongos setup</code> command builders run on their own machine.</p></li>';
7043
- }
7044
- if (live.manifest !== 'done') {
7045
- return '<li class="slocked soptional" data-onboard-step="device-flow">' + title + '<p>Optional — turn it on later. It lets builders sign in from a terminal, with the <code style="font-family:var(--font-mono)">bongos setup</code> command they run on their own machine; web sign-in works without it.</p></li>';
6869
+ else if (ghStep('sign_in') === 'now') { signIn.state = 'now'; signIn.right = 'not yet seen'; }
6870
+ else signIn.state = 'todo';
6871
+
6872
+ /* optional: GitHub leaves device flow off on a new app and lets only the founder switch
6873
+ it on (the spike in task 1004649); it never holds the project back */
6874
+ var device = { key: 'device_flow', title: 'Command-line sign-in', opt: true };
6875
+ if (ghStep('device_flow') === 'done') { device.state = 'done'; device.sub = 'Builders can sign in from a terminal too.'; }
6876
+ else {
6877
+ var settings = live.manifest === 'done' ? appSettingsUrl() : null;
6878
+ device.state = 'todo';
6879
+ device.sub = settings ? ['Turn it on later in ', { t: 'the app settings', href: settings }, '.'] : 'Turn it on later in the app settings.';
6880
+ /* past the runner's checking window: ask it to look again, once the install has ticked */
6881
+ if (live.pickUp === 'check' && ghStep('install') === 'done') device.act = 'check-again';
7046
6882
  }
7047
- var appUrl = live.appSlug ? esc(appSettingsUrl()) : null;
7048
- return '<li class="soptional" data-onboard-step="device-flow">' + title + '<p>Optional — turn it on later. GitHub leaves it off on new apps and lets only you switch it on: ' +
7049
- (appUrl
7050
- ? 'open <a href="' + appUrl + '" target="_blank" rel="noopener">your app’s settings</a>, check <strong>Enable Device Flow</strong>, and save. '
7051
- : 'GitHub → Settings → Developer settings → GitHub Apps → your app → check <strong>Enable Device Flow</strong> → save. ') +
7052
- 'We tick this when GitHub has it on. Until then builders sign in on the web, and terminal sign-in falls back to a web token from the hall’s Settings page.</p>' +
7053
- /* past the runner's checking window (task 1004652): ask it to look again. The
7054
- install step carries the same action while it is unverified, so it shows here
7055
- only once the install has ticked. */
7056
- (live.pickUp === 'check' && ghStep('install') === 'done' ? '<button type="button" class="linklike" data-action="pick-up">Turned it on? Check again</button>' : '') + '</li>';
7057
- }
7058
-
7059
- /* app-install — "Install it on your repo": the ADR 0055 publish credential (task
7060
- 1003049). The same trip carries the founder here straight from the approval (task
7061
- 1004649); this step ticks only once the runner has asked GitHub, with the app's own
7062
- key, and GitHub said the app is on this repo. The button is the way back for a
7063
- founder who returned without installing. */
7064
- function renderStepAppInstall() {
7065
- var title = '<b>Install it on your repo</b>';
7066
- if (ghStep('install') === 'done') {
7067
- return '<li class="sdone" data-onboard-step="app-install">' + title + '<p>The app is on ' + (state.owner && state.repo ? esc(state.owner + '/' + state.repo) : 'your repo') + ', so finished work can merge and ship itself.</p></li>';
6883
+
6884
+ var rows = [approve, install, signIn, device];
6885
+ /* "Send it to the platform team", once per attempt: sent, or try again */
6886
+ rows.forEach(function (r) {
6887
+ var acts = [].concat(r.act || []);
6888
+ if (acts.indexOf('ask-platform') === -1) return;
6889
+ if (live.askedPlatform === true) { r.sub = 'Sent. The platform team has it and finishes it from here.'; r.act = acts.filter(function (a) { return a !== 'ask-platform'; }); }
6890
+ else if (live.askedPlatform === 'failed') r.sub = [].concat(r.sub || [], ' That didn’t send. Try again.');
6891
+ });
6892
+ return rows;
6893
+ }
6894
+
6895
+ function factDone(r) { return r.state === 'done'; }
6896
+ function factRequired(r) { return !r.opt; }
6897
+ /* every required fact done: the optional one never holds the project back. PURE. */
6898
+ function setupReady(home, gh) { return home.concat(gh).filter(factRequired).every(factDone); }
6899
+ /* the part's count: "N of M done", the optional fact not counted; "ready ✓" when done. PURE. */
6900
+ function setupCount(home, gh) {
6901
+ var req = home.concat(gh).filter(factRequired);
6902
+ var done = req.filter(factDone).length;
6903
+ return done === req.length ? 'ready ✓' : done + ' of ' + req.length + ' done';
6904
+ }
6905
+ /* ONE sentence: what is happening now, and whether it is the founder's turn. Parts are
6906
+ { t, b (bold), href } — text only, drawn as nodes. PURE over the facts and the reads. */
6907
+ function setupSentence(home, gh) {
6908
+ var inst = live.inst, name = projectName();
6909
+ if (live.gone === 'session') return [{ t: 'Your session ended. ' }, { t: 'Sign in again', href: API + '/auth/web/start?return=/projects%3Fview%3Dnew&repos=1' }, { t: ' to keep watching.' }];
6910
+ if (live.gone === 'missing') return [{ t: 'This project can’t be seen any more. It may have been switched off.' }];
6911
+ if (!inst) return [{ t: 'Asking the platform how it is going…' }];
6912
+ if (setupReady(home, gh)) {
6913
+ return [{ t: name + ' is ready.', b: true }].concat(ghStep('device_flow') === 'done' ? [] : [{ t: ' Command-line sign-in is optional; it ticks itself whenever you turn it on.' }]);
7068
6914
  }
7069
- if (!live.appSlug) {
7070
- return '<li class="slocked" data-onboard-step="app-install">' + title + '<p>Comes straight after the approval, on the same trip: pick your repo and you land back here.</p></li>';
6915
+ var oi = inst.open_intent;
6916
+ if (inst.status === 'tearing_down' || (oi && oi.action === 'teardown') || (inst.status === 'torn_down' && !(oi && oi.action === 'provision'))) {
6917
+ return [{ t: (inst.status === 'torn_down' ? 'This project is switched off.' : 'This project is switching off.') + ' Bring it back any time from ' }, { t: 'its own page', href: manageHref() }, { t: '.' }];
7071
6918
  }
7072
- var installUrl = live.installUrl || ('https://github.com/apps/' + encodeURIComponent(live.appSlug) + '/installations/new');
7073
- /* the runner stopped checking and GitHub never confirmed the install (task 1004652):
7074
- the link opens the SAME app's install page, and the click asks the runner to look again */
7075
- if (live.pickUp === 'check') {
7076
- return '<li class="sstopped" data-onboard-step="app-install">' + title + '<p>GitHub stopped before the app was installed on your repo. ' + NO_SECOND_APP + '</p>' +
7077
- '<a class="ghbtn" href="' + esc(installUrl) + '" target="_blank" rel="noopener" data-action="pick-up">' + GH_ICON + 'Pick up where it stopped ↗</a></li>';
6919
+ var all = home.concat(gh);
6920
+ var bad = all.filter(function (r) { return r.state === 'bad'; })[0];
6921
+ if (bad) return [{ t: 'One step stopped.', b: true }, { t: ' ' + (gh.indexOf(bad) !== -1 ? NO_SECOND_APP : 'Nothing is lost.') }];
6922
+ if (!hostOk(inst.domain)) return [{ t: 'It has no address yet, so GitHub has to wait. Attach one any time from ' }, { t: 'its own page', href: manageHref() }, { t: '.' }];
6923
+ var homeDone = home.every(factDone);
6924
+ var approve = gh[0], install = gh[1];
6925
+ if (approve.state === 'you' && live.manifest) return [{ t: 'Your turn on GitHub:', b: true }, { t: ' approve the app in the tab that opened. This page ticks on its own when you do, on any device.' }];
6926
+ if (approve.state === 'you') {
6927
+ return homeDone ? [{ t: 'Its home is ready. ' }, { t: 'Your turn: connect GitHub.', b: true }]
6928
+ : [{ t: 'Setting up its home. ' }, { t: 'You can connect GitHub now', b: true }, { t: '; nothing waits on you otherwise.' }];
7078
6929
  }
7079
- return '<li data-onboard-step="app-install">' + title + '<p>Your GitHub tab goes here next. We check with GitHub that the app is on your repo, and this step ticks itself. Came back without installing it?</p>' +
7080
- '<a class="ghbtn" href="' + esc(installUrl) + '" target="_blank" rel="noopener">' + GH_ICON + 'Install it on your repo ↗</a></li>';
7081
- }
7082
-
7083
- /* first-sign-in — "Builders can sign in": ticks when the platform has seen sign-in
7084
- work on the project (the runner's auth_configured check, the manifest 'done'). The
7085
- door opens only when the project ANSWERS too: a link to an address that does not
7086
- resolve yet is the worst first impression a founder can get (task 1003232). */
7087
- function renderStepFirstSignIn() {
7088
- var domain = resolvedDomain();
7089
- var title = '<b>Builders can sign in</b>';
7090
- if (!domain) {
7091
- return '<li class="slocked" data-onboard-step="first-sign-in">' + title + '<p>Unlocks with an address — then you open it, sign in with GitHub once, and take the owner’s seat (the founding Archon).</p></li>';
6930
+ if (install.state === 'you') return [{ t: 'Your turn on GitHub:', b: true }, { t: ' confirm the install on ' + (repoName() || 'your repo') + '. The repo is already picked.' }];
6931
+ if (gh.some(function (r) { return r.state === 'now'; })) return [{ t: 'Nothing is needed from you. The platform is checking with GitHub, and this page ticks on its own.' }];
6932
+ return [{ t: homeDone ? 'Its home is ready.' : 'Setting up its home. Nothing is needed from you.' }];
6933
+ }
6934
+
6935
+ /* ── drawing it: nodes and textContent only (no innerHTML), change-only so a poll that
6936
+ moved nothing writes nothing, and a fact's buttons are rebuilt only when the fact
6937
+ itself changed (its "checked 6s ago" is rewritten in place). The sentence is the
6938
+ one live region (#setupNow), written only when its words change. ── */
6939
+ var FACT_MARK = { done: '✓', bad: '!', you: '→', now: '', todo: '' };
6940
+ var FACT_SAID = { done: 'done', bad: 'stopped', you: 'your turn', now: 'under way', todo: 'not yet' };
6941
+ /* the canonical onboard plan's human step each GitHub fact is (modules/provisioning/
6942
+ onboard-plan.js); the plan's other two live on the page too — github-repo is the new
6943
+ repo part 02 makes at Create (#ghFresh), enter-hall the Enter Genesis door. The drift
6944
+ guard in tests/onboard.mjs holds the set equal to the plan's. */
6945
+ var FACT_PLAN_STEP = { approve: 'oauth-app', install: 'app-install', sign_in: 'first-sign-in', device_flow: 'device-flow' };
6946
+ var factsDrawn = {}; /* fact key → what was last drawn, without its right-hand time */
6947
+ var sentenceSaid = '';
6948
+ function textNode(tag, cls, text) {
6949
+ var e = document.createElement(tag);
6950
+ if (cls) e.className = cls;
6951
+ if (text != null) e.textContent = text;
6952
+ return e;
6953
+ }
6954
+ /* the only places a part 03 link may go: GitHub's own pages (the app's install page and
6955
+ settings), this page's own views, and this platform's sign-in. Anything else — a value
6956
+ a read handed over that is not one of these — is drawn as plain text, never a link. PURE. */
6957
+ function factHref(h) {
6958
+ h = String(h || '');
6959
+ return /^https:\/\/github\.com\/[A-Za-z0-9/_.?=&%-]*$/.test(h) || /^\?view=[a-z]+(&id=[0-9A-Za-z%]+)?$/.test(h) || /^\/api\/[A-Za-z0-9/_.?=&%-]*$/.test(h) ? h : '';
6960
+ }
6961
+ function partsInto(el, parts) {
6962
+ parts.forEach(function (p) {
6963
+ if (typeof p === 'string') p = { t: p };
6964
+ if (p.href && !factHref(p.href)) p = { t: p.t };
6965
+ var n;
6966
+ if (p.href) {
6967
+ n = textNode('a', null, p.t);
6968
+ n.setAttribute('href', p.href);
6969
+ if (/^https:\/\//.test(p.href)) { n.setAttribute('target', '_blank'); n.setAttribute('rel', 'noopener'); }
6970
+ } else n = p.b ? textNode('b', null, p.t) : document.createTextNode(p.t);
6971
+ el.appendChild(n);
6972
+ });
6973
+ }
6974
+ function factAct(k, r) {
6975
+ var b;
6976
+ if (k === 'pick-up-link' || k === 'install-link') {
6977
+ b = textNode('a', k === 'pick-up-link' ? 'btn small ghost' : 'linklike', k === 'pick-up-link' ? 'Pick up where it stopped ↗' : 'Open the install page ↗');
6978
+ /* the install page of THIS app on GitHub (seedSetup and ghFacts both build it to that
6979
+ shape); anything else falls back to the founder's own GitHub installations list */
6980
+ b.setAttribute('href', /^https:\/\/github\.com\/apps\/[A-Za-z0-9-]+\/installations\/new(\?state=[A-Za-z0-9%_-]+)?$/.test(r.href || '') ? r.href : 'https://github.com/settings/installations');
6981
+ b.setAttribute('target', '_blank'); b.setAttribute('rel', 'noopener');
6982
+ if (k === 'pick-up-link') b.setAttribute('data-action', 'pick-up');
6983
+ return b;
7092
6984
  }
7093
- /* the app and its keys are placed, but the project never confirmed sign-in (task
7094
- 1004652): the runner re-asks it with the keys already there, so picking it up never
7095
- makes a second app — and a project that still will not answer goes to the platform team */
7096
- if (live.manifest === 'error' && live.manifestFailure === 'sign_in_unconfirmed') {
7097
- return '<li' + (live.verifying ? '' : ' class="sstopped"') + ' data-onboard-step="first-sign-in">' + title + '<p>' +
7098
- (live.verifying
7099
- ? 'Your project hasn’t confirmed sign-in yet. We’re asking it again, with the app GitHub already made, and this step ticks itself.'
7100
- : 'Your project didn’t confirm that sign-in works. ' + NO_SECOND_APP) + '</p>' +
7101
- (live.pickUp === 'check' ? pickUpBtn() : '') + askPlatformBtn() + '</li>';
6985
+ if (k === 'manage') { b = textNode('a', 'btn small ghost', r.actText); b.setAttribute('href', manageHref()); return b; }
6986
+ var words = { connect: 'Connect GitHub ↗', reopen: 'Reopen GitHub ↗', 'pick-up': 'Pick up where it stopped', 'check-again': 'Turned it on? Check again', 'ask-platform': 'Send it to the platform team' };
6987
+ var cls = { connect: 'btn', reopen: 'btn small ghost', 'pick-up': 'btn small ghost', 'check-again': 'linklike', 'ask-platform': 'linklike' };
6988
+ var action = { connect: 'manifest-start', reopen: 'reopen-github', 'pick-up': 'pick-up', 'check-again': 'pick-up', 'ask-platform': 'ask-platform' };
6989
+ b = textNode('button', cls[k], words[k]);
6990
+ b.setAttribute('type', 'button');
6991
+ b.setAttribute('data-action', action[k]);
6992
+ return b;
6993
+ }
6994
+ function factNode(r) {
6995
+ var f = textNode('div', 'setFact ' + r.state);
6996
+ f.setAttribute('role', 'listitem');
6997
+ f.setAttribute('data-fact', r.key);
6998
+ if (FACT_PLAN_STEP[r.key]) f.setAttribute('data-onboard-step', FACT_PLAN_STEP[r.key]);
6999
+ var d = textNode('span', 'd', FACT_MARK[r.state]);
7000
+ d.setAttribute('aria-hidden', 'true');
7001
+ f.appendChild(d);
7002
+ var t = textNode('span', 't', r.title);
7003
+ if (r.opt) t.appendChild(textNode('span', 'optional', 'optional'));
7004
+ t.appendChild(textNode('span', 'vh', ', ' + FACT_SAID[r.state]));
7005
+ if (r.sub && r.sub.length) { var sm = textNode('small'); partsInto(sm, [].concat(r.sub)); t.appendChild(sm); }
7006
+ f.appendChild(t);
7007
+ f.appendChild(textNode('span', 'r', r.right || ''));
7008
+ var acts = [].concat(r.act || []);
7009
+ if (acts.length || r.tech) {
7010
+ var box = textNode('div', 'acts');
7011
+ acts.forEach(function (k) { box.appendChild(factAct(k, r)); });
7012
+ if (r.tech) {
7013
+ var det = textNode('details');
7014
+ det.appendChild(textNode('summary', null, 'Technical details'));
7015
+ det.appendChild(textNode('p', null, r.tech));
7016
+ box.appendChild(det);
7017
+ }
7018
+ f.appendChild(box);
7102
7019
  }
7103
- if (live.manifest !== 'done') {
7104
- return '<li class="slocked" data-onboard-step="first-sign-in">' + title + '<p>Ticks once the platform has seen sign-in work on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code>.</p></li>';
7020
+ return f;
7021
+ }
7022
+ function drawFacts(list, rows) {
7023
+ if (!list) return;
7024
+ rows.forEach(function (r, i) {
7025
+ var shape = JSON.stringify(Object.assign({}, r, { right: '' }));
7026
+ var have = list.children[i];
7027
+ if (have && have.getAttribute('data-fact') === r.key && factsDrawn[r.key] === shape) {
7028
+ var right = have.querySelector('.r');
7029
+ if (right && right.textContent !== (r.right || '')) right.textContent = r.right || '';
7030
+ return;
7031
+ }
7032
+ var fresh = factNode(r);
7033
+ if (have) { parkAppPick(have); list.replaceChild(fresh, have); } else list.appendChild(fresh);
7034
+ factsDrawn[r.key] = shape;
7035
+ });
7036
+ while (list.children.length > rows.length) list.removeChild(list.lastChild);
7037
+ }
7038
+ /* the account choice sits beside Connect GitHub (task 1004651): moved into the Approve
7039
+ fact while Connect is offered, and back to its own place otherwise */
7040
+ function parkAppPick(from) {
7041
+ var box = $('aaPick');
7042
+ if (box && from && from.contains(box)) $('doneBody').insertBefore(box, $('hopLink'));
7043
+ }
7044
+ function placeAppPick(offered) {
7045
+ var box = $('aaPick');
7046
+ var acts = offered ? document.querySelector('#ghFacts [data-fact="approve"] .acts') : null;
7047
+ if (acts && box.parentNode !== acts) acts.appendChild(box);
7048
+ else if (!acts) parkAppPick($('ghFacts'));
7049
+ syncAppPick(!!acts);
7050
+ }
7051
+ /* every required fact done: "<name> is alive.", the invite once the hall answers (PS15)
7052
+ and the door into Genesis, the hall's founding mode, on the address that answered */
7053
+ function paintAlive(ready) {
7054
+ var inst = live.inst;
7055
+ var on = !!(ready && inst && hostOk(inst.domain) && addressReady(inst) && inst.last_verified_up === true);
7056
+ $('done-h').textContent = projectName() + ' is alive.';
7057
+ var box = $('doneAlive');
7058
+ if (box.hidden === !on) return;
7059
+ box.hidden = !on;
7060
+ /* inst.domain has passed hostOk (a bare host name, nothing else), so this is always an
7061
+ https URL to the project's own address — stricter than safeHttpUrl, which takes any URL */
7062
+ if (on) $('enterGenesis').setAttribute('href', 'https://' + inst.domain);
7063
+ else $('enterGenesis').removeAttribute('href');
7064
+ paintInviteGate(on);
7065
+ if (on) hopArrive();
7066
+ }
7067
+ function paintFacts() {
7068
+ var title = $('donePartT'), count = $('ghState');
7069
+ if (!title || !count || !live) return; /* hoisted: show() can ask before live is set */
7070
+ if (state.step !== 2) { title.textContent = 'Connect GitHub'; count.textContent = 'opens once your project exists'; return; }
7071
+ var now = Date.now();
7072
+ var home = homeFacts(live.inst, now), gh = ghFacts(now);
7073
+ var ready = setupReady(home, gh);
7074
+ title.textContent = 'Getting ' + projectName() + ' ready';
7075
+ count.textContent = setupCount(home, gh);
7076
+ drawFacts($('homeFacts'), home);
7077
+ drawFacts($('ghFacts'), gh);
7078
+ placeAppPick([].concat(gh[0].act || []).indexOf('connect') !== -1);
7079
+ var said = setupSentence(home, gh), key = JSON.stringify(said);
7080
+ if (key !== sentenceSaid) {
7081
+ sentenceSaid = key;
7082
+ var line = $('setupNow');
7083
+ line.textContent = '';
7084
+ partsInto(line, said);
7105
7085
  }
7106
- var open = live.instStatus === 'active' && live.addrReady;
7107
- return '<li class="sdone" data-onboard-step="first-sign-in">' + title + '<p>Sign-in works on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code>.' +
7108
- (open ? ' Open it and sign in with GitHub once to take the owner’s seat — the founding Archon.' : ' The door opens here once your project is answering.') + '</p>' +
7109
- (open ? '<a class="ghbtn" href="https://' + esc(domain) + '" target="_blank" rel="noopener">Open ' + esc(domain) + ' →</a>' : '') + '</li>';
7110
- }
7111
-
7112
- /* enter-hall — not a row of the GitHub checklist (the "is alive" block below carries
7113
- the way in); kept only for a project with no address, whose hall is local */
7114
- function renderStepEnterHall() {
7115
- var domain = resolvedDomain();
7116
- if (domain) return '';
7117
- return '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>No address yet — run <code style="font-family:var(--font-mono)">bongos dev</code> in your repo checkout to start the local hall on your machine (on an address it lives at /builders).</p></li>';
7118
- }
7119
-
7120
- var STEP_RENDERERS = {
7121
- 'github-repo': renderStepGithubRepo,
7122
- 'oauth-app': renderStepOauthApp,
7123
- 'device-flow': renderStepDeviceFlow,
7124
- 'app-install': renderStepAppInstall,
7125
- 'first-sign-in': renderStepFirstSignIn,
7126
- 'enter-hall': renderStepEnterHall
7127
- };
7086
+ $('hopLink').hidden = ready;
7087
+ paintAlive(ready);
7088
+ }
7089
+ /* a fact's buttons drawn afresh — after an action that may have changed nothing the
7090
+ server says (a refused start, a pick-up that did not go through) */
7091
+ function repaintFacts() { factsDrawn = {}; paintFacts(); }
7128
7092
 
7129
7093
  /* The newborn-instance welcome + account-nag suppression (direction §5.6,
7130
7094
  idea 1000701) is NOT implemented here by design: it lives on the CHILD
@@ -7132,42 +7096,19 @@ summary{min-height:24px;padding:3px 0;}
7132
7096
  control-plane wizard cannot reach. Spun out per the direction's §9
7133
7097
  delivery-split as task 1002739. */
7134
7098
 
7135
- /* the mockup's order: the repo (only when the founder must make it), approve,
7136
- install, sign-in, command line; a plan step this page has no row for goes last */
7137
- var STEP_ORDER = ['github-repo', 'oauth-app', 'app-install', 'first-sign-in', 'device-flow', 'enter-hall'];
7138
- function stepRank(s) { var i = STEP_ORDER.indexOf(s.key); return i < 0 ? STEP_ORDER.length : i; }
7139
- function renderSteps() {
7140
- fetchPlan().then(function (steps) {
7141
- var html = steps.filter(function (s) { return s && s.kind === 'human'; }).slice().sort(function (a, b) { return stepRank(a) - stepRank(b); }).map(function (s) {
7142
- var r = STEP_RENDERERS[s.key];
7143
- return r ? r(s) : '<li data-onboard-step="' + esc(s.key) + '"><b>' + esc(s.label || s.key) + '</b></li>';
7144
- }).join('');
7145
- /* the account picker stands beside Connect GitHub, so it shows while that is offered */
7146
- syncAppPick(html.indexOf('data-action="manifest-start"') !== -1);
7147
- if (html === lastStepsHtml) return; /* change-only re-render — polls never clobber open UI */
7148
- lastStepsHtml = html;
7149
- $('humanSteps').innerHTML = html;
7150
- if (typeof stepsPaint === 'function') stepsPaint();
7151
- maybeAutoOpen();
7152
- });
7153
- }
7154
-
7155
7099
  function renderDone() {
7156
- var adopt = state.mode === 'adopt';
7157
7100
  $('doneRender').hidden = true; // sendRenderSetup shows it again when Render is the choice
7158
7101
  $('doneApp').hidden = true; $('doneApp').textContent = '';
7159
- $('done-h').textContent = 'Setting up ' + (state.name || state.slug || 'your project');
7160
- $('doneLede').innerHTML = 'We’re setting up <strong>' + esc(state.name || state.slug) + '</strong>' +
7161
- (adopt ? ' on your existing repo' : '') +
7162
- ': its server, database and address. Watch it here, or any time on <a href="?view=mine">your projects page</a>.';
7163
7102
  /* the picker's later half (task 1002342, BV1.R23). The modules step is
7164
7103
  skippable, so this panel names where it lives afterwards rather than
7165
7104
  leaving an owner to go looking for it. */
7166
- $('doneModules').innerHTML = 'Add or remove modules later in ' + projectPageLink('the project’s settings') + '.';
7105
+ var mods = $('doneModules');
7106
+ mods.textContent = '';
7107
+ partsInto(mods, ['Add or remove modules later in ', { t: 'the project’s settings', href: manageHref() }, '.']);
7167
7108
  /* the invite step waits for the hall (task 1004503): until it answers, one line */
7168
7109
  paintInviteGate(false);
7169
- setMoment('wait'); /* the cloud hop until the project answers (task 1004578) */
7170
- renderSteps();
7110
+ hopOpen(false); /* the cloud hop waits behind its link until asked (task 1004683) */
7111
+ paintFacts();
7171
7112
  }
7172
7113
 
7173
7114
  /* The wizard's own copy links between views ("your projects page", the
@@ -7190,30 +7131,20 @@ summary{min-height:24px;padding:3px 0;}
7190
7131
  setView(to, true);
7191
7132
  });
7192
7133
 
7193
- /* One delegated listener — renderSteps swaps innerHTML, so handlers live on the list. */
7194
- $('humanSteps').addEventListener('click', function (ev) {
7195
- var mBtn = ev.target.closest('[data-action="manifest-start"]');
7196
- if (mBtn) { startManifest(mBtn); return; }
7197
- /* task 1004652: the pick-up link to GitHub's install page keeps its default (it opens) */
7198
- var pBtn = ev.target.closest('[data-action="pick-up"]');
7199
- if (pBtn) { pickUpSetup(pBtn); return; }
7200
- var rBtn = ev.target.closest('[data-action="reopen-github"]');
7201
- if (rBtn) { reopenGithub(rBtn); return; }
7202
- var aBtn = ev.target.closest('[data-action="ask-platform"]');
7203
- if (aBtn) { askPlatform(aBtn); return; }
7204
- var t = ev.target.closest('.copy');
7205
- if (!t) return;
7206
- navigator.clipboard.writeText(t.getAttribute('data-copy')).then(function () {
7207
- t.classList.add('did'); t.textContent = 'Copied';
7208
- setTimeout(function () { t.classList.remove('did'); t.textContent = 'Copy'; }, 1600);
7209
- });
7210
- });
7211
-
7212
- $('humanSteps').addEventListener('change', function (ev) {
7213
- var box = ev.target.closest && ev.target.closest('[data-action="auto-open"]');
7214
- if (!box) return;
7215
- try { localStorage.setItem(AUTO_OPEN_KEY, box.checked ? '1' : '0'); } catch (e) {}
7134
+ /* One delegated listener for part 03's actions — paintFacts redraws a fact's buttons,
7135
+ so the handlers live on the body. The pick-up LINK to GitHub's install page keeps its
7136
+ default (it opens) and asks the runner to look again (task 1004652). */
7137
+ $('doneBody').addEventListener('click', function (ev) {
7138
+ var b = ev.target.closest && ev.target.closest('[data-action]');
7139
+ if (!b) return;
7140
+ var a = b.getAttribute('data-action');
7141
+ if (a === 'manifest-start') startManifest(b);
7142
+ else if (a === 'pick-up') pickUpSetup(b);
7143
+ else if (a === 'reopen-github') reopenGithub(b);
7144
+ else if (a === 'ask-platform') askPlatform(b);
7216
7145
  });
7146
+ /* the right-hand "checked 6s ago" stays true between reads (text only, change-only) */
7147
+ setInterval(function () { if (currentView === 'new' && state.step === 2 && !document.hidden) paintFacts(); }, 15000);
7217
7148
 
7218
7149
  /* ── C8: the wizard's polls run only while the create view is on stage, like the
7219
7150
  sky. Off-view a due tick parks itself instead of fetching; enter() resumes it.
@@ -7325,8 +7256,7 @@ summary{min-height:24px;padding:3px 0;}
7325
7256
  function applyPickUp(d) {
7326
7257
  if (typeof d.status === 'string') { live.manifest = d.status; live.manifestFailure = d.failure || null; rememberManifest(undefined, d.status); }
7327
7258
  seedSetup(d.checklist, d.install_url, d);
7328
- lastStepsHtml = '';
7329
- renderSteps();
7259
+ repaintFacts();
7330
7260
  }
7331
7261
  /* …and keep watching: the attempt's own poll when this tab knows it, else the instance read */
7332
7262
  function watchSetupAgain() {
@@ -7353,7 +7283,7 @@ summary{min-height:24px;padding:3px 0;}
7353
7283
  }).then(function (res) {
7354
7284
  live.askedPlatform = res.ok ? true : 'failed';
7355
7285
  }).catch(function () { live.askedPlatform = 'failed'; })
7356
- .then(function () { lastStepsHtml = ''; renderSteps(); });
7286
+ .then(repaintFacts);
7357
7287
  }
7358
7288
 
7359
7289
  function startManifest(btn) {
@@ -7390,7 +7320,7 @@ summary{min-height:24px;padding:3px 0;}
7390
7320
  noteAppOrg(d.account === 'org' ? d.org_login : null);
7391
7321
  manifestStartedAt = Date.now();
7392
7322
  rememberManifest(d.state, 'pending');
7393
- renderSteps();
7323
+ paintFacts();
7394
7324
  pollManifest(d.state);
7395
7325
  }).catch(function (err) {
7396
7326
  if (String(err && err.message) === 'signing in') return;
@@ -7398,13 +7328,13 @@ summary{min-height:24px;padding:3px 0;}
7398
7328
  /* GitHub already approved an app for this project (task 1004652): no second one —
7399
7329
  this page was behind, so it re-learns the truth and draws that step's own action */
7400
7330
  if (err && (err.code === 'app_exists' || err.code === 'github_in_progress')) {
7401
- lastStepsHtml = '';
7331
+ repaintFacts();
7402
7332
  if (instanceId) startProgress(instanceId);
7403
7333
  return;
7404
7334
  }
7405
7335
  live.manifest = 'error'; live.manifestFailure = null; live.manifestMsg = String((err && err.message) || 'could not start');
7406
7336
  live.manifestCode = String((err && err.code) || '');
7407
- renderSteps();
7337
+ repaintFacts();
7408
7338
  });
7409
7339
  }
7410
7340
 
@@ -7422,8 +7352,10 @@ summary{min-height:24px;padding:3px 0;}
7422
7352
  var moreMoved = false;
7423
7353
  if (more && typeof more === 'object') {
7424
7354
  var pick = typeof more.pick_up === 'string' ? more.pick_up : null, ver = !!more.verifying;
7425
- moreMoved = pick !== live.pickUp || ver !== live.verifying;
7426
- live.pickUp = pick; live.verifying = ver;
7355
+ /* the repo it installs on: github-setup.js targetRepo, strict, re-checked here */
7356
+ var repo = typeof more.repo === 'string' && /^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}$/.test(more.repo) ? more.repo : live.repo;
7357
+ moreMoved = pick !== live.pickUp || ver !== live.verifying || repo !== live.repo;
7358
+ live.pickUp = pick; live.verifying = ver; live.repo = repo;
7427
7359
  }
7428
7360
  var next = JSON.stringify(checklist), was = live.setup ? JSON.stringify(live.setup) : '';
7429
7361
  var url = typeof installUrl === 'string' && /^https:\/\/github\.com\/apps\//.test(installUrl) ? installUrl : null;
@@ -7466,7 +7398,7 @@ summary{min-height:24px;padding:3px 0;}
7466
7398
  }
7467
7399
  var focused = !document.hidden && (typeof document.hasFocus !== 'function' || document.hasFocus());
7468
7400
  var next = ghTabState(ghTab, ghTabLeft, ghTabSince, Date.now(), focused);
7469
- if (next !== ghTabTrouble) { ghTabTrouble = next; renderSteps(); }
7401
+ if (next !== ghTabTrouble) { ghTabTrouble = next; paintFacts(); }
7470
7402
  }
7471
7403
  function pollManifest(stateToken) {
7472
7404
  stopManifestPoll();
@@ -7480,14 +7412,15 @@ summary{min-height:24px;padding:3px 0;}
7480
7412
  .then(function (res) { if (!res.ok) throw new Error('soft'); return res.json(); })
7481
7413
  .then(function (d) {
7482
7414
  if (myGen !== manifestGen) return;
7483
- if (d.app_slug && d.app_slug !== live.appSlug) { live.appSlug = d.app_slug; rememberManifest(undefined, undefined, d.app_slug); renderSteps(); }
7415
+ live.readAt = Date.now(); /* the state-keyed read is a server read too */
7416
+ if (d.app_slug && d.app_slug !== live.appSlug) { live.appSlug = d.app_slug; rememberManifest(undefined, undefined, d.app_slug); paintFacts(); }
7484
7417
  /* the server-verified ticks (task 1004649), and whose account the app is on */
7485
7418
  var setupMoved = seedSetup(d.checklist, d.install_url, d);
7486
7419
  if (noteAppOrg(d.app_org)) setupMoved = true;
7487
- if (setupMoved) renderSteps();
7420
+ if (setupMoved) paintFacts();
7488
7421
  var next = d.ready ? 'done' : (d.status || 'pending');
7489
7422
  var failure = d.failure || null;
7490
- if (next !== live.manifest || failure !== live.manifestFailure) { live.manifest = next; live.manifestFailure = failure; rememberManifest(undefined, next); renderSteps(); }
7423
+ if (next !== live.manifest || failure !== live.manifestFailure) { live.manifest = next; live.manifestFailure = failure; rememberManifest(undefined, next); paintFacts(); }
7491
7424
  else noteManifestWait();
7492
7425
  if (!d.terminal) manifestPollTimer = setTimeout(tick, wizPollDelay(4000));
7493
7426
  else manifestPollTimer = null; /* a stale fired id would read as "still polling" and resume a finished poll */
@@ -7497,179 +7430,23 @@ summary{min-height:24px;padding:3px 0;}
7497
7430
  tick();
7498
7431
  }
7499
7432
 
7500
- /* ── live build progress (task 2083): poll the instance until it's up.
7501
- Read-only — GET /provisioning/instances/:id (own-scoped) every 5s. ── */
7433
+ /* ── the owner's instance read (task 2083): poll the project until it is up.
7434
+ Read-only — GET /provisioning/instances/:id (own-scoped) every 5s. That one read
7435
+ is part 03's Its home, and its github_setup is the GitHub checklist after a reload
7436
+ (task 1004683: no stage strip, no tick counter — the facts are the read). ── */
7502
7437
  var pollTimer = null;
7503
7438
  var watchPending = false; /* a parked progress watch, resumed on re-entry */
7504
7439
  var watchGen = 0; /* same orphaning contract as manifestGen above */
7505
7440
  function stopPolling() { watchGen += 1; if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } }
7506
- /* The done panel's own flow vocabularies (F22). A local mirror of the mine
7507
- view's FLOWS — that table lives in the OTHER page IIFE, out of scope here,
7508
- the same isolation that makes .prog duplicate .strip. */
7509
- var PROG_FLOWS = {
7510
- standup: { names: ['Request filed', 'Standing it up', 'Live'], cls: '' },
7511
- revive: { names: ['Revive queued', 'Standing it up', 'Back live'], cls: '' },
7512
- teardown: { names: ['Teardown queued', 'Winding down', 'Torn down'], cls: 'flowDown' },
7513
- settings: { names: ['Change saved', 'Restarting to apply it', 'Applied'], cls: '' },
7514
- /* the GitHub sign-in wait, drawn inside its checklist step (task 1004548) */
7515
- signin: { names: ['Approve on GitHub', 'Placing credentials', 'Ready to sign in'], cls: '' }
7516
- };
7517
- /* No live-region attributes here: this strip is destroyed and rebuilt on
7518
- every 5s tick, and a live region that is itself replaced never announces.
7519
- #liveProgress is the stable region; this is only its contents. (The
7520
- mine-card twin stripMarkup() still carries its own — left for a later pass.) */
7521
- function progressMarkup(stageNow, stagesDone, note, cls, flow) {
7522
- var f = PROG_FLOWS[flow || 'standup'];
7523
- return '<div class="prog ' + (f.cls ? f.cls + ' ' : '') + (cls || '') + '">' +
7524
- '<ul class="stages">' + f.names.map(function (n, i) {
7525
- var c = stagesDone > i ? 'done' : (stageNow === i ? 'now' : '');
7526
- /* the dot carries the state for the eye only — a screen reader needs
7527
- the word, and the live stage marked, or it hears three bare names */
7528
- var word = c === 'done' ? 'done' : (c === 'now' ? 'current' : 'upcoming');
7529
- return '<li class="' + c + '"' + (c === 'now' ? ' aria-current="step"' : '') + '>' +
7530
- '<span class="sdot"></span><span class="slbl">' + n + '</span>' +
7531
- '<span class="vh">, ' + word + '</span></li>';
7532
- }).join('') + '</ul>' +
7533
- '<p class="pnote">' + note + '</p></div>';
7534
- }
7535
- /* Change-only write into the stable live region: an identical tick performs
7536
- no DOM write at all, so the region announces real movement only — the same
7537
- contract renderSteps() uses with lastStepsHtml. */
7538
- function setProgress(html) {
7539
- var el = $('liveProgress');
7540
- if (!el || html === lastProgHtml) return;
7541
- lastProgHtml = html;
7542
- el.innerHTML = html;
7543
- if (typeof hopSync === 'function') hopSync(el);
7544
- }
7545
- /* The done panel's moment (task 1004578): 'wait' while the project is being
7546
- stood up (the cloud hop), 'arrive' once it answers, and 'plain' for a snag,
7547
- a teardown, a settings change, or a project standing with no address (it
7548
- will never answer, so there is nothing to arrive at) — each keeps the strip
7549
- as it always was. Same branch order as renderProgress below. */
7550
- function momentFor(inst) {
7551
- var s = inst && inst.status, oi = inst && inst.open_intent;
7552
- /* only the intents renderProgress itself speaks for count — any other
7553
- (a running standup, say) falls through to the status, as it does there */
7554
- var spoken = oi && s !== 'provisioning' && s !== 'tearing_down' && s !== 'requested' ? oi.action : '';
7555
- if (spoken === 'teardown' || spoken === 'settings-apply') return 'plain';
7556
- if (spoken === 'provision') return 'wait';
7557
- if (s === 'error' || s === 'tearing_down' || s === 'torn_down') return 'plain';
7558
- if (s === 'active') {
7559
- if (!inst.domain) return 'plain';
7560
- if (addressReady(inst) && inst.last_verified_up) return 'arrive';
7561
- }
7562
- return 'wait';
7563
- }
7564
- function setMoment(m) {
7565
- var panel = $('panel-done');
7566
- if (!panel) return;
7567
- if (typeof hopMoment === 'function') hopMoment(panel, m);
7568
- else panel.setAttribute('data-moment', m);
7569
- }
7570
- /* Renders the current instance state; returns true when the state is terminal
7571
- (no further polling wanted). The strip is chosen by the OPEN INTENT first,
7572
- then status (F22): a torn-down instance with a queued revive must never be
7573
- told to "start another project". */
7574
- function renderProgress(inst) {
7575
- if (!$('liveProgress')) return true;
7576
- if (inst) setMoment(momentFor(inst));
7577
- var s = inst && inst.status;
7578
- var revive = !!(inst && inst.torn_down_at);
7579
- var oi = inst && inst.open_intent;
7580
- /* requested/provisioning/tearing_down speak their STATUS (a requested
7581
- instance always has its provision intent open — the intent branch
7582
- would make the status branch dead and diverge from the mine card) */
7583
- if (oi && s !== 'provisioning' && s !== 'tearing_down' && s !== 'requested') {
7584
- if (oi.action === 'provision') {
7585
- setProgress(progressMarkup(0, 0,
7586
- (s === 'torn_down' ? 'A revive is queued' : 'A standup is queued') + ' — this panel updates when it starts.',
7587
- '', s === 'torn_down' ? 'revive' : 'standup'));
7588
- return false;
7589
- }
7590
- if (oi.action === 'teardown') {
7591
- setProgress(progressMarkup(0, 0, 'A teardown is queued — this panel updates when it starts.', '', 'teardown'));
7592
- return false;
7593
- }
7594
- /* task 1003140: a settings change restarts the project — the panel says so, by state */
7595
- if (oi.action === 'settings-apply') {
7596
- setProgress(oi.state === 'running'
7597
- ? progressMarkup(1, 1, 'Applying a settings change — the project restarts to pick it up.', '', 'settings')
7598
- : progressMarkup(0, 0, 'A settings change is queued — this panel updates when it starts.', '', 'settings'));
7599
- return false;
7600
- }
7601
- }
7602
- if (s === 'requested') {
7603
- setProgress(progressMarkup(0, 0, 'Filed — waiting for the platform to pick it up. This updates the moment it starts.', '', revive ? 'revive' : 'standup'));
7604
- } else if (s === 'provisioning') {
7605
- setProgress(progressMarkup(1, 1,
7606
- revive ? 'Bringing it back on the newest platform version — nothing needed from you.'
7607
- : 'Raising the server, database, and address — nothing needed from you.',
7608
- '', revive ? 'revive' : 'standup'));
7609
- } else if (s === 'active') {
7610
- /* Live lights on a VERIFIED answer, not on script completion (direction
7611
- §7): the strip keys on the same last_verified_* overlay the manage
7612
- list's chips read. null is deliberately distinct from false — a fresh
7613
- standup stays unprobed for up to a sweep interval and must not read
7614
- as a failure. */
7615
- var addr = inst.domain ? 'https://' + inst.domain : null;
7616
- var link = addr ? '<a href="' + esc(addr) + '" target="_blank" rel="noopener">' + esc(inst.domain) + '</a>' : null;
7617
- var flow = revive ? 'revive' : 'standup';
7618
- if (!addr) {
7619
- /* domainless instances are never probed — there is no address to
7620
- answer at, so a verified answer can never arrive; say what IS true
7621
- instead of promising the hall (sign-in stays locked without one) */
7622
- setProgress(progressMarkup(-1, 2,
7623
- 'The build finished — your project is standing, but it has no address yet, so sign-in can’t be wired up. Attach a domain any time from ' +
7624
- projectPageLink('its own page') + '; the steps below that need one unlock then.', '', flow));
7625
- return true;
7626
- }
7627
- /* task 1004505: no link until the platform's own lookup has seen the name
7628
- answer. A founder's first click on a name that does not resolve yet is
7629
- cached as "not found" by their resolver for up to half an hour. */
7630
- if (!addressReady(inst)) {
7631
- setProgress(progressMarkup(2, 2,
7632
- 'The build finished — your address is being set up. This panel links it the moment it answers.', '', flow));
7633
- return false;
7634
- }
7635
- if (inst.last_verified_up === false) {
7636
- setProgress(progressMarkup(2, 2,
7637
- 'The build finished, but ' + link + ' isn’t answering right now — this panel keeps checking.', '', flow));
7638
- return false;
7639
- }
7640
- if (inst.last_verified_up) {
7641
- /* the alive moment (task 1004504): said only once the project has answered */
7642
- $('done-h').textContent = (state.name || state.slug || 'Your project') + ' is alive.';
7643
- paintInviteGate(true); /* the hall answers, so others can join now */
7644
- /* task 1004543: the line under it stops saying "setting up" once it answers */
7645
- $('doneLede').innerHTML = 'Its home is ready. Watch the rest here, or any time on <a href="?view=mine">your projects page</a>.';
7646
- setProgress(progressMarkup(-1, 3,
7647
- 'Your project is answering at ' + link + '. Finish the steps below and the hall is yours.', 'isLive', flow));
7648
- return true;
7649
- }
7650
- /* null: standing, not checked yet — keep watching until the first probe */
7651
- setProgress(progressMarkup(2, 2,
7652
- 'The build finished — checking that ' + link + ' answers…', '', flow));
7653
- return false;
7654
- } else if (s === 'error') {
7655
- /* the honest snag (direction §5 / F19): no raw stderr as the headline, no
7656
- false "keeps retrying" — retries stop, and the retry door is the project's
7657
- own page (task 1003355 moved it off the row) */
7658
- setProgress(progressMarkup(1, 1,
7659
- 'The standup stopped partway — it won’t continue on its own. Retry it (and see the technical details) from ' +
7660
- projectPageLink('its own page') + '.', 'isErr'));
7661
- } else if (s === 'tearing_down') {
7662
- /* a deliberate end is calm, never styled as a failure (direction §5) —
7663
- and it speaks the teardown vocabulary, not the standup's (F22) */
7664
- setProgress(progressMarkup(1, 1, 'Taking the server offline. The repository stays yours; the database is kept.', '', 'teardown'));
7665
- } else if (s === 'torn_down') {
7666
- setProgress(progressMarkup(-1, 3, 'This instance was torn down. Bring it back any time from ' +
7667
- projectPageLink('its own page') + ', or start another below.', '', 'teardown'));
7668
- return true;
7669
- } else {
7670
- setProgress(progressMarkup(0, 0, 'Filed. Watching for the standup to start…'));
7671
- }
7672
- return false;
7441
+ /* Nothing more will change without the founder: every required fact done (and the
7442
+ runner no longer checking), a project switched off, or one standing with no address. */
7443
+ function setupSettled() {
7444
+ var inst = live.inst, oi = inst && inst.open_intent;
7445
+ if (!inst) return false;
7446
+ if (inst.status === 'torn_down' && !(oi && oi.action === 'provision')) return true;
7447
+ if (inst.status === 'active' && !hostOk(inst.domain) && !oi) return true;
7448
+ var now = Date.now();
7449
+ return setupReady(homeFacts(inst, now), ghFacts(now)) && !live.verifying;
7673
7450
  }
7674
7451
  function startProgress(id) {
7675
7452
  if (!id) return;
@@ -7677,41 +7454,21 @@ summary{min-height:24px;padding:3px 0;}
7677
7454
  stopPolling();
7678
7455
  watchPending = false;
7679
7456
  var myGen = watchGen;
7680
- var ticks = 0;
7681
- var watchFlow = 'standup'; /* the vocabulary the strip is speaking (F22) */
7682
- setProgress(progressMarkup(0, 0, 'Filed. Watching for the standup to start…'));
7683
7457
  function tick() {
7684
7458
  if (myGen !== watchGen) return;
7685
7459
  if (!wizPollActive()) { watchPending = true; stopPolling(); return; }
7686
7460
  progressKick = tick;
7687
- ticks += 1;
7688
- if (ticks > 400) {
7689
- /* ~33 min: this tab stops watching, but the standup keeps going — hand
7690
- the long watch to the mine view, the owner's durable surface (task
7691
- 1002693). The old "stop quietly" here was a dead-end lie: the hall
7692
- card it assumed was removed in task 1002675. */
7693
- /* a SNAGGED standup keeps its honest error — "still building" would be
7694
- a lie exactly when the founder most needs the truth */
7695
- if (live.instStatus !== 'error') {
7696
- setProgress(progressMarkup(1, 1,
7697
- 'Still working — this tab will stop watching now. Follow it any time on <a href="?view=mine">your projects page</a>.', '', watchFlow));
7698
- }
7699
- setMoment('plain');
7700
- stopPolling();
7701
- return;
7702
- }
7703
7461
  fetch(API + '/provisioning/instances/' + encodeURIComponent(id), { credentials: 'same-origin' })
7704
7462
  .then(function (res) {
7705
- if (res.status === 401) { throw new Error('gone:Your session expired — <a href="' + API + '/auth/web/start?return=/projects%3Fview%3Dnew&amp;repos=1">sign in again</a> to keep watching.'); }
7706
- if (res.status === 404) { throw new Error('gone:Can’t see this instance any more — it may have been torn down.'); }
7463
+ if (res.status === 401) throw new Error('gone:session');
7464
+ if (res.status === 404) throw new Error('gone:missing');
7707
7465
  if (!res.ok) throw new Error('soft');
7708
7466
  return res.json();
7709
7467
  })
7710
7468
  .then(function (data) {
7711
7469
  if (myGen !== watchGen) return;
7712
7470
  var inst = data.instance;
7713
- if (inst) inst.open_intent = data.open_intent || null; /* F22: the strip reads intents */
7714
- if (inst) watchFlow = inst.torn_down_at ? 'revive' : 'standup';
7471
+ if (inst) inst.open_intent = data.open_intent || null; /* F22: the facts read intents */
7715
7472
  /* server-derived manifest truth (task 1003235): a reload, a second tab
7716
7473
  or another device re-learns the GitHub-App leg from this same read.
7717
7474
  A live manifest poll owns live.manifest while it runs, so the seed
@@ -7726,22 +7483,19 @@ summary{min-height:24px;padding:3px 0;}
7726
7483
  /* the wait is timed from when the link was MINTED, so a reload still
7727
7484
  reaches the "start again" offer (task 1004572) */
7728
7485
  if (!manifestStartedAt && ga.started_at) manifestStartedAt = Date.parse(ga.started_at) || 0;
7729
- if (seedChanged) { rememberManifest(undefined, live.manifest, live.appSlug || undefined); renderSteps(); }
7486
+ if (seedChanged) rememberManifest(undefined, live.manifest, live.appSlug || undefined);
7730
7487
  else noteManifestWait();
7731
7488
  }
7732
7489
  /* the server-verified checklist rides the same read (task 1004649) */
7733
7490
  if (data.github_setup && !manifestPollTimer) {
7734
- var seedMoved = seedSetup(data.github_setup.checklist, data.github_setup.install_url, data.github_setup);
7735
- if (noteAppOrg(data.github_setup.app_org)) seedMoved = true;
7736
- if (seedMoved) renderSteps();
7737
- }
7738
- /* the checklist reflects live state (task 1002694): a status transition —
7739
- e.g. the scaffold landing — re-renders the steps (change-only, so the
7740
- 5s tick never clobbers an open dropdown or mid-flight button) */
7741
- if (inst && (inst.status !== live.instStatus || addressReady(inst) !== live.addrReady)) {
7742
- live.instStatus = inst.status; live.addrReady = addressReady(inst); renderSteps();
7491
+ seedSetup(data.github_setup.checklist, data.github_setup.install_url, data.github_setup);
7492
+ noteAppOrg(data.github_setup.app_org);
7743
7493
  }
7744
- var terminal = renderProgress(inst);
7494
+ /* the read IS Its home: every fact is redrawn from it (change-only) */
7495
+ live.inst = inst || null; live.readAt = Date.now(); live.gone = '';
7496
+ if (inst) { live.instStatus = inst.status; live.addrReady = addressReady(inst); }
7497
+ paintFacts();
7498
+ var terminal = setupSettled();
7745
7499
  /* a GitHub leg still in flight keeps this read alive even once the
7746
7500
  project is up — after a reload it is the ONLY thing watching it */
7747
7501
  if (!terminal || manifestInFlight() || setupOpen()) pollTimer = setTimeout(tick, wizPollDelay(5000));
@@ -7751,8 +7505,9 @@ summary{min-height:24px;padding:3px 0;}
7751
7505
  if (myGen !== watchGen) return;
7752
7506
  var m = String(err && err.message || '');
7753
7507
  if (m.indexOf('gone:') === 0) {
7754
- setProgress(progressMarkup(1, 1, m.slice(5), 'isErr', watchFlow));
7755
- setMoment('plain');
7508
+ /* signed out, or the project is gone: said once, in the sentence */
7509
+ live.gone = m.slice(5);
7510
+ paintFacts();
7756
7511
  stopPolling();
7757
7512
  return;
7758
7513
  }
@@ -7774,9 +7529,8 @@ summary{min-height:24px;padding:3px 0;}
7774
7529
  instanceId = null;
7775
7530
  watchPending = false; manifestPending = false; manifestState = null;
7776
7531
  manifestStartedAt = 0; ghTabTrouble = null;
7777
- live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null, pickUp: null, verifying: false, askedPlatform: false };
7778
- lastStepsHtml = '';
7779
- lastProgHtml = '';
7532
+ live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', manifestFailure: null, appSlug: null, appOrg: null, setup: null, installUrl: null, pickUp: null, verifying: false, askedPlatform: false, inst: null, readAt: 0, gone: '', repo: null };
7533
+ factsDrawn = {}; sentenceSaid = '';
7780
7534
  hopReset();
7781
7535
  try { sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
7782
7536
  /* Every key the initial state declares has to be re-declared here. `modules`
@@ -7985,7 +7739,7 @@ summary{min-height:24px;padding:3px 0;}
7985
7739
  }
7986
7740
  function hopStart() {
7987
7741
  var hopEl = $('hop');
7988
- if (!hop || !hopEl) return; /* hoisted: show() can ask before the hop is declared */
7742
+ if (!hop || !hopEl || hopEl.hidden) return; /* hoisted; and the game runs only once asked */
7989
7743
  hopEl.classList.toggle('isStill', REDUCED);
7990
7744
  hopLoadFeed();
7991
7745
  if (!hopSize()) return;
@@ -8161,28 +7915,31 @@ summary{min-height:24px;padding:3px 0;}
8161
7915
  hop.raf = requestAnimationFrame(hopFrame);
8162
7916
  }
8163
7917
 
8164
- /* the panel's moment, from renderProgress (setMoment). Arriving from a
8165
- game that has been running gets the curtain first; a resume onto a
8166
- project that is already live, or reduced motion, switches straight. */
8167
- function hopMoment(panel, m) {
8168
- if (!hop) { panel.setAttribute('data-moment', m); return; }
8169
- if (hop.target === m) return;
8170
- var was = hop.target;
8171
- hop.target = m;
8172
- if (hop.tr) return; /* the curtain is up — it applies the LATEST target when it lands */
8173
- function apply() {
8174
- panel.setAttribute('data-moment', hop.target);
8175
- $('hop').classList.remove('hopFading');
8176
- if (hop.target === 'wait') hopStart(); else hopStop();
8177
- stepsPaint();
8178
- }
8179
- if (m === 'arrive' && was === 'wait' && !REDUCED && hop.raf && performance.now() - hop.since > 2500) {
7918
+ /* "Play cloud hop while you wait" (task 1004683): the game opens in place only when
7919
+ asked, and the wait never depends on it — the facts above tick whether it is open or
7920
+ not. Hoisted: show() can ask before the hop is declared. */
7921
+ function hopOpen(on) {
7922
+ var box = $('hop'), link = $('hopLink');
7923
+ if (!box || !link) return;
7924
+ box.hidden = !on;
7925
+ box.classList.remove('hopFading');
7926
+ link.setAttribute('aria-expanded', on ? 'true' : 'false');
7927
+ link.textContent = on ? 'Put the game away' : 'Play cloud hop while you wait';
7928
+ if (!hop) return;
7929
+ hop.target = on ? 'wait' : '';
7930
+ if (on) hopStart(); else hopStop();
7931
+ }
7932
+ /* every required fact done: a game that has been running gets the curtain first and
7933
+ then puts itself away; a closed game, or reduced motion, just stays away */
7934
+ function hopArrive() {
7935
+ if (!hop || hop.tr) return; /* the curtain is already up */
7936
+ if (!$('hop').hidden && !REDUCED && hop.raf && performance.now() - hop.since > 2500) {
8180
7937
  cancelAnimationFrame(hop.raf); hop.raf = 0;
8181
- hopCurtain(apply);
7938
+ hopCurtain(function () { hopOpen(false); });
8182
7939
  hop.raf = requestAnimationFrame(hopFrame);
8183
7940
  return;
8184
7941
  }
8185
- apply();
7942
+ hopOpen(false);
8186
7943
  }
8187
7944
  function hopReset() {
8188
7945
  if (!hop) return;
@@ -8190,35 +7947,7 @@ summary{min-height:24px;padding:3px 0;}
8190
7947
  hop.target = ''; hop.since = 0; hop.tr = 0; hop.trDone = null; hop.p = null;
8191
7948
  hop.best = 0; hop.maxed = false;
8192
7949
  if (hop.w) hopBuild();
8193
- $('panel-done').removeAttribute('data-moment');
8194
- }
8195
-
8196
- /* the status line and the bar read what the strip already says — one
8197
- vocabulary, one source (renderProgress), never a second copy */
8198
- function hopSync(el) {
8199
- var st = $('hopStatus');
8200
- if (!st || !el) return;
8201
- var note = el.querySelector('.pnote');
8202
- var text = note ? note.textContent : '';
8203
- if (st.textContent !== text) st.textContent = text;
8204
- var lis = el.querySelectorAll('.stages li').length;
8205
- var done = el.querySelectorAll('.stages li.done').length + (el.querySelector('.stages li.now') ? 0.5 : 0);
8206
- $('hopBarFill').style.width = Math.max(4, lis ? Math.min(100, done / lis * 100) : 0) + '%';
8207
- }
8208
-
8209
- /* Part 03's checklist (task 1004648): every step on show at once, each ticking
8210
- itself from what the server has verified (renderSteps, change-only), and the
8211
- part's state line saying where the founder is. It replaces the paged steps. */
8212
- function stepsPaint() {
8213
- var list = $('humanSteps'), st = $('ghState');
8214
- if (!list || !st) return;
8215
- if (state.step !== 2) { st.textContent = 'opens once your project exists'; return; }
8216
- /* the optional step (command-line sign-in) never holds "connected" back */
8217
- var items = Array.prototype.slice.call(list.children).filter(function (li) { return !li.classList.contains('soptional'); });
8218
- var done = items.filter(function (li) { return li.classList.contains('sdone'); }).length;
8219
- st.textContent = items.length && done === items.length ? 'connected ✓'
8220
- : manifestInFlight() ? 'waiting on GitHub…'
8221
- : done ? done + ' of ' + items.length + ' done' : 'ready';
7950
+ hopOpen(false);
8222
7951
  }
8223
7952
 
8224
7953
  (function hopInput() {
@@ -8249,6 +7978,8 @@ summary{min-height:24px;padding:3px 0;}
8249
7978
  }
8250
7979
  pad('hopLeft', 'L'); pad('hopRight', 'R');
8251
7980
  $('hopJump').addEventListener('click', hopJump);
7981
+ /* the game opens in place only when asked, and puts itself away the same way */
7982
+ $('hopLink').addEventListener('click', function () { hopOpen($('hop').hidden); });
8252
7983
  document.addEventListener('visibilitychange', function () { if (!document.hidden) hopStart(); });
8253
7984
  window.addEventListener('resize', function () { if (hop.target === 'wait') { hop.sky = null; hopStart(); } });
8254
7985
  })();