@bongos/core 1.17.2 → 1.18.0

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 (151) hide show
  1. package/.bongos-core.json +426 -96
  2. package/.devcontainer/healthcheck.sh +0 -0
  3. package/.devcontainer/init-firewall.sh +0 -0
  4. package/.devcontainer/post-create.sh +0 -0
  5. package/GOVERNANCE.md +1 -1
  6. package/README.md +26 -22
  7. package/bin/bongos.js +0 -0
  8. package/clients/bongos-client/README.md +1 -1
  9. package/clients/bongos-client/bongos-client.global.js +2 -0
  10. package/clients/bongos-client/index.cjs +2 -0
  11. package/clients/bongos-client/index.d.ts +4 -2
  12. package/clients/bongos-client/index.mjs +2 -0
  13. package/docker-entrypoint.sh +0 -0
  14. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +94 -0
  15. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +43 -0
  16. package/docs/adr/README.md +3 -0
  17. package/docs/api/openapi.json +68 -370
  18. package/docs/api-reference.md +7 -18
  19. package/docs/file-map.md +7 -25
  20. package/docs/onboarding/diagrams/05-versioning.mmd +4 -6
  21. package/docs/onboarding/diagrams/assertions.json +5 -9
  22. package/modules/hall-ui/public/builders.js +132 -645
  23. package/modules/hall-ui/public/goal-inbox.js +67 -26
  24. package/modules/hall-ui/public/goals-page.js +102 -177
  25. package/modules/hall-ui/public/goals.css +183 -0
  26. package/modules/hall-ui/public/goals.html +35 -253
  27. package/modules/hall-ui/public/goals.js +7 -2
  28. package/modules/hall-ui/public/hall-kit.js +474 -0
  29. package/modules/hall-ui/public/hall-widgets.js +24 -20
  30. package/modules/hall-ui/public/instances-panel.js +24 -3
  31. package/modules/hall-ui/public/new-project-wizard.js +6 -5
  32. package/modules/hall-ui/public/settings.css +355 -0
  33. package/modules/hall-ui/public/settings.html +86 -414
  34. package/modules/hall-ui/public/settings.js +96 -35
  35. package/modules/hall-ui/public/style.css +156 -0
  36. package/modules/hall-ui/public/watch.css +184 -0
  37. package/modules/hall-ui/public/watch.html +111 -285
  38. package/modules/hall-ui/public/watch.js +59 -21
  39. package/modules/hall-ui/public/work.css +194 -0
  40. package/modules/hall-ui/public/work.html +46 -374
  41. package/modules/hall-ui/public/work.js +137 -102
  42. package/modules/lifecycle/criterion-suggest.js +80 -0
  43. package/modules/lifecycle/db.js +42 -5
  44. package/modules/lifecycle/routes/goals.js +26 -0
  45. package/modules/lifecycle/routes/tasks.js +21 -0
  46. package/package-lock.json +2 -2
  47. package/package.json +1 -1
  48. package/public-landing/create.html +83 -7
  49. package/public-landing/index.html +11 -76
  50. package/scripts/cron/style_guide_audit.sh +0 -0
  51. package/scripts/gds/api.js +0 -0
  52. package/scripts/gds/architect-audit.js +0 -0
  53. package/scripts/gds/audit-authorship.sh +0 -0
  54. package/scripts/gds/audit-replay.sh +0 -0
  55. package/scripts/gds/box.js +0 -0
  56. package/scripts/gds/capture.js +0 -0
  57. package/scripts/gds/conductor-dispatch.js +0 -0
  58. package/scripts/gds/coreB-carve-proof.sh +0 -0
  59. package/scripts/gds/db-backup-nightly.sh +0 -0
  60. package/scripts/gds/db-backup-offsite.sh +0 -0
  61. package/scripts/gds/doctor.js +0 -0
  62. package/scripts/gds/grade-smoke.js +0 -0
  63. package/scripts/gds/hooks/pre-push.js +0 -0
  64. package/scripts/gds/install-git-hooks.js +0 -0
  65. package/scripts/gds/main-audit.js +0 -0
  66. package/scripts/gds/oauth-secret.js +0 -0
  67. package/scripts/gds/onboarding-analyze.js +0 -0
  68. package/scripts/gds/onboarding-ship.js +0 -0
  69. package/scripts/gds/paste-token.js +0 -0
  70. package/scripts/gds/portability-smoke-art.sh +0 -0
  71. package/scripts/gds/portability-smoke-memory.sh +0 -0
  72. package/scripts/gds/provision.js +125 -4
  73. package/scripts/gds/push-main.js +0 -0
  74. package/scripts/gds/render-ideas.js +0 -0
  75. package/scripts/gds/security-baseline-check.sh +0 -0
  76. package/scripts/gds/session.js +0 -0
  77. package/scripts/gds/ship.js +11 -1
  78. package/scripts/gds/smoke-batch-claim.sh +0 -0
  79. package/scripts/gds/smoke-batch-release.sh +0 -0
  80. package/scripts/gds/smoke-box.sh +0 -0
  81. package/scripts/gds/smoke-claim-consistency.sh +0 -0
  82. package/scripts/gds/smoke-dependencies.sh +0 -0
  83. package/scripts/gds/smoke-write.sh +0 -0
  84. package/scripts/hall-preview/README.md +103 -0
  85. package/scripts/hall-preview/fixtures/access-requests.json +20 -0
  86. package/scripts/hall-preview/fixtures/achievements.json +76 -0
  87. package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
  88. package/scripts/hall-preview/fixtures/audit-log.json +68 -0
  89. package/scripts/hall-preview/fixtures/blockers__detail.json +12 -0
  90. package/scripts/hall-preview/fixtures/box__me.json +5 -0
  91. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +69 -0
  92. package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
  93. package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
  94. package/scripts/hall-preview/fixtures/builders__roster.json +48 -0
  95. package/scripts/hall-preview/fixtures/goals.json +502 -0
  96. package/scripts/hall-preview/fixtures/goals__25.json +96 -0
  97. package/scripts/hall-preview/fixtures/goals__26.json +67 -0
  98. package/scripts/hall-preview/fixtures/goals__3.json +103 -0
  99. package/scripts/hall-preview/fixtures/goals__35.json +49 -0
  100. package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
  101. package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
  102. package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
  103. package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
  104. package/scripts/hall-preview/fixtures/grades__by-builder.json +32 -0
  105. package/scripts/hall-preview/fixtures/inbox.json +330 -0
  106. package/scripts/hall-preview/fixtures/inbox__detail.json +15 -0
  107. package/scripts/hall-preview/fixtures/instance.json +124 -0
  108. package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
  109. package/scripts/hall-preview/fixtures/me.json +476 -0
  110. package/scripts/hall-preview/fixtures/me__art-key__own.json +6 -0
  111. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +209 -0
  112. package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
  113. package/scripts/hall-preview/fixtures/me__render.json +5 -0
  114. package/scripts/hall-preview/fixtures/me__skill-prefs.json +143 -0
  115. package/scripts/hall-preview/fixtures/me__sound-prefs.json +67 -0
  116. package/scripts/hall-preview/fixtures/me__wandering.json +43 -0
  117. package/scripts/hall-preview/fixtures/override-requests.json +15 -0
  118. package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
  119. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
  120. package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
  121. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
  122. package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
  123. package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
  124. package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
  125. package/scripts/hall-preview/fixtures/public__grades.json +50 -0
  126. package/scripts/hall-preview/fixtures/public__progress.json +124 -0
  127. package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
  128. package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
  129. package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
  130. package/scripts/hall-preview/fixtures/security__docs.json +10 -0
  131. package/scripts/hall-preview/fixtures/security__reports.json +31 -0
  132. package/scripts/hall-preview/fixtures/sessions__search.json +9 -0
  133. package/scripts/hall-preview/fixtures/tasks.json +2420 -0
  134. package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
  135. package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
  136. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
  137. package/scripts/hall-preview/fixtures/versions.json +89 -0
  138. package/scripts/hall-preview/refresh-fixtures.js +77 -0
  139. package/scripts/hall-preview/server.js +195 -0
  140. package/scripts/migrate.sh +29 -4
  141. package/scripts/migrate_cost_ledger.sh +0 -0
  142. package/scripts/render-diagrams.sh +0 -0
  143. package/scripts/security/dep-audit.js +0 -0
  144. package/scripts/setup-builder.js +0 -0
  145. package/scripts/setup-builder.sh +0 -0
  146. package/scripts/snapshots/maybe-render-on-deploy.sh +0 -0
  147. package/src/bongos/optimizer.js +37 -2
  148. package/src/module-api.js +20 -1
  149. package/tests/criterion_suggest.mjs +106 -0
  150. package/tests/migrate_root_resolution.mjs +97 -0
  151. package/tests/provision.mjs +69 -0
@@ -182,6 +182,25 @@ input[type=text].bad{border-color:var(--err);}
182
182
  }
183
183
  .reassure.on{display:block;}
184
184
 
185
+ /* one-click deep link to GitHub's own "New repository" page with the name
186
+ pre-filled (greenfield only). No permissions — the visitor creates the empty
187
+ repo in GitHub's own UI; we just fill in the blanks. */
188
+ .ghbtn{
189
+ display:inline-flex;align-items:center;gap:9px;text-decoration:none;cursor:pointer;
190
+ background:#24292f;color:#fff;border:none;border-radius:999px;
191
+ font-family:var(--font-body);font-weight:700;font-size:15px;padding:12px 22px;
192
+ box-shadow:0 12px 26px -12px rgba(36,41,47,0.55);
193
+ transition:transform .12s ease, background .12s ease;
194
+ }
195
+ .ghbtn:hover{background:#1b1f24;transform:translateY(-1px);}
196
+ .ghbtn:active{transform:translateY(0) scale(.98);}
197
+ .ghbtn .gh{width:18px;height:18px;fill:currentColor;}
198
+ .ghcreate{display:none;margin-bottom:18px;}
199
+ .ghcreate.on{display:block;}
200
+ .ghcreate-note{font-size:13px;font-weight:500;color:var(--ink-faint);line-height:1.5;margin-top:9px;max-width:52ch;}
201
+ .ghcreate-note strong{color:var(--ink-soft);}
202
+ .steps .ghbtn{font-size:14px;padding:9px 16px;margin-top:10px;}
203
+
185
204
  /* buttons */
186
205
  .nav{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:26px;}
187
206
  .btn{
@@ -394,6 +413,13 @@ input[type=text].bad{border-color:var(--err);}
394
413
  <input type="text" id="f-repo" placeholder="mercury" autocomplete="off" maxlength="120">
395
414
  </label>
396
415
  </div>
416
+ <div class="ghcreate" id="ghCreate">
417
+ <a class="ghbtn" id="ghCreateBtn" href="https://github.com/new" target="_blank" rel="noopener">
418
+ <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>
419
+ Create this repo on GitHub
420
+ </a>
421
+ <p class="ghcreate-note">Opens GitHub with the name already filled in — just click <strong>Create repository</strong> and leave it empty, then come back and press Next. Already made it? Just press Next.</p>
422
+ </div>
397
423
  <div class="hint" id="h-home">&nbsp;</div>
398
424
  <div class="nav">
399
425
  <button type="button" class="btn ghost" data-back="1">← Back</button>
@@ -470,7 +496,7 @@ input[type=text].bad{border-color:var(--err);}
470
496
  /* ── state ── */
471
497
  var DRAFT_KEY = 'cb-create-draft';
472
498
  var CREATED_KEY = 'cb-created-instance';
473
- var state = { step: 0, name: '', mode: 'greenfield', owner: '', repo: '', slug: '', slugTouched: false, domain: '' };
499
+ var state = { step: 0, name: '', mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '' };
474
500
  try { Object.assign(state, JSON.parse(sessionStorage.getItem(DRAFT_KEY) || '{}')); } catch (e) {}
475
501
  var me = null;
476
502
 
@@ -483,6 +509,7 @@ input[type=text].bad{border-color:var(--err);}
483
509
  if (handedName && handedName.trim()) {
484
510
  state.name = handedName.trim().slice(0, 80);
485
511
  state.slug = deriveSlug(state.name); state.slugTouched = false; // (hoisted declaration)
512
+ state.repo = deriveRepo(state.name); state.repoTouched = false; // auto-repo from the name
486
513
  state.step = state.step >= 1 && state.step <= 4 ? state.step : 1;
487
514
  try { sessionStorage.setItem(DRAFT_KEY, JSON.stringify(state)); } catch (e) {}
488
515
  history.replaceState(null, '', location.pathname);
@@ -507,14 +534,19 @@ input[type=text].bad{border-color:var(--err);}
507
534
  var v = String(s || '');
508
535
  return v.length >= 2 && v.length <= 40 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(v) && v.indexOf('--') === -1;
509
536
  }
510
- function deriveSlug(name) {
537
+ function slugify(name, maxLen) {
511
538
  return String(name || '').toLowerCase()
512
539
  .replace(/['’]/g, '')
513
540
  .replace(/[^a-z0-9]+/g, '-')
514
541
  .replace(/-+/g, '-')
515
542
  .replace(/^-|-$/g, '')
516
- .slice(0, 40);
543
+ .slice(0, maxLen);
517
544
  }
545
+ function deriveSlug(name) { return slugify(name, 40); }
546
+ /* Repo-name default from the project name — the same slug-safe transform (GitHub
547
+ repo names accept [A-Za-z0-9._-], so a lowercase-dashed name is always valid and
548
+ matches the placeholder). Wider cap than the handle; the visitor can still edit. */
549
+ function deriveRepo(name) { return slugify(name, 100); }
518
550
  function looksLikeDomain(d) {
519
551
  return /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/i.test(String(d || ''));
520
552
  }
@@ -524,6 +556,20 @@ input[type=text].bad{border-color:var(--err);}
524
556
  var origin = 'https://' + domain;
525
557
  return { homepageUrl: origin, callbackUrl: origin + '/api/gds/auth/web/callback' };
526
558
  }
559
+ /* GitHub's own "New repository" page, pre-filled with the owner + name. No auth or
560
+ permissions on our side — the visitor creates the empty repo in GitHub's UI, which
561
+ is exactly what the scaffold then clones. `owner` selects the account/org; omitting
562
+ a blank param just leaves that field empty on GitHub. */
563
+ function githubNewRepoUrl(owner, repo) {
564
+ var u = new URL('https://github.com/new');
565
+ if (repo) u.searchParams.set('name', repo);
566
+ if (owner) u.searchParams.set('owner', owner);
567
+ return u.toString();
568
+ }
569
+ function updateGhCreateHref() {
570
+ var a = $('ghCreateBtn');
571
+ if (a) a.href = githubNewRepoUrl(state.owner, state.repo);
572
+ }
527
573
  function esc(s) {
528
574
  return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
529
575
  return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
@@ -559,12 +605,25 @@ input[type=text].bad{border-color:var(--err);}
559
605
  $('f-slug').value = state.slug;
560
606
  $('f-domain').value = state.domain;
561
607
 
608
+ /* Auto-fill the repo name from the project name until the visitor edits it
609
+ directly (mirrors the handle's auto-derive). The owner is filled once we know
610
+ who's signed in — see the /me boot below. */
611
+ if (!state.repoTouched && state.name && !state.repo) {
612
+ state.repo = deriveRepo(state.name);
613
+ $('f-repo').value = state.repo;
614
+ }
615
+
562
616
  $('f-name').addEventListener('input', function () {
563
617
  state.name = this.value;
564
618
  if (!state.slugTouched) {
565
619
  state.slug = deriveSlug(state.name);
566
620
  $('f-slug').value = state.slug;
567
621
  }
622
+ if (!state.repoTouched) {
623
+ state.repo = deriveRepo(state.name);
624
+ $('f-repo').value = state.repo;
625
+ updateGhCreateHref();
626
+ }
568
627
  var h = $('h-name');
569
628
  if (state.slug) { h.innerHTML = 'its handle on the platform: <code>' + esc(state.slug) + '</code>'; h.className = 'hint'; }
570
629
  else { h.innerHTML = '&nbsp;'; }
@@ -577,14 +636,18 @@ input[type=text].bad{border-color:var(--err);}
577
636
  $('c-green').setAttribute('aria-pressed', String(mode === 'greenfield'));
578
637
  $('c-adopt').setAttribute('aria-pressed', String(mode === 'adopt'));
579
638
  $('adoptNote').classList.toggle('on', mode === 'adopt');
639
+ // The "create the repo on GitHub" button only makes sense when starting fresh —
640
+ // an adopted repo already exists.
641
+ $('ghCreate').classList.toggle('on', mode === 'greenfield');
642
+ updateGhCreateHref();
580
643
  saveDraft();
581
644
  }
582
645
  $('c-green').addEventListener('click', function () { setMode('greenfield'); });
583
646
  $('c-adopt').addEventListener('click', function () { setMode('adopt'); });
584
647
  setMode(state.mode);
585
648
 
586
- $('f-owner').addEventListener('input', function () { state.owner = this.value.trim(); saveDraft(); });
587
- $('f-repo').addEventListener('input', function () { state.repo = this.value.trim(); saveDraft(); });
649
+ $('f-owner').addEventListener('input', function () { state.owner = this.value.trim(); state.ownerTouched = true; updateGhCreateHref(); saveDraft(); });
650
+ $('f-repo').addEventListener('input', function () { state.repo = this.value.trim(); state.repoTouched = true; updateGhCreateHref(); saveDraft(); });
588
651
 
589
652
  $('f-slug').addEventListener('input', function () {
590
653
  state.slugTouched = true;
@@ -719,9 +782,12 @@ input[type=text].bad{border-color:var(--err);}
719
782
 
720
783
  var steps = [];
721
784
  var repoLabel = state.owner && state.repo ? '<strong>' + esc(state.owner + '/' + state.repo) + '</strong>' : 'your new repository';
785
+ var ghNewUrl = githubNewRepoUrl(state.owner, state.repo);
786
+ var ghIcon = '<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>';
722
787
  steps.push(adopt
723
788
  ? '<li data-onboard-step="github-repo"><b>Layer onto your existing repo</b><p>In your repo run <code style="font-family:var(--font-mono)">bongos init --adopt</code>. It shows a conflict pre-flight and writes <em>nothing</em> until you accept — your files and history stay yours, and your existing backlog is imported. Commit and push.</p></li>'
724
- : '<li data-onboard-step="github-repo"><b>Create the empty GitHub repo</b><p>On github.com: New repository → ' + repoLabel + ' → leave it completely empty.</p></li>');
789
+ : '<li 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 name already filled in), press <strong>Create repository</strong>, and leave it completely empty. Seeing “repository not found” above? That just means the repo isn’t created yet — make it here.</p>' +
790
+ '<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + ghIcon + 'Create ' + repoLabel + ' on GitHub</a></li>');
725
791
  if (!state.domain) {
726
792
  steps.push('<li><b>Add a domain you own</b><p>Sign-in on your new instance needs a real domain. When you have one, come back here or ask your hall — the address gets wired in minutes.</p></li>');
727
793
  }
@@ -829,8 +895,10 @@ input[type=text].bad{border-color:var(--err);}
829
895
  again.addEventListener('click', function () {
830
896
  stopPolling();
831
897
  try { sessionStorage.removeItem(CREATED_KEY); sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
832
- state = { step: 1, name: '', mode: 'greenfield', owner: '', repo: '', slug: '', slugTouched: false, domain: '' };
898
+ state = { step: 1, name: '', mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '' };
833
899
  $('f-name').value = ''; $('f-owner').value = ''; $('f-repo').value = ''; $('f-slug').value = ''; $('f-domain').value = '';
900
+ // Re-seed the owner from the signed-in account for the next project.
901
+ if (me && me.github_login) { state.owner = me.github_login; $('f-owner').value = state.owner; }
834
902
  $('h-name').innerHTML = '&nbsp;';
835
903
  setMode('greenfield');
836
904
  show(1);
@@ -884,6 +952,14 @@ input[type=text].bad{border-color:var(--err);}
884
952
  .then(function (data) {
885
953
  me = data.builder || data.me || data;
886
954
  renderWho();
955
+ // Default the GitHub owner to the signed-in account until the visitor edits
956
+ // it — one less field to fill for the common "my own account" case.
957
+ if (!state.ownerTouched && !state.owner && me && me.github_login) {
958
+ state.owner = me.github_login;
959
+ $('f-owner').value = state.owner;
960
+ updateGhCreateHref();
961
+ saveDraft();
962
+ }
887
963
  // returning after a create? resume watching that instance instead of a blank wizard
888
964
  var created = null;
889
965
  try { created = JSON.parse(sessionStorage.getItem(CREATED_KEY) || 'null'); } catch (e) {}
@@ -45,20 +45,17 @@ body{
45
45
  .stage{
46
46
  position:relative;z-index:1;min-height:100vh;
47
47
  display:flex;flex-direction:column;align-items:center;justify-content:center;
48
- text-align:center;padding:48px 24px 110px;gap:8px;
48
+ text-align:center;padding:48px 24px 64px;gap:8px;
49
49
  }
50
50
  /* a soft white glow behind the mascot so a white- or transparent-bg PNG blends */
51
51
  .hero-mascot{
52
52
  position:relative;width:min(540px,86vw);margin-bottom:6px;
53
- /* cap by height on short viewports so the hall CTA + sign-in hint always clear
54
- the fixed footer (task 2094); on tall desktops the width cap wins, unchanged */
55
- max-height:50vh;display:flex;justify-content:center;
56
53
  }
57
54
  .hero-mascot::before{
58
55
  content:"";position:absolute;inset:-6% -10%;z-index:-1;border-radius:50%;
59
56
  background:radial-gradient(ellipse at center, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.7) 45%, rgba(255,255,255,0) 72%);
60
57
  }
61
- .hero-mascot img{display:block;width:100%;height:auto;max-height:50vh;object-fit:contain;}
58
+ .hero-mascot img{display:block;width:100%;height:auto;}
62
59
  /* fallback shown if buddha.png is not present yet */
63
60
  .mascot-fallback{
64
61
  width:min(540px,86vw);aspect-ratio:1.6/1;border-radius:28px;
@@ -80,27 +77,9 @@ body{
80
77
  border:none;border-radius:999px;padding:15px 32px;text-decoration:none;
81
78
  display:inline-flex;align-items:center;gap:9px;
82
79
  background:var(--accent);color:#fff;box-shadow:0 12px 28px -10px var(--accent);
83
- transition:transform .12s ease, box-shadow .12s ease, background .12s ease;
80
+ transition:transform .12s ease, box-shadow .12s ease;
84
81
  }
85
82
  .btn:hover{transform:translateY(-1px);box-shadow:0 16px 34px -10px var(--accent);}
86
- .btn.ghost{background:rgba(255,255,255,0.75);color:var(--ink);box-shadow:0 6px 18px -8px rgba(42,31,23,0.25);}
87
- .btn.ghost:hover{box-shadow:0 10px 24px -8px rgba(42,31,23,0.3);}
88
- /* signed-out: the create door is visible but resting — it hints, and clicking it
89
- starts the GitHub sign-in (returning straight into /create). */
90
- .btn.resting{background:#eeddc8;color:#a08a74;box-shadow:none;}
91
- .btn.resting:hover{background:#ecd6ba;transform:none;box-shadow:none;}
92
- .cta-row{display:flex;gap:14px;flex-wrap:wrap;justify-content:center;}
93
- .signin-hint{font-size:13.5px;font-weight:600;color:var(--ink-soft);margin-top:14px;min-height:19px;}
94
- .signin-hint a{color:var(--accent-deep);text-decoration:none;font-weight:700;}
95
- .signin-hint a:hover{text-decoration:underline;}
96
- .who{
97
- position:fixed;top:16px;right:20px;z-index:2;display:flex;align-items:center;gap:9px;
98
- font-size:14px;font-weight:600;color:var(--ink-soft);min-height:30px;
99
- }
100
- .who img{width:28px;height:28px;border-radius:50%;border:2px solid #fff;box-shadow:0 2px 8px rgba(42,31,23,0.14);}
101
- .who a{color:var(--accent-deep);text-decoration:none;font-weight:700;}
102
- .who a:hover{text-decoration:underline;}
103
- .who .who-act{padding-left:10px;margin-left:2px;border-left:1px solid rgba(42,31,23,0.18);}
104
83
  .foot{
105
84
  position:fixed;bottom:18px;left:0;right:0;z-index:1;text-align:center;
106
85
  color:var(--ink-soft);font-size:13px;opacity:0.8;
@@ -119,68 +98,24 @@ body{
119
98
  </div>
120
99
  <div class="wordmark" data-brand-product>cloud bongos</div>
121
100
  <p class="tagline" data-brand-tagline>Building software is laying down a beat.</p>
122
- <div class="cta-row">
123
- <a class="btn" id="hallBtn" data-link-hall href="https://builders.cloudbongos.com">Enter the Hall →</a>
124
- </div>
125
- <p class="signin-hint" id="signinHint" aria-live="polite"></p>
101
+ <a class="btn" data-link-hall href="https://builders.cloudbongos.com">Enter the Hall →</a>
126
102
  </main>
127
- <div class="who" id="who" aria-live="polite"></div>
128
103
 
129
104
  <div class="foot">A non-profit, capture-resistant commons. Built by agents, on Cloud Bongos.</div>
130
105
 
131
106
  <script>
132
107
  /* Brand-pack fill (R57). The server injects window.__BRANDING__ synchronously
133
- ahead of this script (src/bongos/serve-internal.js); the baked markup above is
108
+ ahead of this script (src/gds/serve-internal.js); the baked markup above is
134
109
  the no-JS / standalone fallback. A differently-branded game-less instance
135
110
  overrides the product name, tagline, and Hall link from its own config. */
136
111
  (function () {
137
112
  var b = (typeof window !== 'undefined' && window.__BRANDING__) || null;
138
- if (b) {
139
- var id = b.identity || {}, copy = b.copy || {}, dom = b.domains || {};
140
- var set = function (sel, val){ if(!val) return; document.querySelectorAll(sel).forEach(function(n){ n.textContent = val; }); };
141
- if (id.productName) set('[data-brand-product]', id.productName.toLowerCase());
142
- if (copy.tagline) set('[data-brand-tagline]', copy.tagline);
143
- if (dom.buildersOrigin) document.querySelectorAll('[data-link-hall]').forEach(function(n){ n.setAttribute('href', dom.buildersOrigin); });
144
- }
145
-
146
- /* Membership-aware hall access (goal 35, task 2094). This is a PROJECT's own
147
- landing page. Signed in as an admitted builder here → "Enter the Hall".
148
- Otherwise → "Request access" (this project's own hall shows the request-
149
- access card), with a quiet "already a builder? sign in" for returning
150
- builders. Sign-in is itself gated to approved builders (auth.js), so a
151
- newcomer who tries it is routed to request access anyway. The identity +
152
- action live top-right. Degrades to the request-access visual when the API
153
- is unreachable (static preview / standalone viewing). */
154
- var hallHref = (b && b.domains && b.domains.buildersOrigin) || 'https://builders.cloudbongos.com';
155
- var signInHref = '/api/bongos/auth/web/start?return=' + encodeURIComponent('/');
156
- function esc(s) {
157
- return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
158
- return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
159
- });
160
- }
161
- var hallBtn = document.getElementById('hallBtn');
162
- var signinHint = document.getElementById('signinHint');
163
- var who = document.getElementById('who');
164
-
165
- function showSignedOut() {
166
- if (hallBtn) { hallBtn.textContent = 'Request access'; hallBtn.setAttribute('href', hallHref); }
167
- if (signinHint) signinHint.innerHTML = 'Already a builder here? <a href="' + signInHref + '">Sign in with GitHub</a>.';
168
- if (who) who.innerHTML = '<a href="' + signInHref + '">Sign in with GitHub</a>';
169
- }
170
- function showMember(me) {
171
- if (hallBtn) { hallBtn.textContent = 'Enter the Hall →'; hallBtn.setAttribute('href', hallHref); }
172
- if (signinHint) signinHint.textContent = '';
173
- if (who) who.innerHTML =
174
- (me.avatar_url ? '<img src="' + esc(me.avatar_url) + '" alt="">' : '') +
175
- '<span>' + esc(me.github_login || 'signed in') + '</span>' +
176
- '<a class="who-act" href="' + hallHref + '">Enter hall →</a>';
177
- }
178
-
179
- showSignedOut(); // default until /me resolves (and the no-JS request-access intent)
180
- fetch('/api/bongos/me', { credentials: 'same-origin' })
181
- .then(function (res) { if (!res.ok) throw new Error(String(res.status)); return res.json(); })
182
- .then(function (data) { showMember(data.builder || data.me || data); })
183
- .catch(function () { /* signed out / not a member / unreachable: request-access visual stands */ });
113
+ if (!b) return;
114
+ var id = b.identity || {}, copy = b.copy || {}, dom = b.domains || {};
115
+ function set(sel, val){ if(!val) return; document.querySelectorAll(sel).forEach(function(n){ n.textContent = val; }); }
116
+ if (id.productName) set('[data-brand-product]', id.productName.toLowerCase());
117
+ if (copy.tagline) set('[data-brand-tagline]', copy.tagline);
118
+ if (dom.buildersOrigin) document.querySelectorAll('[data-link-hall]').forEach(function(n){ n.setAttribute('href', dom.buildersOrigin); });
184
119
  })();
185
120
  </script>
186
121
  </body>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -98,6 +98,11 @@ const CONFIG = {
98
98
  // <slug>.caddy file (idempotent overwrite, no fragile append to a shared file);
99
99
  // the main Caddyfile carries a one-time `import <dir>/*.caddy` (operator setup).
100
100
  caddySnippetDir: process.env.PROVISION_CADDY_SNIPPET_DIR || '/etc/caddy/sites',
101
+ // Where nightly pg_dump backups land (db-backup-nightly.sh's GDS_BACKUP_DIR
102
+ // default). Shared across co-tenant instances on one box — dumps are named
103
+ // `<db>-<ts>.sql.gz`, so each instance's files + retention prune stay scoped to
104
+ // its own DB. Owned by the app user, mode 0750. (task 2213, productizes 2210.)
105
+ backupDir: process.env.PROVISION_BACKUP_DIR || '/var/backups/gds',
101
106
  // Dedicated-droplet defaults (only used for hosting_shape='dedicated').
102
107
  region: process.env.PROVISION_REGION || 'nyc3',
103
108
  baseImage: process.env.PROVISION_BASE_IMAGE || 'ubuntu-24-04-x64',
@@ -252,6 +257,10 @@ function seedFirstVersionCmd(inst, { privileged = false } = {}) {
252
257
  // It is the ADR 0108 DESTINATION model; these generators make provision.js drive the
253
258
  // scripts/deploy/deploy-instance.sh runbook API-driven instead of by hand.
254
259
  const CORE_PKG_DIR = 'node_modules/@bongos/core';
260
+ // The unix account the instance's Node process + its backup timer run as. Mirrors
261
+ // serviceUnit's hardcoded `User=lars`/`Group=lars` (the droplet's app user); kept as
262
+ // one constant so the backup dir owner + the backup unit's User= can't drift from it.
263
+ const APP_USER = 'lars';
255
264
  function standaloneRoot(inst, { base = CONFIG.standaloneBase } = {}) { return path.posix.join(base, inst.slug); }
256
265
  // Sync the instance's own repo (host content + the core pin). Mirrors deploy-instance.sh
257
266
  // step 1 — assumes the repo is already cloned at standaloneRoot (initial clone is a
@@ -447,6 +456,80 @@ WantedBy=multi-user.target
447
456
  }
448
457
  function serviceUnitPath(inst) { return `/etc/systemd/system/${inst.slug}.service`; }
449
458
 
459
+ // ── nightly DB backup: per-instance timer + service (task 2213) ─────────────
460
+ // PRODUCTIZES the manual task-2210 fix: a newly-provisioned instance was born with
461
+ // NO backups (dir absent, GDS_BACKUP_DB unset → the script fell back to the
462
+ // nonexistent `amazonprimea` default, and the repo's global gds-db-backup units
463
+ // hardcode /home/lars/amazonprimea — wrong for the consumer node_modules/@bongos/
464
+ // core layout). These generators emit a PER-INSTANCE unit pair that fixes all three:
465
+ // • name → `<slug>-db-backup.{service,timer}` so co-tenant instances on one box
466
+ // never collide (each dumps its OWN db; the script's retention prune is
467
+ // already `<db>-*.sql.gz`-scoped, so a shared backupDir is safe).
468
+ // • DB → Environment=GDS_BACKUP_DB=<instance db>, set on the unit (not read from
469
+ // the web.env) so the dump targets the right database with no fallback.
470
+ // • path → ExecStart resolves the canonical script INSIDE the instance's pinned
471
+ // @bongos/core (standalone) or this control-plane checkout (co-tenant),
472
+ // the SAME workDir/CORE_PKG_DIR split serviceUnit uses — never the old
473
+ // monorepo path.
474
+ function backupUnitName(inst) { return `${inst.slug}-db-backup`; }
475
+ function backupServicePath(inst) { return `/etc/systemd/system/${backupUnitName(inst)}.service`; }
476
+ function backupTimerPath(inst) { return `/etc/systemd/system/${backupUnitName(inst)}.timer`; }
477
+ // The consumer-layout-correct absolute path to db-backup-nightly.sh. Standalone runs
478
+ // it from the instance's own pinned core; co-tenant/dedicated from this checkout's
479
+ // scripts/gds/ — mirrors serviceUnit's execStart resolution so they can't diverge.
480
+ function backupScriptPath(inst) {
481
+ const rel = 'scripts/gds/db-backup-nightly.sh';
482
+ return inst.hosting_shape === 'standalone'
483
+ ? path.posix.join(standaloneRoot(inst), CORE_PKG_DIR, rel)
484
+ : path.posix.join(REPO_ROOT, rel);
485
+ }
486
+ function backupService(inst, { backupDir = CONFIG.backupDir } = {}) {
487
+ return `[Unit]
488
+ Description=Nightly pg_dump of the ${inst.slug} GDS DB (provisioned by provision.js, task 2213)
489
+ After=postgresql.service
490
+ Requires=postgresql.service
491
+
492
+ [Service]
493
+ Type=oneshot
494
+ User=${APP_USER}
495
+ Group=${APP_USER}
496
+ # The DB to dump + where dumps land — set on the unit (not via the instance web.env)
497
+ # so the dump targets the right database with no fallback to a stale default (root
498
+ # cause of the task-2210 breakage). db-backup-nightly.sh reads both as env.
499
+ Environment=GDS_BACKUP_DB=${dbName(inst)}
500
+ Environment=GDS_BACKUP_DIR=${backupDir}
501
+ # Consumer-layout path: the canonical script inside this instance's pinned core.
502
+ ExecStart=/bin/bash ${backupScriptPath(inst)}
503
+ # Modest hardening — the script only needs read on the pg socket + write on the
504
+ # backup dir. Mirrors the repo's gds-db-backup.service.
505
+ NoNewPrivileges=true
506
+ ProtectSystem=strict
507
+ ReadWritePaths=${backupDir}
508
+ ProtectHome=read-only
509
+ PrivateTmp=true
510
+
511
+ [Install]
512
+ # Not socket-activated; the timer drives this. The timer's WantedBy is what enables.
513
+ `;
514
+ }
515
+ function backupTimer(inst) {
516
+ return `[Unit]
517
+ Description=Daily timer for the ${inst.slug} GDS DB pg_dump (provisioned by provision.js, task 2213)
518
+
519
+ [Timer]
520
+ # 04:00 UTC daily, mirroring the repo's gds-db-backup.timer (low-traffic window).
521
+ OnCalendar=*-*-* 04:00:00 UTC
522
+ # Catch up on next boot if the box was off when the timer was due.
523
+ Persistent=true
524
+ # Jitter so simultaneous timers don't dogpile the disk at exactly 04:00.
525
+ RandomizedDelaySec=300
526
+ Unit=${backupUnitName(inst)}.service
527
+
528
+ [Install]
529
+ WantedBy=timers.target
530
+ `;
531
+ }
532
+
450
533
  // The branding pack a provisioned CO-TENANT instance loads: the shared, committed
451
534
  // neutral starter (the co-tenant model shares ONE checkout, differentiated only by
452
535
  // env — src/branding.js's GDS_BRANDING_FILE knob). Per-instance identity/domains
@@ -950,6 +1033,21 @@ async function provisionInstance(inst, deps) {
950
1033
  boxExec(`${sudoP}systemctl enable --now ${inst.slug}`);
951
1034
  } else { log(' (unit baked into cloud-init)'); }
952
1035
 
1036
+ // ── step 4b: nightly DB backup — dir + consumer-layout timer/service (task 2213) ──
1037
+ // Every instance must be born WITH backups (the task-2210 gap: cloudbongos was
1038
+ // provisioned without any). Create the app-user-owned backup dir, install the
1039
+ // per-instance timer/service (correct db + consumer-layout script path), enable
1040
+ // the timer. Idempotent: `install -d` no-ops on an existing dir, unit writes are
1041
+ // overwrites, `enable --now` is idempotent. Dedicated bakes the same into cloud-init.
1042
+ log(' [backup] create backup dir + install + enable nightly DB backup timer');
1043
+ if (local) {
1044
+ boxExec(`${sudoP}install -d -o ${APP_USER} -g ${APP_USER} -m 0750 ${CONFIG.backupDir}`);
1045
+ boxWriteFile(backupServicePath(inst), backupService(inst), { mode: 0o644, sudo: true });
1046
+ boxWriteFile(backupTimerPath(inst), backupTimer(inst), { mode: 0o644, sudo: true });
1047
+ boxExec(`${sudoP}systemctl daemon-reload`);
1048
+ boxExec(`${sudoP}systemctl enable --now ${backupUnitName(inst)}.timer`);
1049
+ } else { log(' (backup dir + units baked into cloud-init)'); }
1050
+
953
1051
  // ── step 5: DNS — Cloudflare A-record UPSERT for the instance's domain (P4) ──
954
1052
  // Formalized in P4 (#1945): the shared box-dns-cloudflare.sh hook UPSERTs an A
955
1053
  // record pointing the domain at its target IP (recordIp), in the domain's own
@@ -1009,12 +1107,28 @@ async function provisionInstance(inst, deps) {
1009
1107
  // SAME pure command generators so local + dedicated never diverge.
1010
1108
  function dedicatedUserData(inst) {
1011
1109
  const i = { ...inst, db_name: inst.db_name || inst.slug };
1110
+ // task 2213: a dedicated droplet gets the same nightly backup as local shapes —
1111
+ // write the per-instance unit pair (base64-encoded to sidestep YAML block-scalar
1112
+ // escaping of the multi-line bodies), create the app-user-owned backup dir, and
1113
+ // enable the timer alongside the web unit. Mirrors provisionInstance's step 4b.
1114
+ const svcB64 = Buffer.from(backupService(i), 'utf8').toString('base64');
1115
+ const timerB64 = Buffer.from(backupTimer(i), 'utf8').toString('base64');
1012
1116
  return `#cloud-config
1117
+ write_files:
1118
+ - path: ${backupServicePath(i)}
1119
+ encoding: b64
1120
+ permissions: '0644'
1121
+ content: ${svcB64}
1122
+ - path: ${backupTimerPath(i)}
1123
+ encoding: b64
1124
+ permissions: '0644'
1125
+ content: ${timerB64}
1013
1126
  runcmd:
1014
1127
  - [ bash, -lc, "${dbCreateCmd(i)} || true" ]
1015
1128
  - [ bash, -lc, "cd ${REPO_ROOT} && ${migrateCmd(i)}" ]
1016
1129
  - [ bash, -lc, "${seedFirstVersionCmd(i)}" ]
1017
- - [ bash, -lc, "systemctl daemon-reload && systemctl enable --now ${i.slug}" ]
1130
+ - [ bash, -lc, "install -d -o ${APP_USER} -g ${APP_USER} -m 0750 ${CONFIG.backupDir}" ]
1131
+ - [ bash, -lc, "systemctl daemon-reload && systemctl enable --now ${i.slug} ${backupUnitName(i)}.timer" ]
1018
1132
  `;
1019
1133
  }
1020
1134
 
@@ -1034,10 +1148,16 @@ async function teardownInstance(inst, deps) {
1034
1148
  if (inst.hosting_shape !== 'dedicated') {
1035
1149
  boxExec(`${sudoP}systemctl disable --now ${inst.slug}`, { allowFail: true });
1036
1150
  boxExec(`${sudoP}rm -f ${serviceUnitPath(inst)}`, { allowFail: true });
1151
+ // Stop + remove the nightly backup units too (task 2213). The backup DIR + its
1152
+ // dumps are left in place — like the DB below, they are precious data a teardown
1153
+ // must not silently destroy.
1154
+ boxExec(`${sudoP}systemctl disable --now ${backupUnitName(inst)}.timer`, { allowFail: true });
1155
+ boxExec(`${sudoP}rm -f ${backupServicePath(inst)} ${backupTimerPath(inst)}`, { allowFail: true });
1037
1156
  boxExec(`${sudoP}systemctl daemon-reload`, { allowFail: true });
1038
- // DB + Caddy block are left for an explicit operator (data is precious; a
1039
- // teardown should not silently dropdb). We log the manual follow-ups.
1040
- log(` (left for the operator: dropdb ${dbName(inst)}, remove the Caddy block for ${inst.slug})`);
1157
+ // DB + Caddy block + backup dumps are left for an explicit operator (data is
1158
+ // precious; a teardown should not silently dropdb or delete backups). Log the
1159
+ // manual follow-ups.
1160
+ log(` (left for the operator: dropdb ${dbName(inst)}, remove the Caddy block for ${inst.slug}, prune ${CONFIG.backupDir}/${dbName(inst)}-*.sql.gz)`);
1041
1161
  } else if (apply && deps.doApi && inst.droplet_id) {
1042
1162
  await deps.doApi.deleteDroplet(inst.droplet_id);
1043
1163
  }
@@ -1396,6 +1516,7 @@ module.exports = {
1396
1516
  CONFIG, MAX_INTENT_ATTEMPTS,
1397
1517
  // pure step generators (tested directly)
1398
1518
  dbName, dbCreateCmd, migrateCmd, seedFirstVersionCmd, serviceUnit, serviceUnitPath, webEnvBody, webEnvPath,
1519
+ backupUnitName, backupServicePath, backupTimerPath, backupScriptPath, backupService, backupTimer,
1399
1520
  standaloneRoot, standalonePullCmd, standaloneInstallCmd, standaloneMigrateCmd,
1400
1521
  // standalone scaffold (ADR 0108 / 0125, task 2055)
1401
1522
  parseTargetRef, instanceRepoRemote, instanceInitSpec, scaffoldStandaloneRepo, coreVersionSafe,
File without changes
File without changes
File without changes
File without changes
@@ -84,7 +84,17 @@ const GRADER_TIMEOUT_MS = 240_000; // 4 min — Opus on a big diff with Read too
84
84
  // feature worktree this script lives in. Falls back to two-levels-up if we
85
85
  // can't ask git (e.g. fresh clone with no `main` worktree set up yet).
86
86
  const REPO_ROOT = findMainWorktree(__dirname) || path.resolve(__dirname, '..', '..');
87
- const { playSound } = require(path.resolve(__dirname, '../../.claude/sounds/play.js'));
87
+ // Sound playback is COSMETIC — the ship pipeline must NEVER depend on it. Guard
88
+ // the require so a missing/broken player (e.g. .claude/sounds/ absent on a fresh
89
+ // clone or after a re-seed) degrades to a silent no-op instead of crashing ship.js
90
+ // at load. A cosmetic feature can't be allowed to break shipping (task 1002199).
91
+ const { playSound } = (() => {
92
+ try {
93
+ return require(path.resolve(__dirname, '../../.claude/sounds/play.js'));
94
+ } catch {
95
+ return { playSound: () => {} };
96
+ }
97
+ })();
88
98
  const DROPLET = 'lars@104.236.254.243';
89
99
  const BEHIND_MAIN_LIMIT = 5;
90
100
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes