@bongos/core 1.20.61 → 1.20.63

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 (40) hide show
  1. package/.bongos-core.json +82 -37
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +5 -1
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0357-the-update-rule-set-on-deploy-is-the-one-the-sweep-follows.md +2 -0
  8. package/docs/api/openapi.json +96 -3
  9. package/docs/api-reference.md +4 -3
  10. package/docs/copy-inventory.md +372 -324
  11. package/docs/copy-registry.json +881 -417
  12. package/docs/module-api-changelog.md +4 -0
  13. package/docs/page-inventory.json +4 -1
  14. package/docs/page-readings.json +544 -481
  15. package/docs/recipes/upgrading-the-core.md +32 -1
  16. package/modules/provisioning/demo.js +176 -0
  17. package/modules/provisioning/migrations/provisioning_035_demo.sql +26 -0
  18. package/modules/provisioning/module.json +5 -3
  19. package/modules/provisioning/pollers/demo-archive.js +61 -0
  20. package/modules/provisioning/provisioning.js +2 -2
  21. package/modules/provisioning/routes/demo.js +54 -0
  22. package/modules/provisioning/routes/provisioning.js +7 -4
  23. package/modules/public-landing/public/projects-demo.states.json +53 -0
  24. package/modules/public-landing/public/projects.html +300 -23
  25. package/modules/public-landing/public/projects.probes.json +3 -3
  26. package/modules/public-landing/public/projects.states.json +2 -1
  27. package/modules/ui-design/kit/fixtures/provisioning-instances-demo.json +114 -0
  28. package/package-lock.json +2 -2
  29. package/package.json +1 -1
  30. package/release-notes.json +20 -0
  31. package/scripts/gds/provision-core-upgrade.js +16 -1
  32. package/scripts/gds/update-channel.js +11 -2
  33. package/scripts/gds/update-sweep.js +717 -0
  34. package/src/module-api.js +1 -1
  35. package/tests/core_upgrade_runner.mjs +13 -0
  36. package/tests/update_channel_db.mjs +13 -6
  37. package/tests/update_subscription_engine.mjs +5 -5
  38. package/tests/update_sweep_home.mjs +100 -0
  39. package/tests/wizard_demo.mjs +264 -0
  40. package/tests/wizard_front_door.mjs +16 -20
@@ -124,7 +124,31 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
124
124
  .doorPill{display:inline-grid;grid-template-columns:1fr 1fr;gap:6px;padding:7px;margin:6px 0 16px;border-radius:999px;border:1px solid var(--rule);background:color-mix(in srgb,var(--bg) 55%,transparent);}
125
125
  #view-new .doorPill .btn.ghost{border-color:transparent;}
126
126
  #view-new .doorPill .btn.ghost:hover{border-color:var(--rule);}
127
- .demoSoon{font-size:14px;line-height:1.5;color:var(--ink-soft);max-width:52ch;margin:0 0 16px;}
127
+ /* the demo route (task 1004420): two screens inside the fork. Steppers on the wizard's
128
+ field grammar, the fit check as a hairline band, the picked defaults as a ledger, and a
129
+ dashed ring on the planet for the time box (the canvas's "when it closes, the demo ends") */
130
+ .demoSteppers{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 24px;}
131
+ .demoStep{display:flex;align-items:center;gap:12px;margin-top:6px;}
132
+ .demoStep output{min-width:2ch;text-align:center;font-family:var(--font-display);font-weight:300;font-size:28px;color:var(--ink);}
133
+ .demoStep .btn{min-width:40px;min-height:40px;justify-content:center;}
134
+ .demoFit{border-top:1px solid var(--rule);padding:14px 0 6px;margin:0 0 10px;max-width:62ch;}
135
+ .demoFitLine{font-size:15.5px;line-height:1.5;color:var(--ink);margin:8px 0 12px;}
136
+ .demoFitTry{font-size:14px;color:var(--ink-soft);margin:0 0 4px;}
137
+ .demoTry{margin:0 0 6px;padding-left:18px;font-size:14.5px;line-height:1.6;color:var(--ink);}
138
+ .demoLedger{display:grid;grid-template-columns:150px minmax(0,1fr);gap:0;margin:6px 0 18px;border-top:1px solid var(--rule);}
139
+ .demoLedger dt,.demoLedger dd{margin:0;padding:11px 0;border-bottom:1px solid var(--rule-soft);}
140
+ .demoLedger dt{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);padding-top:14px;}
141
+ .demoLedger dd{font-size:15.5px;color:var(--ink);}
142
+ .demoLedger dd small{display:block;font-size:13.5px;color:var(--ink-soft);margin-top:2px;}
143
+ .demoInvites{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 24px;}
144
+ .demoRing{position:absolute;inset:4%;border:1px dashed color-mix(in srgb,var(--accent) 55%,transparent);border-radius:50%;}
145
+ .demoKeep{border-top:1px solid var(--rule);padding-top:14px;margin:6px 0 10px;}
146
+ .demoKeepH{font-family:var(--font-display);font-weight:300;font-size:24px;letter-spacing:-.02em;text-transform:lowercase;color:var(--ink);margin:0 0 10px;}
147
+ .demoKeepCells{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 24px;}
148
+ .demoClock{font-size:15.5px;color:var(--ink);margin:0 0 10px;}
149
+ .demoSoonWarn{color:var(--warn);font-weight:600;}
150
+ .demoStageCap{position:absolute;left:16px;right:16px;bottom:14px;margin:0;font-size:13px;line-height:1.4;color:var(--ink);}
151
+ @media (max-width:640px){ .demoSteppers,.demoInvites,.demoKeepCells{grid-template-columns:1fr;} .demoLedger{grid-template-columns:1fr;} .demoLedger dt{border-bottom:0;padding-bottom:0;} }
128
152
  /* the seed on the field plate: the world's own render, a hairline orbit and a
129
153
  dashed one, the chrome blob cut out on top — nothing drawn but hairlines */
130
154
  .forkBody{display:grid;grid-template-columns:minmax(0,1fr) minmax(240px,380px);gap:48px;align-items:center;}
@@ -912,6 +936,32 @@ summary{min-height:24px;padding:3px 0;}
912
936
  again. For a project whose app has no platform chosen, the OWNER is offered
913
937
  Connect Render (task 1004184): the setup wizard's own Render step, cloned into
914
938
  #mAppConnectForm on first open, so there is one flow and one set of words. -->
939
+ <!-- the demo (task 1004420, BV2.PS09; the canvas's DemoKeep): a project marked demo
940
+ shows its soft time box counting down, then asks "keep your planet?". Carry it
941
+ over and it is a real project from here on; keep it as a demo and it still goes
942
+ offline 30 days after it started (D9), restorable like any project taken
943
+ offline. The owner is told the date here from the start. Hidden for every
944
+ project that is not a demo. -->
945
+ <div class="card mSec" id="mDemo" hidden>
946
+ <h2>Demo planet</h2>
947
+ <p class="demoClock" id="mDemoClock"></p>
948
+ <div class="demoKeep" id="mDemoKeep" hidden>
949
+ <p class="demoKeepH">Keep your planet?</p>
950
+ <div class="demoKeepCells">
951
+ <div>
952
+ <button type="button" class="btn" data-demo-settle="carried">Carry it over</button>
953
+ <p class="finePrint">It becomes a real project: its settings, team and work stay as they are, and the demo’s time limit and 30-day rule stop applying.</p>
954
+ </div>
955
+ <div>
956
+ <button type="button" class="btn ghost" data-demo-settle="kept">Keep it as a demo</button>
957
+ <p class="finePrint">Leave it as it is: a finished demo you can come back to until it goes offline.</p>
958
+ </div>
959
+ </div>
960
+ </div>
961
+ <p class="finePrint" id="mDemoOffline"></p>
962
+ <div class="projMsg" id="mDemoMsg" role="status" aria-live="polite"></div>
963
+ </div>
964
+
915
965
  <div class="card mSec" id="mAppCard" hidden>
916
966
  <h2>Where its app runs</h2>
917
967
  <div id="mApp"></div>
@@ -1273,35 +1323,105 @@ summary{min-height:24px;padding:3px 0;}
1273
1323
  <!-- step 2 · the front door, part two: demo or create (task 1004417; the canvas's
1274
1324
  Fork.dc.html). Two equal ways in, so it is the world's door pill (DESIGN.md):
1275
1325
  two doors joined into one object, equal in size, the accent only on the one
1276
- that goes on. The seed stands on the field plate beside it. The demo is its
1277
- own task (BV2.PS09); until it ships, its door says so here instead of
1278
- leading anywhere. -->
1326
+ that goes on. The seed stands on the field plate beside it. The demo door opens
1327
+ the demo route's two screens in place (task 1004420, BV2.PS09). -->
1279
1328
  <section class="panel" id="panel-fork" aria-labelledby="fork-h">
1280
1329
  <div class="forkBody">
1281
1330
  <div>
1282
- <h1 id="fork-h">You are now creating a digital planet</h1>
1283
- <p class="lede">If that sounds like too much for now, try our demo system. Otherwise, proceed with planet initiation :)</p>
1284
- <form data-next="3">
1285
- <div class="doorPill" role="group" aria-label="How to begin">
1286
- <button type="button" class="btn big ghost" id="wizDemo" aria-controls="demoSoon" aria-expanded="false">Try the demo</button>
1287
- <button type="submit" class="btn big">Begin planet creation</button>
1331
+ <div id="forkIntro">
1332
+ <h1 id="fork-h">You are now creating a digital planet</h1>
1333
+ <p class="lede">If that sounds like too much for now, try our demo system. Otherwise, proceed with planet initiation :)</p>
1334
+ <form data-next="3">
1335
+ <div class="doorPill" role="group" aria-label="How to begin">
1336
+ <button type="button" class="btn big ghost" id="wizDemo" aria-controls="demoSetup" aria-expanded="false">Try the demo</button>
1337
+ <button type="submit" class="btn big">Begin planet creation</button>
1338
+ </div>
1339
+ <div class="nav">
1340
+ <button type="button" class="btn ghost" data-back="1">← Back</button>
1341
+ <span></span>
1342
+ </div>
1343
+ </form>
1344
+ </div>
1345
+ <!-- the demo route (task 1004420, BV2.PS09; the canvas's DemoSetup and DemoReady).
1346
+ Two screens inside the fork, then the ordinary wizard from the name step on:
1347
+ a demo is a normal project marked "demo" (D9). The fit check is a copy of
1348
+ modules/provisioning/demo.js fitCheck, held equal by tests/wizard_demo.mjs. -->
1349
+ <div id="demoSetup" class="demoView" hidden>
1350
+ <h1 id="demo-h1">Training wheels</h1>
1351
+ <p class="lede">A small, time-boxed project to learn the platform by doing. Everyone uses their own AI plan.</p>
1352
+ <div class="demoSteppers">
1353
+ <div class="wizField"><span id="demoPeopleLbl">People, you included</span>
1354
+ <div class="demoStep" role="group" aria-labelledby="demoPeopleLbl">
1355
+ <button type="button" class="btn small ghost" data-demo-step="people" data-by="-1" aria-label="One fewer person">−</button>
1356
+ <output id="demoPeople" aria-live="polite">3</output>
1357
+ <button type="button" class="btn small ghost" data-demo-step="people" data-by="1" aria-label="One more person">+</button>
1358
+ </div>
1359
+ </div>
1360
+ <div class="wizField"><span id="demoHoursLbl">Hours each</span>
1361
+ <div class="demoStep" role="group" aria-labelledby="demoHoursLbl">
1362
+ <button type="button" class="btn small ghost" data-demo-step="hours" data-by="-1" aria-label="One hour less">−</button>
1363
+ <output id="demoHours" aria-live="polite">4</output>
1364
+ <button type="button" class="btn small ghost" data-demo-step="hours" data-by="1" aria-label="One hour more">+</button>
1365
+ </div>
1366
+ </div>
1367
+ </div>
1368
+ <label class="wizField"><span>Your demo project idea</span>
1369
+ <textarea id="demoIdea" rows="3" maxlength="600" placeholder="A shared reading list for our team: everyone adds books, we vote, and the winner is next month’s read."></textarea>
1370
+ </label>
1371
+ <div class="demoFit" aria-live="polite">
1372
+ <p class="routeLbl">First read</p>
1373
+ <p class="demoFitLine"><span id="demoFitSum">3 people × 4 hours = 12 builder-hours.</span> <span id="demoFitVerdict">Enough for a small version. Trim it to one shared list, voting and a winner reveal; accounts and notifications will not fit.</span></p>
1374
+ <p class="demoFitTry">What you’ll get to try</p>
1375
+ <ul class="demoTry">
1376
+ <li>Claim a task and ship it</li>
1377
+ <li>See your work graded</li>
1378
+ <li>Put an idea to a vote in the Board Room</li>
1379
+ <li>Watch your rank grow with shipped work</li>
1380
+ </ul>
1288
1381
  </div>
1289
- <p class="demoSoon" id="demoSoon" hidden>The demo isn’t open yet. It’s being built now; until it opens, begin planet creation to make a real project.</p>
1290
1382
  <div class="nav">
1291
- <button type="button" class="btn ghost" data-back="1">← Back</button>
1292
- <span></span>
1383
+ <button type="button" class="btn ghost" id="demoBack1">← Back</button>
1384
+ <button type="button" class="btn" id="demoNext1">See the settings</button>
1293
1385
  </div>
1294
- </form>
1386
+ </div>
1387
+ <div id="demoReady" class="demoView" hidden>
1388
+ <h1 id="demo-h2">Your demo planet’s settings</h1>
1389
+ <p class="lede">We picked sensible settings so you can start straight away, and you can change any of them later. Next you name it and choose where its code lives, and then it is created.</p>
1390
+ <dl class="demoLedger">
1391
+ <dt>People</dt><dd><span id="demoLPeople">3</span><small id="demoLPeopleSub">You, plus the two people you invite below</small></dd>
1392
+ <dt>Time box</dt><dd><span id="demoLHours">4 hours each</span><small id="demoLTotal">12 builder-hours in total. When it runs out you are asked whether to keep your planet; your work is never stopped.</small></dd>
1393
+ <dt>Government</dt><dd>Monarchy / BDFL<small>One leader decides: you make the calls.</small></dd>
1394
+ <dt>Rewards</dt><dd>None<small>It’s a demo, for learning.</small></dd>
1395
+ <dt>Framing</dt><dd>All managed for you</dd>
1396
+ <dt>Who can see it</dt><dd>Only your team</dd>
1397
+ <dt>AI usage</dt><dd>Everyone uses their own AI plan</dd>
1398
+ </dl>
1399
+ <div class="demoInvites" id="demoInvites">
1400
+ <label class="wizField"><span>Invite by GitHub handle</span>
1401
+ <input type="text" id="demoInvite1" placeholder="teammate’s handle" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="39">
1402
+ </label>
1403
+ <label class="wizField"><span>And one more</span>
1404
+ <input type="text" id="demoInvite2" placeholder="teammate’s handle" autocomplete="off" autocapitalize="off" spellcheck="false" maxlength="39">
1405
+ </label>
1406
+ </div>
1407
+ <p class="finePrint">A demo nobody keeps goes offline 30 days after it starts. You are told before, and it can be brought back.</p>
1408
+ <div class="nav">
1409
+ <button type="button" class="btn ghost" id="demoBack2">← Back</button>
1410
+ <button type="button" class="btn" id="demoStart">Name my demo planet</button>
1411
+ </div>
1412
+ </div>
1295
1413
  </div>
1296
- <div class="forkStage" aria-hidden="true">
1414
+ <div class="forkStage" id="forkStage" aria-hidden="true">
1297
1415
  <span class="forkOrbit"></span>
1298
1416
  <span class="forkOrbit forkOrbitDash"></span>
1417
+ <span class="demoRing" id="demoRing" hidden></span>
1299
1418
  <img class="forkSeed" src="/assets/world/r2-chrome-blob-one-cut.webp" alt="">
1419
+ <p class="demoStageCap" id="demoStageCap" hidden>time box · <span id="demoStageHours">12</span> builder-hours · when the dashed ring closes, the demo asks whether to keep your planet</p>
1300
1420
  </div>
1301
1421
  </div>
1302
1422
  <div class="forkRoutes">
1303
1423
  <div>
1304
- <p class="routeLbl">Route 1 · demo · coming soon</p>
1424
+ <p class="routeLbl">Route 1 · demo</p>
1305
1425
  <p class="routeT">Training wheels</p>
1306
1426
  <p class="routeD">Pick how many people, how many hours each, and an idea. The platform checks that it fits, picks the settings, and you learn by building something small. Keep your planet at the end if you like it.</p>
1307
1427
  </div>
@@ -1685,6 +1805,7 @@ summary{min-height:24px;padding:3px 0;}
1685
1805
  <button type="button" class="btn small ghost" data-invite-skip="wizInvite">Skip</button>
1686
1806
  </div>
1687
1807
  <div class="inviteWhy" id="wizInviteWhy"></div>
1808
+ <p class="inviteWhy" id="wizDemoTeam" hidden></p>
1688
1809
  <ul class="suggList" id="wizInviteSugg"></ul>
1689
1810
  <ul class="inviteList" id="wizInviteList"></ul>
1690
1811
  <div class="projMsg" id="wizInviteMsg" role="status" aria-live="polite"></div>
@@ -4311,7 +4432,7 @@ summary{min-height:24px;padding:3px 0;}
4311
4432
  request omits the field), [] = the owner switched them all off, [...] =
4312
4433
  what they picked. Never seed it with the preselect — that would store an
4313
4434
  answer nobody gave. */
4314
- var state = { step: 0, name: '', projType: null, description: '', teamShape: null, modules: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false, appPlatform: 'later', look: null, termsAck: false };
4435
+ var state = { step: 0, name: '', projType: null, description: '', teamShape: null, modules: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false, appPlatform: 'later', demo: null, look: null, termsAck: false };
4315
4436
  try { Object.assign(state, JSON.parse(sessionStorage.getItem(DRAFT_KEY) || '{}')); } catch (e) {}
4316
4437
  var authKnown = false; /* flips true when the boot's /me settles, either way */
4317
4438
 
@@ -4608,18 +4729,109 @@ summary{min-height:24px;padding:3px 0;}
4608
4729
  }
4609
4730
 
4610
4731
  /* ── the front door (task 1004417) ── the tick mirrors into the draft; the
4611
- demo door opens a line saying the demo is not open yet (BV2.PS09 builds it) */
4732
+ demo door opens the demo route (task 1004420, below) */
4612
4733
  $('termsAck').checked = !!state.termsAck;
4613
4734
  $('termsAck').addEventListener('change', function () {
4614
4735
  state.termsAck = !!this.checked;
4615
4736
  if (state.termsAck) { $('h-terms').textContent = '\u00a0'; $('h-terms').className = 'hint'; }
4616
4737
  saveDraft();
4617
4738
  });
4618
- $('wizDemo').addEventListener('click', function () {
4619
- var soon = $('demoSoon');
4620
- soon.hidden = !soon.hidden;
4621
- this.setAttribute('aria-expanded', soon.hidden ? 'false' : 'true');
4739
+ /* ── the demo route (task 1004420, BV2.PS09) ──
4740
+ "Try the demo" opens two screens inside the fork: set up (people, hours each, the
4741
+ idea, and a first read of what fits) and the picked defaults with two invites.
4742
+ "Start the demo" goes on through the ordinary wizard from the name step: a demo is
4743
+ a normal project marked "demo" (D9), so the create carries `demo` and the server
4744
+ applies the demo's picked answers and makes it private to the team. The time box
4745
+ is soft (D3). The limits and the fit sentences are copies of
4746
+ modules/provisioning/demo.js, held equal by tests/wizard_demo.mjs. */
4747
+ var DEMO_LIMITS = { people: [1, 12], hours: [1, 40] };
4748
+ var demoDraft = state.demo
4749
+ ? { people: state.demo.people, hours: state.demo.hours_each, idea: state.demo.idea || '', invites: state.demo.invites || [] }
4750
+ : { people: 3, hours: 4, idea: '', invites: [] };
4751
+ function demoFit(people, hours) {
4752
+ var total = people * hours;
4753
+ var verdict = total < 8 ? 'tight' : total < 20 ? 'small' : 'plenty';
4754
+ var sentence = {
4755
+ tight: 'Too tight for this idea. Try one list with voting only, or add hours.',
4756
+ small: 'Enough for a small version. Trim it to one shared list, voting and a winner reveal; accounts and notifications will not fit.',
4757
+ plenty: 'Plenty for this idea, with room for one extra feature.'
4758
+ }[verdict];
4759
+ return { total: total, verdict: verdict, sentence: sentence };
4760
+ }
4761
+ function demoHandle(v) {
4762
+ var h = String(v || '').trim().replace(/^@/, '');
4763
+ return /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/.test(h) ? h : '';
4764
+ }
4765
+ function paintDemo() {
4766
+ var fit = demoFit(demoDraft.people, demoDraft.hours);
4767
+ $('demoPeople').textContent = String(demoDraft.people);
4768
+ $('demoHours').textContent = String(demoDraft.hours);
4769
+ $('demoFitSum').textContent = demoDraft.people + (demoDraft.people === 1 ? ' person' : ' people') + ' × ' + demoDraft.hours + (demoDraft.hours === 1 ? ' hour' : ' hours') + ' = ' + fit.total + ' builder-hours.';
4770
+ $('demoFitVerdict').textContent = fit.sentence;
4771
+ $('demoLPeople').textContent = String(demoDraft.people);
4772
+ var others = demoDraft.people - 1;
4773
+ $('demoLPeopleSub').textContent = others === 0 ? 'Just you' : others === 1 ? 'You, plus the person you invite below' : others === 2 ? 'You, plus the two people you invite below' : 'You, plus ' + others + ' people: invite two below, and the rest from your hall';
4774
+ $('demoLHours').textContent = demoDraft.hours + (demoDraft.hours === 1 ? ' hour each' : ' hours each');
4775
+ $('demoLTotal').textContent = fit.total + ' builder-hours in total. When it runs out you are asked whether to keep your planet; your work is never stopped.';
4776
+ $('demoStageHours').textContent = String(fit.total);
4777
+ $('demoInvites').hidden = others === 0;
4778
+ $('demoInvite2').closest('label').hidden = others < 2;
4779
+ }
4780
+ function demoShow(which) {
4781
+ $('forkIntro').hidden = which !== 'intro';
4782
+ $('demoSetup').hidden = which !== 'setup';
4783
+ $('demoReady').hidden = which !== 'ready';
4784
+ $('demoRing').hidden = which === 'intro';
4785
+ $('demoStageCap').hidden = which === 'intro';
4786
+ $('wizDemo').setAttribute('aria-expanded', which === 'intro' ? 'false' : 'true');
4787
+ if (which !== 'intro') {
4788
+ paintDemo();
4789
+ var h = $(which === 'setup' ? 'demo-h1' : 'demo-h2');
4790
+ h.setAttribute('tabindex', '-1'); h.focus({ preventScroll: false });
4791
+ }
4792
+ }
4793
+ $('demoIdea').value = demoDraft.idea;
4794
+ $('demoInvite1').value = demoDraft.invites[0] || '';
4795
+ $('demoInvite2').value = demoDraft.invites[1] || '';
4796
+ $('wizDemo').addEventListener('click', function () { demoShow('setup'); });
4797
+ root.querySelectorAll('[data-demo-step]').forEach(function (b) {
4798
+ b.addEventListener('click', function () {
4799
+ var key = b.getAttribute('data-demo-step');
4800
+ var lim = DEMO_LIMITS[key];
4801
+ var next = demoDraft[key] + Number(b.getAttribute('data-by'));
4802
+ demoDraft[key] = Math.min(lim[1], Math.max(lim[0], next));
4803
+ paintDemo();
4804
+ });
4805
+ });
4806
+ $('demoIdea').addEventListener('input', function () { demoDraft.idea = this.value; });
4807
+ $('demoNext1').addEventListener('click', function () { demoShow('ready'); });
4808
+ $('demoBack1').addEventListener('click', function () { demoShow('intro'); $('wizDemo').focus(); });
4809
+ $('demoBack2').addEventListener('click', function () { demoShow('setup'); });
4810
+ $('demoStart').addEventListener('click', function () {
4811
+ var invites = [demoHandle($('demoInvite1').value), demoHandle($('demoInvite2').value)]
4812
+ .filter(function (h, i, all) { return h && all.indexOf(h) === i; })
4813
+ .slice(0, Math.max(0, demoDraft.people - 1));
4814
+ state.demo = { people: demoDraft.people, hours_each: demoDraft.hours, idea: String(demoDraft.idea || '').trim(), invites: invites };
4815
+ /* the idea is the project's description unless one was already written, and the
4816
+ team is the people count — both the founder can still change on the way */
4817
+ if (state.demo.idea && !String(state.description || '').trim()) {
4818
+ state.description = state.demo.idea;
4819
+ $('f-desc').value = state.description;
4820
+ }
4821
+ state.teamShape = demoDraft.people > 1 ? 'small-team' : 'solo';
4822
+ saveDraft();
4823
+ show(3, true);
4622
4824
  });
4825
+ /* the done panel's invite step names the demo's teammates (task 1004420): the hub
4826
+ holds no invite of its own, so the founder sends each from the project's own box */
4827
+ function paintDemoTeam() {
4828
+ var line = $('wizDemoTeam');
4829
+ var who = state.demo && state.demo.invites && state.demo.invites.length ? state.demo.invites : [];
4830
+ line.hidden = !who.length;
4831
+ line.textContent = who.length ? 'Your demo teammates: ' + who.map(function (h) { return '@' + h; }).join(', ') + '. Invite each one here.' : '';
4832
+ if (who.length && !$('wizInviteWho').value) $('wizInviteWho').value = who[0];
4833
+ }
4834
+
4623
4835
 
4624
4836
  /* ── field wiring ── */
4625
4837
  $('newName').value = state.name;
@@ -5200,6 +5412,8 @@ summary{min-height:24px;padding:3px 0;}
5200
5412
  /* per-panel validation on Next */
5201
5413
  function validateStep(step) {
5202
5414
  if (step === 9) return validateRender();
5415
+ /* the fork's own door is "Begin planet creation": a real project, not the demo */
5416
+ if (step === 2) { state.demo = null; return true; }
5203
5417
  /* the front door's one tick (task 1004417): nothing past it until it is given */
5204
5418
  if (step === 1) {
5205
5419
  if (!state.termsAck) {
@@ -5434,6 +5648,11 @@ summary{min-height:24px;padding:3px 0;}
5434
5648
  ['App', appRow()],
5435
5649
  ['Look', lookRow()]
5436
5650
  ];
5651
+ /* the demo route (task 1004420): the review says it is a demo, and what that means */
5652
+ if (state.demo) {
5653
+ rows.unshift(['Demo', esc(state.demo.people + (state.demo.people === 1 ? ' person' : ' people') + ', ' + state.demo.hours_each + ' hours each.') +
5654
+ ' Private to your team, no rewards, all managed. When the time box runs out you are asked whether to keep your planet.']);
5655
+ }
5437
5656
  $('reviewList').innerHTML = rows.map(function (r) {
5438
5657
  return '<li><span class="k">' + r[0] + '</span><span class="v">' + r[1] + '</span></li>';
5439
5658
  }).join('');
@@ -5535,6 +5754,9 @@ summary{min-height:24px;padding:3px 0;}
5535
5754
  modules: modulesForCreate(),
5536
5755
  /* the front door's tick (task 1004417) — the server stamps WHICH terms */
5537
5756
  terms_acknowledged: state.termsAck ? true : undefined,
5757
+ /* the demo route (task 1004420): a normal project marked demo — the server
5758
+ applies the demo's picked answers and keeps it private to the team */
5759
+ demo: state.demo ? { people: state.demo.people, hours_each: state.demo.hours_each } : undefined,
5538
5760
  hosting_shape: 'cloud-host',
5539
5761
  /* ONLY a bring-your-own domain travels (task 1002898 N2). The server owns
5540
5762
  the assigned <slug>.<apex> default (task 1002704) and applies it to an
@@ -5828,6 +6050,7 @@ summary{min-height:24px;padding:3px 0;}
5828
6050
  sets only AFTER this runs. */
5829
6051
  var created = createdRecord();
5830
6052
  setInviteSlug('wizInvite', (me && created && created.id && created.slug) ? created.slug : '');
6053
+ paintDemoTeam();
5831
6054
  loadInviteSuggestions('wizInvite', (me && created && created.slug) ? created.id : '');
5832
6055
  $('wizInviteLater').innerHTML = 'You can invite builders any time from ' + projectPageLink('the project’s own page') + '.';
5833
6056
  renderSteps();
@@ -6196,7 +6419,7 @@ summary{min-height:24px;padding:3px 0;}
6196
6419
  is three-valued and null is its "nothing said" sentinel; leaving the key out
6197
6420
  left it UNDEFINED, which is not null — so pickedModules() reached .slice()
6198
6421
  on it and the second project's Modules step threw. */
6199
- state = { step: 1, name: '', projType: null, description: '', teamShape: null, modules: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false, appPlatform: 'later', look: null, termsAck: false };
6422
+ state = { step: 1, name: '', projType: null, description: '', teamShape: null, modules: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false, appPlatform: 'later', demo: null, look: null, termsAck: false };
6200
6423
  $('newName').value = ''; $('f-owner').value = ''; $('f-repo').value = ''; $('f-slug').value = ''; $('f-domain').value = '';
6201
6424
  $('f-noaddr').checked = false; $('f-desc').value = '';
6202
6425
  /* Re-seed the owner from the signed-in account for the next project. */
@@ -7077,6 +7300,59 @@ summary{min-height:24px;padding:3px 0;}
7077
7300
  $('mPlanet').innerHTML = ledgerRow(['planet', planetWords(pl) + (pl.chosen ? '<small>your choice.</small>' : pl.curated ? '' : '<small>chosen for it by the platform.</small>')]);
7078
7301
  $('mPlanet').hidden = false;
7079
7302
  }
7303
+ /* ── the demo card (task 1004420, BV2.PS09) ──
7304
+ Fetch-free like the rest of renderManage, so the once-a-minute tick keeps the
7305
+ countdown current: everything it draws is the row's own `demo` view, which the server
7306
+ shapes (remaining_seconds, time_up, offline_on). The time box is SOFT (D3): the clock
7307
+ only ever counts down and then asks; nothing here stops the work. */
7308
+ function demoLeft(seconds) {
7309
+ var m = Math.max(0, Math.round(seconds / 60));
7310
+ var h = Math.floor(m / 60);
7311
+ return h ? h + ' h ' + (m % 60) + ' min' : m + ' min';
7312
+ }
7313
+ function renderDemoCard(inst) {
7314
+ var d = inst && inst.demo;
7315
+ $('mDemo').hidden = !d;
7316
+ if (!d) return;
7317
+ var hours = d.hours_each + (d.hours_each === 1 ? ' hour' : ' hours');
7318
+ $('mDemoClock').textContent = d.time_up
7319
+ ? 'The time box has run out: ' + d.people + ' × ' + hours + '. Your work keeps going either way.'
7320
+ : demoLeft(d.remaining_seconds) + ' left in the time box (' + d.people + ' × ' + hours + ' = ' + d.builder_hours + ' builder-hours). Nothing stops when it runs out.';
7321
+ $('mDemoKeep').hidden = !(d.time_up && !d.outcome);
7322
+ var date = listedDate(d.offline_on);
7323
+ $('mDemoOffline').textContent = (d.outcome === 'kept' ? 'Kept as a demo. ' : '') +
7324
+ (d.offline_soon ? 'This demo goes offline on ' + date + ' unless you carry it over. ' : 'A demo nobody carries over goes offline on ' + date + '. ') +
7325
+ 'It can be brought back afterwards.';
7326
+ $('mDemoOffline').classList.toggle('demoSoonWarn', !!d.offline_soon);
7327
+ }
7328
+ /* guarded: some tests run regions of this script in a vm with no document */
7329
+ if (typeof document !== 'undefined' && document.querySelectorAll) document.querySelectorAll('[data-demo-settle]').forEach(function (b) {
7330
+ b.addEventListener('click', function () {
7331
+ var inst = manageInst;
7332
+ if (!inst || !inst.demo) return;
7333
+ var outcome = b.getAttribute('data-demo-settle');
7334
+ document.querySelectorAll('[data-demo-settle]').forEach(function (x) { x.disabled = true; });
7335
+ $('mDemoMsg').textContent = '';
7336
+ fetch(API + '/provisioning/instances/' + encodeURIComponent(inst.id) + '/demo', {
7337
+ method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
7338
+ body: JSON.stringify({ outcome: outcome })
7339
+ }).then(function (r) { return r.json().catch(function () { return {}; }).then(function (data) { return { ok: r.ok, data: data }; }); })
7340
+ .then(function (res) {
7341
+ document.querySelectorAll('[data-demo-settle]').forEach(function (x) { x.disabled = false; });
7342
+ if (!res.ok) { $('mDemoMsg').textContent = (res.data && res.data.error && res.data.error.message) || 'That didn’t go through. Try again.'; return; }
7343
+ inst.demo = res.data.demo;
7344
+ renderDemoCard(inst);
7345
+ $('mDemoMsg').textContent = outcome === 'carried'
7346
+ ? 'It’s a real project now. Its settings, team and work carry on as they are.'
7347
+ : 'Kept as a demo.';
7348
+ })
7349
+ .catch(function () {
7350
+ document.querySelectorAll('[data-demo-settle]').forEach(function (x) { x.disabled = false; });
7351
+ $('mDemoMsg').textContent = 'That didn’t go through. Try again.';
7352
+ });
7353
+ });
7354
+ });
7355
+
7080
7356
  function renderManage(inst) {
7081
7357
  manageInst = inst;
7082
7358
  $('mLoading').hidden = true;
@@ -7123,6 +7399,7 @@ summary{min-height:24px;padding:3px 0;}
7123
7399
  renderPick(inst);
7124
7400
  renderCardPick(inst);
7125
7401
  renderLookCard(inst);
7402
+ renderDemoCard(inst);
7126
7403
  /* invite a builder (task 1002346): fetch-free too — the widget needs only the
7127
7404
  slug, and the API is the wall. This read is own-scoped and 404s for anyone
7128
7405
  who is not the owner, so reaching renderManage at all IS the ownership
@@ -161,9 +161,9 @@
161
161
  { "name": "the wizard driver (task 1003346): name, type, about, home (fresh; the stub grants repo access, so the new repository is named from the project), address (the handle's verdict), the review as a ledger, then the filing — the done panel with the standup rail and the human steps", "auth": true, "width": 1440, "url": "/projects?view=new",
162
162
  "steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400],["click","#typeCards .tpill[data-type=\"game\"]"],["click","#panel-type button[type=submit]"],["wait",400],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#teamCards .tpill[data-team=\"small-team\"]"],["click","#panel-detail button[type=submit]"],["wait",900],["click","#panel-modules button[type=submit]"],["wait",400],["click","#panel-home button[type=submit]"],["wait",400],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#panel-address button[type=submit]"],["wait",400],["click","#panel-app button[type=submit]"],["wait",400],["click","#panel-look button[type=submit]"],["wait",400],["click","#doCreate"],["wait",1500]],
163
163
  "expect": {"visible":["#panel-done","#liveProgress .prog","#liveProgress .prog .stages li.now","#humanSteps li"],"hidden":["#panel-review","#panel-name"],"text":[["#done-h","The drums are rolling"]],"attr":[["#panel-done","class","panel on"]]} },
164
- { "name": "the front door (task 1004417): Proceed is refused until the terms are ticked, the fork's demo door says the demo is not open yet, and Begin planet creation goes on to the name step with the rail back", "auth": true, "width": 1440, "url": "/projects?view=new",
165
- "steps": [["wait",600],["click","#panel-terms button[type=submit]"],["wait",300],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",200],["click","#panel-fork button[type=submit]"],["wait",400]],
166
- "expect": {"visible":["#panel-name","#view-new .steps","#view-new .steps li.now"],"hidden":["#panel-fork","#panel-terms"],"attr":[["#wizDemo","aria-expanded","true"],["#view-new .steps li.now","data-step","3"]]} },
164
+ { "name": "the front door (task 1004417): Proceed is refused until the terms are ticked, the fork's demo door opens the demo's setup and Back returns to the doors, and Begin planet creation goes on to the name step with the rail back", "auth": true, "width": 1440, "url": "/projects?view=new",
165
+ "steps": [["wait",600],["click","#panel-terms button[type=submit]"],["wait",300],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",200],["click","#demoBack1"],["wait",200],["click","#panel-fork button[type=submit]"],["wait",400]],
166
+ "expect": {"visible":["#panel-name","#view-new .steps","#view-new .steps li.now"],"hidden":["#panel-fork","#panel-terms"],"attr":[["#wizDemo","aria-expanded","false"],["#view-new .steps li.now","data-step","3"]]} },
167
167
  { "name": "the front door at 390: the fork's two doors stack inside one pill and the walk still reaches the name step", "auth": true, "width": 390, "url": "/projects?view=new",
168
168
  "steps": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400]],
169
169
  "expect": {"visible":["#panel-fork",".doorPill","#wizDemo","#panel-fork button[type=submit]"],"hidden":["#view-new .steps"]} },
@@ -39,7 +39,8 @@
39
39
  "in-new": { "auth": true, "url": "/projects?view=new", "expect": { "visible": ["#panel-terms", "#termsAck"], "hidden": ["#qWrap", "#view-new .steps", "#panel-name"] } },
40
40
  "in-new-terms-refused": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#panel-terms button[type=submit]"],["wait",300]], "expect": {"visible":["#panel-terms","#h-terms"],"hidden":["#panel-fork"],"text":[["#h-terms","Tick the box to go on. It’s how your project records that you acknowledged the terms."]]} },
41
41
  "in-new-fork": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-fork",".doorPill","#wizDemo",".forkStage",".forkRoutes"],"hidden":["#panel-terms","#view-new .steps","#demoSoon",".bongoCell"]} },
42
- "in-new-fork-demo": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",200]], "expect": {"visible":["#panel-fork","#demoSoon"],"attr":[["#wizDemo","aria-expanded","true"]]} },
42
+ "in-new-fork-demo": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",300]], "expect": {"visible":["#panel-fork","#demoSetup","#demoFitVerdict","#demoRing"],"hidden":["#forkIntro","#demoReady"],"attr":[["#wizDemo","aria-expanded","true"]]} },
43
+ "in-new-demo-ready": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#wizDemo"],["wait",300],["click","[data-demo-step=\"hours\"][data-by=\"1\"]"],["click","#demoNext1"],["wait",300]], "expect": {"visible":["#demoReady",".demoLedger","#demoInvite1","#demoInvite2"],"hidden":["#demoSetup","#forkIntro"],"text":[["#demoLHours","5 hours each"]]} },
43
44
  "in-new-name": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-name","#newName","#view-new .steps"],"hidden":["#panel-fork","#panel-terms"]} },
44
45
  "out-new": { "auth": false, "url": "/projects?view=new", "expect": { "hidden": ["#qWrap"] } },
45
46
  "in-new-type": { "auth": true, "url": "/projects?view=new", "actions": [["wait",600],["click","#termsAck"],["click","#panel-terms button[type=submit]"],["wait",400],["click","#panel-fork button[type=submit]"],["wait",400],["fill","#newName","Mercury"],["click","#panel-name button[type=submit]"],["wait",400]], "expect": {"visible":["#panel-type"],"hidden":["#panel-name","#qWrap"]} },
@@ -0,0 +1,114 @@
1
+ {
2
+ "_": "The demo route (task 1004420): provisioning-instances.json with project 101 marked as a demo whose time box has run out, so its page asks keep-your-planet. Only projects-demo.states.json points at this.",
3
+ "instances": [
4
+ {
5
+ "id": 101,
6
+ "slug": "demo",
7
+ "status": "active",
8
+ "domain": "demo.cloudbongos.com",
9
+ "target_ref": "builder/demo",
10
+ "hosting_shape": "co-tenant",
11
+ "onboard_mode": "scaffold",
12
+ "created_at": "2026-08-01T10:00:00.000Z",
13
+ "provisioned_at": "2026-08-01T10:20:00.000Z",
14
+ "updated_at": "$now-3m",
15
+ "last_verified_at": "$now-3m",
16
+ "last_verified_up": true,
17
+ "error_note": null,
18
+ "dns_note": null,
19
+ "torn_down_at": null,
20
+ "open_intent": null,
21
+ "demo": {
22
+ "people": 3,
23
+ "hours_each": 4,
24
+ "builder_hours": 12,
25
+ "started_at": "2026-09-28T09:00:00Z",
26
+ "ends_at": "2026-09-28T13:00:00Z",
27
+ "remaining_seconds": 0,
28
+ "time_up": true,
29
+ "outcome": null,
30
+ "offline_on": "2026-10-28T09:00:00.000Z",
31
+ "offline_soon": false
32
+ }
33
+ },
34
+ {
35
+ "id": 102,
36
+ "slug": "harbor",
37
+ "status": "provisioning",
38
+ "planet_template": "blob-frost-tint",
39
+ "planet_accent": "sun-lit",
40
+ "domain": null,
41
+ "target_ref": "builder/harbor",
42
+ "hosting_shape": "cloud-host",
43
+ "owner_monthly_usd": null,
44
+ "onboard_mode": "scaffold",
45
+ "created_at": "$now-6m",
46
+ "provisioned_at": null,
47
+ "updated_at": "$now-2m",
48
+ "last_verified_at": null,
49
+ "last_verified_up": null,
50
+ "error_note": null,
51
+ "dns_note": null,
52
+ "torn_down_at": null,
53
+ "open_intent": {
54
+ "action": "standup",
55
+ "state": "running",
56
+ "created_at": "$now-6m"
57
+ }
58
+ },
59
+ {
60
+ "id": 103,
61
+ "slug": "tidepool",
62
+ "status": "error",
63
+ "domain": "tidepool.cloudbongos.com",
64
+ "target_ref": "builder/tidepool",
65
+ "hosting_shape": "co-tenant",
66
+ "onboard_mode": "adopt",
67
+ "created_at": "2026-08-20T09:00:00.000Z",
68
+ "provisioned_at": null,
69
+ "updated_at": "$now-1d",
70
+ "last_verified_at": null,
71
+ "last_verified_up": null,
72
+ "error_note": "The deploy could not read the repository: it is private, and the deploy key only reads public ones.",
73
+ "dns_note": null,
74
+ "torn_down_at": null,
75
+ "open_intent": null
76
+ },
77
+ {
78
+ "id": 104,
79
+ "slug": "old-lantern",
80
+ "status": "torn_down",
81
+ "domain": "old-lantern.cloudbongos.com",
82
+ "target_ref": "builder/old-lantern",
83
+ "hosting_shape": "co-tenant",
84
+ "onboard_mode": "scaffold",
85
+ "created_at": "2026-07-02T09:00:00.000Z",
86
+ "provisioned_at": "2026-07-02T09:30:00.000Z",
87
+ "updated_at": "2026-08-15T12:00:00.000Z",
88
+ "last_verified_at": "2026-08-15T11:50:00.000Z",
89
+ "last_verified_up": true,
90
+ "error_note": null,
91
+ "dns_note": null,
92
+ "torn_down_at": "2026-08-15T12:00:00.000Z",
93
+ "open_intent": null
94
+ },
95
+ {
96
+ "id": 105,
97
+ "slug": "mooring",
98
+ "status": "active",
99
+ "domain": "mooring.cloudbongos.com",
100
+ "target_ref": "builder/mooring",
101
+ "hosting_shape": "co-tenant",
102
+ "onboard_mode": "scaffold",
103
+ "created_at": "2026-08-10T09:00:00.000Z",
104
+ "provisioned_at": "2026-08-10T09:20:00.000Z",
105
+ "updated_at": "$now-20m",
106
+ "last_verified_at": "$now-20m",
107
+ "last_verified_up": false,
108
+ "error_note": null,
109
+ "dns_note": null,
110
+ "torn_down_at": null,
111
+ "open_intent": null
112
+ }
113
+ ]
114
+ }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.20.61",
3
+ "version": "1.20.63",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.20.61",
9
+ "version": "1.20.63",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.20.61",
3
+ "version": "1.20.63",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",