@bongos/core 1.19.126 → 1.19.128
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +28 -23
- package/modules/builder-settings/builder-needs.js +59 -2
- package/modules/provisioning/migrations/provisioning_008_dns_note.sql +15 -0
- package/modules/provisioning/provisioning.js +28 -1
- package/modules/provisioning/routes/provisioning.js +26 -1
- package/modules/provisioning/tests/provisioning.mjs +21 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/projects.html +88 -18
- package/scripts/gds/provision.js +115 -19
- package/src/bongos/db-kernel.js +15 -0
- package/src/bongos/db.js +2 -1
- package/src/bongos/routes/me.js +11 -1
- package/src/module-api.js +5 -1
- package/tests/builder_needs.mjs +114 -0
- package/tests/provision.mjs +272 -6
- package/tests/provisioning_domain_attach.mjs +40 -0
- package/tests/public_landing_projects.mjs +33 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.128",
|
|
6
|
+
"core_contract": "1.19.128",
|
|
7
|
+
"source_commit": "15671cf5128a6b644791170f9b76c0637bcc0c2a",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-15T01:56:43.213Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 303,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1246,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1570,
|
|
20
|
+
"tree_sha256": "4df41f4612e88057c6058199bd929bb309af81367a8fd7435309497c2456ac1a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2367,7 +2367,7 @@
|
|
|
2367
2367
|
{
|
|
2368
2368
|
"path": "modules/builder-settings/builder-needs.js",
|
|
2369
2369
|
"mode": "0000644",
|
|
2370
|
-
"sha256": "
|
|
2370
|
+
"sha256": "3cb7478d9e347147e331f0954a9178a1770918067a678efdb8caef96241dfb5a"
|
|
2371
2371
|
},
|
|
2372
2372
|
{
|
|
2373
2373
|
"path": "modules/builder-settings/db.js",
|
|
@@ -3844,6 +3844,11 @@
|
|
|
3844
3844
|
"mode": "0000644",
|
|
3845
3845
|
"sha256": "ddbc1f7e659e716fcbb3ba3ec6c1237756f46dbd895bcb27a1dc925a7c782e76"
|
|
3846
3846
|
},
|
|
3847
|
+
{
|
|
3848
|
+
"path": "modules/provisioning/migrations/provisioning_008_dns_note.sql",
|
|
3849
|
+
"mode": "0000644",
|
|
3850
|
+
"sha256": "8dc04a18ad77563a2bd73029bf13cab03fb5dadd1b62850fddbde475257e6a76"
|
|
3851
|
+
},
|
|
3847
3852
|
{
|
|
3848
3853
|
"path": "modules/provisioning/module.json",
|
|
3849
3854
|
"mode": "0000644",
|
|
@@ -3862,7 +3867,7 @@
|
|
|
3862
3867
|
{
|
|
3863
3868
|
"path": "modules/provisioning/provisioning.js",
|
|
3864
3869
|
"mode": "0000644",
|
|
3865
|
-
"sha256": "
|
|
3870
|
+
"sha256": "90aa880898f6b3235e8b2a1a553695c63c29920968edb9c9af58364902118e33"
|
|
3866
3871
|
},
|
|
3867
3872
|
{
|
|
3868
3873
|
"path": "modules/provisioning/rate-limit.js",
|
|
@@ -3872,7 +3877,7 @@
|
|
|
3872
3877
|
{
|
|
3873
3878
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
3874
3879
|
"mode": "0000644",
|
|
3875
|
-
"sha256": "
|
|
3880
|
+
"sha256": "eddf451d5ee5085563af258b5c605c445776c9e226587341f9b4b8b405492240"
|
|
3876
3881
|
},
|
|
3877
3882
|
{
|
|
3878
3883
|
"path": "modules/provisioning/tests/liveness.mjs",
|
|
@@ -3882,7 +3887,7 @@
|
|
|
3882
3887
|
{
|
|
3883
3888
|
"path": "modules/provisioning/tests/provisioning.mjs",
|
|
3884
3889
|
"mode": "0000644",
|
|
3885
|
-
"sha256": "
|
|
3890
|
+
"sha256": "0dc18db288580ca00792194ce5f50a13aae8c445c14d7fc42a2095ab2379c93f"
|
|
3886
3891
|
},
|
|
3887
3892
|
{
|
|
3888
3893
|
"path": "modules/provisioning/ui/.gitkeep",
|
|
@@ -4167,12 +4172,12 @@
|
|
|
4167
4172
|
{
|
|
4168
4173
|
"path": "package-lock.json",
|
|
4169
4174
|
"mode": "0000644",
|
|
4170
|
-
"sha256": "
|
|
4175
|
+
"sha256": "573c516fc294514ce5ed281eb161de21132ddbfd775c5eb63ae1636bd89fc854"
|
|
4171
4176
|
},
|
|
4172
4177
|
{
|
|
4173
4178
|
"path": "package.json",
|
|
4174
4179
|
"mode": "0000644",
|
|
4175
|
-
"sha256": "
|
|
4180
|
+
"sha256": "bfe73c49720864c119018c0ce9a8889271e7f67c4708ca4dbeaf5af26d277b11"
|
|
4176
4181
|
},
|
|
4177
4182
|
{
|
|
4178
4183
|
"path": "public-docs/index.html",
|
|
@@ -4427,7 +4432,7 @@
|
|
|
4427
4432
|
{
|
|
4428
4433
|
"path": "public-landing/projects.html",
|
|
4429
4434
|
"mode": "0000644",
|
|
4430
|
-
"sha256": "
|
|
4435
|
+
"sha256": "ac866c928701c41ac7f055f8807590a3db71eb2538502a54a6fca9ac1d79868d"
|
|
4431
4436
|
},
|
|
4432
4437
|
{
|
|
4433
4438
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -5082,7 +5087,7 @@
|
|
|
5082
5087
|
{
|
|
5083
5088
|
"path": "scripts/gds/provision.js",
|
|
5084
5089
|
"mode": "0000644",
|
|
5085
|
-
"sha256": "
|
|
5090
|
+
"sha256": "1925ed112ec553a95388b81cccc1fb003ba2a0e91a5a80c6e315b45895688d0f"
|
|
5086
5091
|
},
|
|
5087
5092
|
{
|
|
5088
5093
|
"path": "scripts/gds/publish-credential-check.js",
|
|
@@ -5527,12 +5532,12 @@
|
|
|
5527
5532
|
{
|
|
5528
5533
|
"path": "src/bongos/db-kernel.js",
|
|
5529
5534
|
"mode": "0000644",
|
|
5530
|
-
"sha256": "
|
|
5535
|
+
"sha256": "f31dd847ed641bf49013c345b1f08f9e2794a875dad777de10daeda2a4ae1cf3"
|
|
5531
5536
|
},
|
|
5532
5537
|
{
|
|
5533
5538
|
"path": "src/bongos/db.js",
|
|
5534
5539
|
"mode": "0000644",
|
|
5535
|
-
"sha256": "
|
|
5540
|
+
"sha256": "d3dd606664855a94788751d13ab7fc62ccdc4a8df48f331ec97a5a81a8a6fa96"
|
|
5536
5541
|
},
|
|
5537
5542
|
{
|
|
5538
5543
|
"path": "src/bongos/github-repos.js",
|
|
@@ -5687,7 +5692,7 @@
|
|
|
5687
5692
|
{
|
|
5688
5693
|
"path": "src/bongos/routes/me.js",
|
|
5689
5694
|
"mode": "0000644",
|
|
5690
|
-
"sha256": "
|
|
5695
|
+
"sha256": "83657d8a2f8e1d81a80324350ddd983baf5346da3f88ce1f41c2211275f34440"
|
|
5691
5696
|
},
|
|
5692
5697
|
{
|
|
5693
5698
|
"path": "src/bongos/routes/modules.js",
|
|
@@ -5757,7 +5762,7 @@
|
|
|
5757
5762
|
{
|
|
5758
5763
|
"path": "src/module-api.js",
|
|
5759
5764
|
"mode": "0000644",
|
|
5760
|
-
"sha256": "
|
|
5765
|
+
"sha256": "e0b338840f48746ba4ad9bccd7dd4b80c36b844c2e16ea12bdf42f22ceb2eef5"
|
|
5761
5766
|
},
|
|
5762
5767
|
{
|
|
5763
5768
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6147,7 +6152,7 @@
|
|
|
6147
6152
|
{
|
|
6148
6153
|
"path": "tests/builder_needs.mjs",
|
|
6149
6154
|
"mode": "0000644",
|
|
6150
|
-
"sha256": "
|
|
6155
|
+
"sha256": "49a8146cbb0ec5def5b48130ed61fdec6e9855f22fe5ad3a09b39ae2cb218ca8"
|
|
6151
6156
|
},
|
|
6152
6157
|
{
|
|
6153
6158
|
"path": "tests/bump_version.mjs",
|
|
@@ -7297,7 +7302,7 @@
|
|
|
7297
7302
|
{
|
|
7298
7303
|
"path": "tests/provision.mjs",
|
|
7299
7304
|
"mode": "0000644",
|
|
7300
|
-
"sha256": "
|
|
7305
|
+
"sha256": "5297caf5005fd26c8435aa374511882a6c2d232e43f07cdc6ec19d1168ee1cc1"
|
|
7301
7306
|
},
|
|
7302
7307
|
{
|
|
7303
7308
|
"path": "tests/provisioning_default_domain.mjs",
|
|
@@ -7307,7 +7312,7 @@
|
|
|
7307
7312
|
{
|
|
7308
7313
|
"path": "tests/provisioning_domain_attach.mjs",
|
|
7309
7314
|
"mode": "0000644",
|
|
7310
|
-
"sha256": "
|
|
7315
|
+
"sha256": "856f434c8e1a8e8ad549c81689f1bbe27ff555e4ba08f9b485c3e9d46838a4a5"
|
|
7311
7316
|
},
|
|
7312
7317
|
{
|
|
7313
7318
|
"path": "tests/provisioning_github_app_routes.mjs",
|
|
@@ -7332,7 +7337,7 @@
|
|
|
7332
7337
|
{
|
|
7333
7338
|
"path": "tests/public_landing_projects.mjs",
|
|
7334
7339
|
"mode": "0000644",
|
|
7335
|
-
"sha256": "
|
|
7340
|
+
"sha256": "7f2fe0290fc0a429b54ca53d449453e9009a57e40bb8003b2718beef33be2309"
|
|
7336
7341
|
},
|
|
7337
7342
|
{
|
|
7338
7343
|
"path": "tests/public_mirror_export.mjs",
|
|
@@ -90,13 +90,68 @@ function artKeyNeed(ctx) {
|
|
|
90
90
|
// pure (ctx) => need|null function here — every renderer picks it up for free.
|
|
91
91
|
const NEEDS = [artKeyNeed];
|
|
92
92
|
|
|
93
|
+
// ---- founding grace (task 1002739 / direction §5.6, idea 1000701) -----------
|
|
94
|
+
//
|
|
95
|
+
// A freshly-provisioned instance used to greet its founding owner with "your
|
|
96
|
+
// free art coverage has ended" — a demand as the payoff moment of the whole
|
|
97
|
+
// create flow. While the founding owner's first days are running, the roster
|
|
98
|
+
// leads with a welcome and every action_needed item is held at 'covered'
|
|
99
|
+
// (listed, honest, but not demanding). First-run is a time window, not a
|
|
100
|
+
// visit-counter: counting the first hall visit would end the grace on the very
|
|
101
|
+
// request that renders it.
|
|
102
|
+
//
|
|
103
|
+
// ctx.isFoundingOwner — is this builder the instance's first admitted builder?
|
|
104
|
+
// ctx.founderCreatedAt — that first builder's created_at (≈ the instance's
|
|
105
|
+
// birth on a newborn); the window anchors here.
|
|
106
|
+
// ctx.projectName — branding identity.productName, for the greeting.
|
|
107
|
+
// ctx.nowMs — injectable clock for tests; defaults to Date.now().
|
|
108
|
+
const FOUNDING_GRACE_DAYS = 7;
|
|
109
|
+
const FOUNDING_GRACE_MS = FOUNDING_GRACE_DAYS * 24 * 60 * 60 * 1000;
|
|
110
|
+
|
|
111
|
+
function foundingGraceActive(ctx = {}) {
|
|
112
|
+
if (!ctx.isFoundingOwner || !ctx.founderCreatedAt) return false;
|
|
113
|
+
const born = new Date(ctx.founderCreatedAt).getTime();
|
|
114
|
+
if (!Number.isFinite(born)) return false;
|
|
115
|
+
const now = typeof ctx.nowMs === 'number' ? ctx.nowMs : Date.now();
|
|
116
|
+
return now - born < FOUNDING_GRACE_MS;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function foundingWelcomeNeed(ctx) {
|
|
120
|
+
const name = ctx.projectName || 'your project';
|
|
121
|
+
return {
|
|
122
|
+
id: 'founding_welcome',
|
|
123
|
+
title: `Welcome to ${name}`,
|
|
124
|
+
what: 'Your project is standing, and this hall is where it gets built.',
|
|
125
|
+
learn_more_url: null,
|
|
126
|
+
state: 'covered',
|
|
127
|
+
severity: 'info',
|
|
128
|
+
message: `You made it — ${name} is up, and this hall is its workshop. ` +
|
|
129
|
+
'Look around at your own pace; if the platform ever needs something from you, ' +
|
|
130
|
+
'it will say so here once your first days are behind you.',
|
|
131
|
+
action: null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
93
135
|
// Compute the live list for a builder. `ctx` carries everything the need
|
|
94
136
|
// functions read (see each need's doc). Returns:
|
|
95
137
|
// { items: Need[], action_needed_count, has_action_needed }
|
|
96
138
|
// items is ordered: action_needed first (loudest), then covered, then satisfied,
|
|
97
|
-
// so any renderer that takes "the first one" gets the most urgent.
|
|
139
|
+
// so any renderer that takes "the first one" gets the most urgent. During the
|
|
140
|
+
// founding grace nothing is action_needed, so the welcome (prepended before the
|
|
141
|
+
// stable sort) is what that renderer shows.
|
|
98
142
|
function computeNeeds(ctx = {}) {
|
|
99
|
-
const
|
|
143
|
+
const grace = foundingGraceActive(ctx);
|
|
144
|
+
let items = NEEDS.map((fn) => fn(ctx)).filter(Boolean);
|
|
145
|
+
if (grace) {
|
|
146
|
+
items = items.map((n) => n.state !== 'action_needed' ? n : {
|
|
147
|
+
...n,
|
|
148
|
+
state: 'covered',
|
|
149
|
+
severity: 'info',
|
|
150
|
+
message: 'Nothing needed from you today — we’ll bring this up once your first days are behind you.',
|
|
151
|
+
action: null,
|
|
152
|
+
});
|
|
153
|
+
items.unshift(foundingWelcomeNeed(ctx));
|
|
154
|
+
}
|
|
100
155
|
const order = { action_needed: 0, covered: 1, satisfied: 2 };
|
|
101
156
|
items.sort((a, b) => (order[a.state] ?? 9) - (order[b.state] ?? 9));
|
|
102
157
|
const actionNeeded = items.filter((n) => n.state === 'action_needed');
|
|
@@ -111,5 +166,7 @@ module.exports = {
|
|
|
111
166
|
computeNeeds,
|
|
112
167
|
// exported for tests / direct reuse
|
|
113
168
|
artKeyNeed,
|
|
169
|
+
foundingGraceActive,
|
|
170
|
+
FOUNDING_GRACE_DAYS,
|
|
114
171
|
SETTINGS_PATH,
|
|
115
172
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- provisioning_008_dns_note.sql — transport the runner's "DNS was skipped" fact
|
|
2
|
+
-- to the owner's card (task 1002720 / QA-walk F16).
|
|
3
|
+
--
|
|
4
|
+
-- WHY. With BOX_DNS_HOOK unset, domain-attach wires env + Caddy but SKIPS the
|
|
5
|
+
-- A-record upsert — a fact that lived only in the runner's log while the owner's
|
|
6
|
+
-- card promised the address "will answer shortly" (it never would). This column
|
|
7
|
+
-- carries the outstanding manual step as a plain sentence the card renders.
|
|
8
|
+
--
|
|
9
|
+
-- OVERLAY, NOT A STATE (the provisioning_006 rule): nullable, no default,
|
|
10
|
+
-- never written by the lifecycle. NULL = nothing outstanding. A later attach
|
|
11
|
+
-- with a working hook clears it.
|
|
12
|
+
ALTER TABLE provisioning_instances ADD COLUMN IF NOT EXISTS dns_note text;
|
|
13
|
+
|
|
14
|
+
INSERT INTO schema_migrations (version) VALUES ('provisioning_008_dns_note')
|
|
15
|
+
ON CONFLICT (version) DO NOTHING;
|
|
@@ -198,6 +198,9 @@ function publicInstance(row) {
|
|
|
198
198
|
// them differently ("live?" vs "unreachable"). Never collapse the two.
|
|
199
199
|
last_verified_at: row.last_verified_at || null,
|
|
200
200
|
last_verified_up: row.last_verified_up == null ? null : !!row.last_verified_up,
|
|
201
|
+
// The outstanding manual-DNS step, if the runner had no hook to point the
|
|
202
|
+
// A record (task 1002720 / F16). null = nothing outstanding.
|
|
203
|
+
dns_note: row.dns_note || null,
|
|
201
204
|
cost_estimate_usd: Number(row.cost_estimate_usd || 0),
|
|
202
205
|
error_note: row.error_note || null,
|
|
203
206
|
created_at: row.created_at,
|
|
@@ -372,6 +375,30 @@ async function recordLivenessProbe(db, id, up) {
|
|
|
372
375
|
return rows[0] || null;
|
|
373
376
|
}
|
|
374
377
|
|
|
378
|
+
// Record (or clear, with null) the outstanding manual-DNS sentence the card
|
|
379
|
+
// renders (task 1002720 / F16). Same narrowness rule as recordLivenessProbe:
|
|
380
|
+
// no `status`, no `updated_at` — the runner's lifecycle authorship and the
|
|
381
|
+
// owner's card ordering stay untouched.
|
|
382
|
+
async function recordDnsNote(db, id, note) {
|
|
383
|
+
const { rows } = await db.query(
|
|
384
|
+
`UPDATE provisioning_instances SET dns_note = $2 WHERE id = $1 RETURNING id, dns_note`,
|
|
385
|
+
[id, note || null]
|
|
386
|
+
);
|
|
387
|
+
return rows[0] || null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Clear the error_note latch (task 1002720 / F19). The retire leg of the intent
|
|
391
|
+
// runner sets it when work stops; a later SUCCESSFUL run must release it, or the
|
|
392
|
+
// card shows "we stopped trying" forever on a healed instance. Same narrowness
|
|
393
|
+
// rule: no status, no updated_at.
|
|
394
|
+
async function clearErrorNote(db, id) {
|
|
395
|
+
const { rows } = await db.query(
|
|
396
|
+
`UPDATE provisioning_instances SET error_note = NULL WHERE id = $1 RETURNING id`,
|
|
397
|
+
[id]
|
|
398
|
+
);
|
|
399
|
+
return rows[0] || null;
|
|
400
|
+
}
|
|
401
|
+
|
|
375
402
|
// The caller's own instances, newest first.
|
|
376
403
|
async function listInstancesForOwner(db, ownerBuilderId) {
|
|
377
404
|
const { rows } = await db.query(
|
|
@@ -701,7 +728,7 @@ module.exports = {
|
|
|
701
728
|
isProvisionedHostname,
|
|
702
729
|
getInstanceById, getInstanceBySlug, getInstanceByDomain, listInstancesForOwner, listAllInstances,
|
|
703
730
|
// data access — verification overlay (task 1002696)
|
|
704
|
-
listActiveInstancesForLiveness, recordLivenessProbe,
|
|
731
|
+
listActiveInstancesForLiveness, recordLivenessProbe, recordDnsNote, clearErrorNote,
|
|
705
732
|
createInstance, setInstanceStatus, updateInstanceDomain,
|
|
706
733
|
enqueueIntent, enqueueTeardown, getOpenIntent, claimNextIntent, resolveIntent,
|
|
707
734
|
recordEvent, instanceStateForSlug, costLedger,
|
|
@@ -589,6 +589,28 @@ module.exports = function provisioningRoutes() {
|
|
|
589
589
|
}
|
|
590
590
|
const current = provisioning.normalizeHostname(inst.domain);
|
|
591
591
|
if (current && current === domain) {
|
|
592
|
+
// Re-sending the SAME address is the owner's only re-kick when the
|
|
593
|
+
// wiring stopped (task 1002720 / F19) — the mirror of what POST
|
|
594
|
+
// /instances already does for a snagged standup. Only when the wiring
|
|
595
|
+
// DEMONSTRABLY needs it — a retired intent left its error_note, or the
|
|
596
|
+
// probe says the address doesn't answer — and nothing is in flight. A
|
|
597
|
+
// healthy instance (no note, verified up or unprobed) keeps the
|
|
598
|
+
// idempotent no-op below.
|
|
599
|
+
const needsRewire = inst.error_note || inst.last_verified_up === false;
|
|
600
|
+
if (inst.status === 'active' && needsRewire && !(await provisioning.getOpenIntent(pool, id))) {
|
|
601
|
+
const { created } = await provisioning.enqueueIntent(pool, id, 'domain-attach', 'api:self');
|
|
602
|
+
if (created) {
|
|
603
|
+
await provisioning.recordEvent(pool, {
|
|
604
|
+
instanceId: id, ownerBuilderId: req.builder.id, event: 'domain',
|
|
605
|
+
detail: `domain ${domain} re-attach queued (owner re-kick)`, actor: 'api:self',
|
|
606
|
+
}).catch(() => {});
|
|
607
|
+
}
|
|
608
|
+
// created:false = a racing enqueue got there first — still truthful.
|
|
609
|
+
return res.json({ ok: true, changed: false, queued: !!created, domain: inst.domain,
|
|
610
|
+
message: created
|
|
611
|
+
? 'Wiring that address again — this card updates when it starts.'
|
|
612
|
+
: 'That address is already being wired — this card updates when it starts.' });
|
|
613
|
+
}
|
|
592
614
|
return res.json({ ok: true, changed: false, queued: false, domain: inst.domain,
|
|
593
615
|
message: 'This instance already has that address.' });
|
|
594
616
|
}
|
|
@@ -640,8 +662,11 @@ module.exports = function provisioningRoutes() {
|
|
|
640
662
|
actor: 'api:self',
|
|
641
663
|
}).catch(() => {});
|
|
642
664
|
res.json({ ok: true, changed: true, queued, domain,
|
|
665
|
+
// State-only sentence (task 1002720 / F16): the web tier holds no cloud
|
|
666
|
+
// tokens and cannot know whether a DNS hook exists on the runner, so
|
|
667
|
+
// "it will answer there shortly" was a promise it could not keep.
|
|
643
668
|
message: queued
|
|
644
|
-
? 'Your instance is being wired to its new address —
|
|
669
|
+
? 'Your instance is being wired to its new address — this card will show whether it answers.'
|
|
645
670
|
: 'Address recorded — it will be wired when the instance is provisioned.' });
|
|
646
671
|
} catch (err) {
|
|
647
672
|
console.error('[provisioning] POST /provisioning/instances/:id/domain', err);
|
|
@@ -235,5 +235,26 @@ t('publicInstance projects the declared type — nullable, never silently droppe
|
|
|
235
235
|
assert.equal(q.type, null, 'a row predating provisioning_007 projects null, not undefined');
|
|
236
236
|
});
|
|
237
237
|
|
|
238
|
+
t('publicInstance projects dns_note — nullable, never silently dropped (task 1002720 / F16)', () => {
|
|
239
|
+
const p = P.publicInstance({ id: 1, slug: 'x', status: 'active', dns_note: 'point the A record at 203.0.113.5' });
|
|
240
|
+
assert.match(p.dns_note, /A record/, 'the owner-facing manual-DNS sentence must reach the card');
|
|
241
|
+
const q = P.publicInstance({ id: 2, slug: 'y', status: 'active' });
|
|
242
|
+
assert.equal(q.dns_note, null, 'a row predating provisioning_008 projects null, not undefined');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await ta('recordDnsNote / clearErrorNote are NARROW single-column updates — no status, no updated_at', async () => {
|
|
246
|
+
let sql = '', params = null;
|
|
247
|
+
const db = { query: async (q, p) => { sql = q; params = p; return { rows: [{ id: 5 }] }; } };
|
|
248
|
+
await P.recordDnsNote(db, 5, 'note text');
|
|
249
|
+
assert.match(sql, /SET dns_note = \$2/);
|
|
250
|
+
assert.doesNotMatch(sql, /status|updated_at/, 'the overlay must never reshuffle lifecycle or card order');
|
|
251
|
+
assert.deepEqual(params, [5, 'note text']);
|
|
252
|
+
await P.recordDnsNote(db, 5, null);
|
|
253
|
+
assert.deepEqual(params, [5, null], 'null clears');
|
|
254
|
+
await P.clearErrorNote(db, 5);
|
|
255
|
+
assert.match(sql, /SET error_note = NULL/);
|
|
256
|
+
assert.doesNotMatch(sql, /status\s*=|updated_at/, 'releasing the latch must not touch the lifecycle');
|
|
257
|
+
});
|
|
258
|
+
|
|
238
259
|
console.log(`\nprovisioning module: ${passed} passed, ${failed} failed`);
|
|
239
260
|
process.exit(failed ? 1 : 0);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.128",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.128",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.128",
|
|
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",
|
|
@@ -228,6 +228,17 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
228
228
|
.prog.isLive .pnote{color:color-mix(in srgb,var(--ok) 75%,var(--ink));font-size:14px;}
|
|
229
229
|
.prog.isErr{background:var(--err-soft);border-color:color-mix(in srgb,var(--err) 25%,var(--bg-card));}
|
|
230
230
|
.prog.isErr .pnote{color:var(--err);}
|
|
231
|
+
/* teardown's calm neutral (F22) — transcribed from cosmos's .strip.flowDown,
|
|
232
|
+
the same .prog-mirrors-.strip duplication as the rest of this block */
|
|
233
|
+
.prog.flowDown{border-color:var(--line);background:linear-gradient(180deg,var(--neutral-soft) 0%,var(--bg-card) 100%);}
|
|
234
|
+
.prog.flowDown .stages li:not(:first-child)::before{background:var(--line);}
|
|
235
|
+
.prog.flowDown .stages li.done:not(:first-child)::before{background:var(--ink-faint);}
|
|
236
|
+
.prog.flowDown .sdot{border-color:var(--line);}
|
|
237
|
+
.prog.flowDown .stages li.done .sdot{border-color:var(--ink-faint);background:var(--ink-faint);}
|
|
238
|
+
.prog.flowDown .stages li.now .sdot{border-color:var(--ink-soft);}
|
|
239
|
+
.prog.flowDown .stages li.now .sdot::after{background:var(--ink-soft);}
|
|
240
|
+
.prog.flowDown .slbl{color:var(--ink-faint);}
|
|
241
|
+
.prog.flowDown .stages li.now .slbl,.prog.flowDown .stages li.done .slbl{color:var(--ink-soft);}
|
|
231
242
|
.again{
|
|
232
243
|
display:inline-block;margin-top:14px;font-size:13px;font-weight:700;color:var(--ink-faint);
|
|
233
244
|
background:none;border:none;cursor:pointer;font-family:var(--font-body);text-decoration:underline;
|
|
@@ -2864,25 +2875,55 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2864
2875
|
var watchPending = false; /* a parked progress watch, resumed on re-entry */
|
|
2865
2876
|
var watchGen = 0; /* same orphaning contract as manifestGen above */
|
|
2866
2877
|
function stopPolling() { watchGen += 1; if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } }
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2878
|
+
/* The done panel's own flow vocabularies (F22). A local mirror of the mine
|
|
2879
|
+
view's FLOWS — that table lives in the OTHER page IIFE, out of scope here,
|
|
2880
|
+
the same isolation that makes .prog duplicate .strip. */
|
|
2881
|
+
var PROG_FLOWS = {
|
|
2882
|
+
standup: { names: ['Request filed', 'Standing it up', 'Live'], cls: '' },
|
|
2883
|
+
revive: { names: ['Revive queued', 'Standing it up', 'Back live'], cls: '' },
|
|
2884
|
+
teardown: { names: ['Teardown queued', 'Winding down', 'Torn down'], cls: 'flowDown' }
|
|
2885
|
+
};
|
|
2886
|
+
function progressMarkup(stageNow, stagesDone, note, cls, flow) {
|
|
2887
|
+
var f = PROG_FLOWS[flow || 'standup'];
|
|
2888
|
+
return '<div class="prog ' + (f.cls ? f.cls + ' ' : '') + (cls || '') + '" role="status" aria-live="polite">' +
|
|
2889
|
+
'<ul class="stages">' + f.names.map(function (n, i) {
|
|
2871
2890
|
var c = stagesDone > i ? 'done' : (stageNow === i ? 'now' : '');
|
|
2872
2891
|
return '<li class="' + c + '"><span class="sdot"></span><span class="slbl">' + n + '</span></li>';
|
|
2873
2892
|
}).join('') + '</ul>' +
|
|
2874
2893
|
'<p class="pnote">' + note + '</p></div>';
|
|
2875
2894
|
}
|
|
2876
2895
|
/* Renders the current instance state; returns true when the state is terminal
|
|
2877
|
-
(no further polling wanted).
|
|
2896
|
+
(no further polling wanted). The strip is chosen by the OPEN INTENT first,
|
|
2897
|
+
then status (F22): a torn-down instance with a queued revive must never be
|
|
2898
|
+
told to "start another project". */
|
|
2878
2899
|
function renderProgress(inst) {
|
|
2879
2900
|
var el = $('liveProgress');
|
|
2880
2901
|
if (!el) return true;
|
|
2881
2902
|
var s = inst && inst.status;
|
|
2903
|
+
var revive = !!(inst && inst.torn_down_at);
|
|
2904
|
+
var oi = inst && inst.open_intent;
|
|
2905
|
+
/* requested/provisioning/tearing_down speak their STATUS (a requested
|
|
2906
|
+
instance always has its provision intent open — the intent branch
|
|
2907
|
+
would make the status branch dead and diverge from the mine card) */
|
|
2908
|
+
if (oi && s !== 'provisioning' && s !== 'tearing_down' && s !== 'requested') {
|
|
2909
|
+
if (oi.action === 'provision') {
|
|
2910
|
+
el.innerHTML = progressMarkup(0, 0,
|
|
2911
|
+
(s === 'torn_down' ? 'A revive is queued' : 'A standup is queued') + ' — this panel updates when it starts.',
|
|
2912
|
+
'', s === 'torn_down' ? 'revive' : 'standup');
|
|
2913
|
+
return false;
|
|
2914
|
+
}
|
|
2915
|
+
if (oi.action === 'teardown') {
|
|
2916
|
+
el.innerHTML = progressMarkup(0, 0, 'A teardown is queued — this panel updates when it starts.', '', 'teardown');
|
|
2917
|
+
return false;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2882
2920
|
if (s === 'requested') {
|
|
2883
|
-
el.innerHTML = progressMarkup(1, 1, 'Waiting for a runner to pick it up. This updates the moment it starts.');
|
|
2921
|
+
el.innerHTML = progressMarkup(1, 1, 'Waiting for a runner to pick it up. This updates the moment it starts.', '', revive ? 'revive' : 'standup');
|
|
2884
2922
|
} else if (s === 'provisioning') {
|
|
2885
|
-
el.innerHTML = progressMarkup(1, 1,
|
|
2923
|
+
el.innerHTML = progressMarkup(1, 1,
|
|
2924
|
+
revive ? 'Bringing it back on the newest platform version — nothing needed from you.'
|
|
2925
|
+
: 'Raising the server, database, and address — nothing needed from you.',
|
|
2926
|
+
'', revive ? 'revive' : 'standup');
|
|
2886
2927
|
} else if (s === 'active') {
|
|
2887
2928
|
var addr = inst.domain ? 'https://' + inst.domain : null;
|
|
2888
2929
|
el.innerHTML = progressMarkup(-1, 3,
|
|
@@ -2894,9 +2935,13 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2894
2935
|
false "keeps retrying" — retries stop, and the retry door is the mine view */
|
|
2895
2936
|
el.innerHTML = progressMarkup(1, 1,
|
|
2896
2937
|
'The standup stopped partway — it won’t continue on its own. Retry it (and see the technical details) from <a href="?view=mine">your projects page</a>.', 'isErr');
|
|
2897
|
-
} else if (s === 'tearing_down'
|
|
2898
|
-
/* a deliberate end is calm, never styled as a failure (direction §5)
|
|
2899
|
-
|
|
2938
|
+
} else if (s === 'tearing_down') {
|
|
2939
|
+
/* a deliberate end is calm, never styled as a failure (direction §5) —
|
|
2940
|
+
and it speaks the teardown vocabulary, not the standup's (F22) */
|
|
2941
|
+
el.innerHTML = progressMarkup(1, 1, 'Taking the server offline. The repository stays yours; ' +
|
|
2942
|
+
(inst.hosting_shape === 'dedicated' ? 'its database goes with the machine.' : 'the database is kept.'), '', 'teardown');
|
|
2943
|
+
} else if (s === 'torn_down') {
|
|
2944
|
+
el.innerHTML = progressMarkup(-1, 3, 'This instance was torn down. Bring it back any time from <a href="?view=mine">your projects page</a>, or start another below.', '', 'teardown');
|
|
2900
2945
|
return true;
|
|
2901
2946
|
} else {
|
|
2902
2947
|
el.innerHTML = progressMarkup(0, 1, 'Filed. Watching for the standup to start…');
|
|
@@ -2936,6 +2981,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2936
2981
|
.then(function (data) {
|
|
2937
2982
|
if (myGen !== watchGen) return;
|
|
2938
2983
|
var inst = data.instance;
|
|
2984
|
+
if (inst) inst.open_intent = data.open_intent || null; /* F22: the strip reads intents */
|
|
2939
2985
|
/* the checklist reflects live state (task 1002694): a status transition —
|
|
2940
2986
|
e.g. the scaffold landing — re-renders the steps (change-only, so the
|
|
2941
2987
|
5s tick never clobbers an open dropdown or mid-flight button) */
|
|
@@ -3198,6 +3244,10 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3198
3244
|
/* domainless instances get their first address; an assigned default may be
|
|
3199
3245
|
upgraded to a bring-your-own domain (the server allows exactly that re-point) */
|
|
3200
3246
|
function canAttachDomain(inst) {
|
|
3247
|
+
/* the re-kick door (F19): an active instance whose address wiring stopped
|
|
3248
|
+
(error_note, no intent in flight) can re-send its own domain — the route
|
|
3249
|
+
re-enqueues the attach for exactly this shape */
|
|
3250
|
+
if (inst.status === 'active' && inst.domain && inst.error_note && !inst.open_intent) return true;
|
|
3201
3251
|
return (!inst.domain || inst.domain === assignedDefaultFor(inst)) &&
|
|
3202
3252
|
(inst.status === 'requested' || inst.status === 'error' || inst.status === 'active');
|
|
3203
3253
|
}
|
|
@@ -3258,7 +3308,8 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3258
3308
|
: 'Raising the server, database, and address — nothing needed from you.');
|
|
3259
3309
|
}
|
|
3260
3310
|
if (s === 'tearing_down') {
|
|
3261
|
-
return stripMarkup('teardown', 1, 1, 'Taking the server offline. The repository stays yours;
|
|
3311
|
+
return stripMarkup('teardown', 1, 1, 'Taking the server offline. The repository stays yours; ' +
|
|
3312
|
+
(inst.hosting_shape === 'dedicated' ? 'its database goes with the machine.' : 'the database is kept.'));
|
|
3262
3313
|
}
|
|
3263
3314
|
if (inst.open_intent) {
|
|
3264
3315
|
var a = inst.open_intent.action;
|
|
@@ -3297,6 +3348,14 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3297
3348
|
/* ── rendering ───────────────────────────────────────────────────────── */
|
|
3298
3349
|
|
|
3299
3350
|
var SHAPE_LABEL = { standalone: 'its own server', 'co-tenant': 'shared server', dedicated: 'its own server' };
|
|
3351
|
+
/* what a teardown ACTUALLY does, per hosting shape (F17 — the DECIDED policy:
|
|
3352
|
+
retention is deliberate on shared boxes; a dedicated machine takes its
|
|
3353
|
+
database with it). Copy follows the runner, never the other way around. */
|
|
3354
|
+
function teardownTruth(inst) {
|
|
3355
|
+
return inst && inst.hosting_shape === 'dedicated'
|
|
3356
|
+
? 'This project has a machine to itself, so the machine goes away and its database goes with it. Your repository is untouched.'
|
|
3357
|
+
: 'The server goes away and the address stops answering. Your repository is untouched. The database is kept, so bringing it back later restores its memory.';
|
|
3358
|
+
}
|
|
3300
3359
|
|
|
3301
3360
|
function addressRow(inst) {
|
|
3302
3361
|
if (inst.domain) {
|
|
@@ -3305,6 +3364,9 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3305
3364
|
heartbeat earns it — never the runner finishing its script */
|
|
3306
3365
|
var link = '<a href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">' + esc(inst.domain) + '</a>';
|
|
3307
3366
|
if (inst.last_verified_up) return 'Answering at ' + link;
|
|
3367
|
+
/* the runner had no DNS hook (F16): the CAUSE beats the bare symptom —
|
|
3368
|
+
a successful A record flips last_verified_up and retires the note */
|
|
3369
|
+
if (inst.dns_note) return 'Its address is ' + link + '. ' + esc(inst.dns_note);
|
|
3308
3370
|
if (inst.last_verified_up === false) return 'Not answering at ' + link + ' right now.';
|
|
3309
3371
|
return 'Its address is ' + link + ' — confirming it answers.';
|
|
3310
3372
|
}
|
|
@@ -3335,20 +3397,28 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3335
3397
|
var addr = addressRow(inst);
|
|
3336
3398
|
if (addr) body += '<div class="addr">' + addr + '</div>';
|
|
3337
3399
|
if (canAttachDomain(inst)) {
|
|
3400
|
+
var rekick = inst.status === 'active' && inst.domain && inst.error_note;
|
|
3338
3401
|
body += '<div class="attach">' +
|
|
3339
|
-
'<input type="text" id="dom-' + iid + '" placeholder="myproject.example.com" autocomplete="off" spellcheck="false" maxlength="253"
|
|
3402
|
+
'<input type="text" id="dom-' + iid + '" placeholder="myproject.example.com" autocomplete="off" spellcheck="false" maxlength="253"' +
|
|
3403
|
+
(rekick ? ' value="' + esc(inst.domain) + '"' : '') + '>' +
|
|
3340
3404
|
'<button type="button" class="btn small" data-attach="' + iid + '">' +
|
|
3341
|
-
(inst.domain ? 'Use your own domain' : 'Give it an address') + '</button></div>';
|
|
3405
|
+
(rekick ? 'Try the address again' : inst.domain ? 'Use your own domain' : 'Give it an address') + '</button></div>';
|
|
3342
3406
|
}
|
|
3343
|
-
if (inst.status === 'error' && !inst.open_intent) {
|
|
3407
|
+
if ((inst.status === 'error' || inst.error_note) && !inst.open_intent) {
|
|
3344
3408
|
/* once a retry is queued the strip tells the story — the stale error
|
|
3345
|
-
paragraph would just contradict it (owner walk, 2026-08-12)
|
|
3409
|
+
paragraph would just contradict it (owner walk, 2026-08-12). Also
|
|
3410
|
+
covers an ACTIVE instance whose intent retired with a note (F19) —
|
|
3411
|
+
the work stopped and the card must say so, not stay silent. */
|
|
3346
3412
|
body += '<div class="projMsg err">' + errMarkup(inst) + '</div>';
|
|
3347
3413
|
}
|
|
3348
3414
|
body += stripFor(inst);
|
|
3349
|
-
if (gone) {
|
|
3415
|
+
if (gone && !inst.open_intent) {
|
|
3416
|
+
/* with a revive queued, the strip tells the story — this paragraph would
|
|
3417
|
+
contradict it (F22, the same guard the error paragraph has) */
|
|
3350
3418
|
body += '<div class="projMsg">Torn down' + (inst.torn_down_at ? ' ' + esc(shortDate(inst.torn_down_at)) : '') +
|
|
3351
|
-
|
|
3419
|
+
(inst.hosting_shape === 'dedicated'
|
|
3420
|
+
? '. The repository is still yours. Bringing it back stands up a fresh machine with an empty database. Or <a href="?view=new">start another</a>.</div>'
|
|
3421
|
+
: '. The repository is still yours, and its database is kept — bringing it back takes one click. Or <a href="?view=new">start another</a>.</div>');
|
|
3352
3422
|
}
|
|
3353
3423
|
var lm = lastMsg[inst.id];
|
|
3354
3424
|
body += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '">' + (lm ? lm.text : '') + '</div>';
|
|
@@ -3522,7 +3592,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3522
3592
|
var act = $('act-' + id);
|
|
3523
3593
|
if (!act) return;
|
|
3524
3594
|
act.innerHTML = '<div class="confirmRow">' +
|
|
3525
|
-
'<span class="plain">Tear it down?
|
|
3595
|
+
'<span class="plain">Tear it down? ' + teardownTruth(byId[id]) + '</span>' +
|
|
3526
3596
|
'<button type="button" class="btn small danger" data-teardown-yes="' + esc(id) + '">Yes, tear it down</button>' +
|
|
3527
3597
|
'<button type="button" class="linklike" data-teardown-no="' + esc(id) + '">Keep it running</button></div>';
|
|
3528
3598
|
}
|