@bongos/core 1.20.82 → 1.21.1
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 +154 -64
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0051-full-session-transcript-corpus.md +3 -1
- package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +3 -3
- package/docs/adr/README.md +1 -1
- package/docs/api/openapi.json +153 -4
- package/docs/api-reference.md +5 -3
- package/docs/architecture.md +3 -0
- package/docs/copy-inventory.md +10 -10
- package/docs/copy-registry.json +12 -12
- package/docs/design/project-startup-direction.md +63 -0
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +8 -0
- package/docs/page-inventory.json +8 -3
- package/docs/page-readings.json +382 -373
- package/docs/recipes/private-npm-distribution.md +1 -1
- package/migrations/core_269_session_transcripts.sql +35 -0
- package/modules/hall-ui/public/genesis-home.js +17 -3
- package/modules/hall-ui/public/index-genesis-demo.states.json +13 -0
- package/modules/lifecycle/done-when.js +82 -42
- package/modules/lifecycle/workflow-dispatch.js +21 -11
- package/modules/npm-release/release.js +10 -1
- package/modules/onboarding/founding-birth.js +116 -0
- package/modules/onboarding/genesis-home.js +72 -5
- package/modules/onboarding/port.js +9 -2
- package/modules/onboarding/routes/onboarding.js +8 -1
- package/modules/provisioning/birth.js +64 -0
- package/modules/provisioning/dns-resolves.js +54 -0
- package/modules/provisioning/migrations/provisioning_036_dns_resolved.sql +30 -0
- package/modules/provisioning/pollers/liveness-sweep.js +5 -0
- package/modules/provisioning/provisioning.js +7 -6
- package/modules/provisioning/routes/provisioning.js +6 -2
- package/modules/provisioning/tests/liveness.mjs +48 -0
- package/modules/public-landing/public/projects-dns-pending.states.json +85 -0
- package/modules/public-landing/public/projects.html +40 -15
- package/modules/sessions/db.js +58 -0
- package/modules/sessions/routes/sessions.js +97 -0
- package/modules/ui-design/kit/fixtures/founding-genesis-demo.json +176 -0
- package/modules/ui-design/kit/fixtures/me-founding-demo.json +27 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances-demo.json +3 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances-dns-pending.json +49 -0
- package/modules/ui-design/kit/fixtures/provisioning-instances.json +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +41 -0
- package/scripts/gds/provision.js +77 -53
- package/scripts/gds/session-digest.js +11 -3
- package/scripts/gds/session-transcript-build.js +155 -0
- package/scripts/gds/session-transcript-upload.js +29 -0
- package/scripts/gds/ship-session-upload.js +6 -1
- package/scripts/gds/upgrade-commit-identity.js +32 -0
- package/scripts/gds/upgrade.js +1 -1
- package/src/bongos/routes.js +3 -0
- package/src/branding.js +9 -0
- package/src/module-api.js +1 -1
- package/tests/auto_satisfy_criteria.mjs +20 -0
- package/tests/founding_birth.mjs +200 -0
- package/tests/founding_mode.mjs +3 -1
- package/tests/genesis_home.mjs +71 -2
- package/tests/goal_closure_invariants.mjs +68 -0
- package/tests/hub_map_orb_states.mjs +5 -1
- package/tests/npm_release_release.mjs +37 -2
- package/tests/projects_hub.mjs +5 -3
- package/tests/projects_hub_dns_ready.mjs +153 -0
- package/tests/projects_hub_pre_uat.mjs +9 -5
- package/tests/provision_dns_first.mjs +171 -0
- package/tests/provision_settings_apply.mjs +6 -1
- package/tests/provisioning_settings_env.mjs +49 -0
- package/tests/session_transcript.mjs +273 -0
- package/tests/ui_design_kit.mjs +1 -1
- package/tests/upgrade_commit_identity.mjs +89 -0
- package/tests/wizard_demo.mjs +29 -3
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/provisioning/birth.js — the BIRTH MARKER: this project was made through the
|
|
4
|
+
// startup flow, so it is born into genesis (task 1004506 / BV2.PS18; spec
|
|
5
|
+
// docs/specs/bongos-v2-project-startup.md step 5 + D8).
|
|
6
|
+
//
|
|
7
|
+
// Founding mode lives in the NEW INSTANCE's own project_settings, and this process (the
|
|
8
|
+
// hub's web tier) has no way into that database — the one rule of this module. So the
|
|
9
|
+
// hub only says so, and the instance acts on it (modules/onboarding/founding-birth.js):
|
|
10
|
+
//
|
|
11
|
+
// * the create route stamps `founding_birth` (a reserved settings-jsonb key, like
|
|
12
|
+
// push_owed and look_accent: platform bookkeeping, out of SETTINGS_VOCAB, so the
|
|
13
|
+
// settings PATCH cannot write it and effectiveSettings never projects it) on the
|
|
14
|
+
// row it JUST created — never on one that already existed, so a project made before
|
|
15
|
+
// this shipped is never born into anything (D8);
|
|
16
|
+
// * the companions below carry it, and a demo's time box, into the instance's
|
|
17
|
+
// web.env beside the other settings, at standup and on every settings-apply push.
|
|
18
|
+
//
|
|
19
|
+
// Always emitted, empty meaning "not born through the flow" / "not a demo", for the
|
|
20
|
+
// same reason as every companion: the runner patches these lines in place, and a key
|
|
21
|
+
// that came and went would leave a stale line behind.
|
|
22
|
+
|
|
23
|
+
const BIRTH_KEY = 'founding_birth';
|
|
24
|
+
const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
25
|
+
|
|
26
|
+
const storedOf = (row) => (row && row.settings && typeof row.settings === 'object' && !Array.isArray(row.settings) ? row.settings : {});
|
|
27
|
+
|
|
28
|
+
// bornAt(row) — PURE. The birth stamp, or '' for a row that has none (or a mangled one).
|
|
29
|
+
function bornAt(row) {
|
|
30
|
+
const v = String(storedOf(row)[BIRTH_KEY] || '');
|
|
31
|
+
return ISO.test(v) && Number.isFinite(new Date(v).getTime()) ? v : '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// demoBox(row) — PURE. A born demo's { people, hours_each }, or null. Only a row that is
|
|
35
|
+
// both born and a demo carries one: an older demo has no marker and founds nothing.
|
|
36
|
+
function demoBox(row) {
|
|
37
|
+
if (!bornAt(row)) return null;
|
|
38
|
+
const d = row && row.demo;
|
|
39
|
+
if (!d || typeof d !== 'object') return null;
|
|
40
|
+
const people = Number(d.people);
|
|
41
|
+
const hours = Number(d.hours_each);
|
|
42
|
+
if (!Number.isInteger(people) || !Number.isInteger(hours) || people < 1 || hours < 1) return null;
|
|
43
|
+
return { people, hours_each: hours };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The companion env (SETTINGS_COMPANION_ENV). src/branding.js maps them onto founding.*.
|
|
47
|
+
const BIRTH_COMPANION_ENV = Object.freeze({
|
|
48
|
+
FOUNDING_BIRTH: bornAt,
|
|
49
|
+
DEMO_PEOPLE: (row) => { const d = demoBox(row); return d ? String(d.people) : ''; },
|
|
50
|
+
DEMO_HOURS: (row) => { const d = demoBox(row); return d ? String(d.hours_each) : ''; },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// stampBirth(db, instanceId, now) — the create route's one write, on the row it just
|
|
54
|
+
// made. Write-once: a stamp already there is kept (the WHERE says so).
|
|
55
|
+
async function stampBirth(db, instanceId, now = new Date()) {
|
|
56
|
+
await db.query(
|
|
57
|
+
`UPDATE provisioning_instances
|
|
58
|
+
SET settings = COALESCE(settings, '{}'::jsonb) || jsonb_build_object($2::text, $3::text)
|
|
59
|
+
WHERE id = $1 AND NOT (COALESCE(settings, '{}'::jsonb) ? $2::text)`,
|
|
60
|
+
[instanceId, BIRTH_KEY, now.toISOString()],
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { BIRTH_KEY, BIRTH_COMPANION_ENV, stampBirth };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/provisioning/dns-resolves.js — has the platform's own lookup seen this
|
|
4
|
+
// project's address answer? (task 1004505, BV2.PS17; migration provisioning_036)
|
|
5
|
+
//
|
|
6
|
+
// WHY. A visitor's resolver caches a "not found" answer for the zone's negative TTL
|
|
7
|
+
// (cloudbongos.com's SOA minimum is 1800s). The hub used to link an address as soon as
|
|
8
|
+
// the row read 'active', so a founder who clicked before the name answered anywhere
|
|
9
|
+
// could not reach their own project from their home network for up to half an hour.
|
|
10
|
+
// The fix has two halves: the runner creates the DNS record FIRST (scripts/gds/
|
|
11
|
+
// provision.js), and the hub holds every link to the address until `dns_resolves` is
|
|
12
|
+
// true — a fact only a lookup the PLATFORM made can establish, never the status alone.
|
|
13
|
+
//
|
|
14
|
+
// Who writes it: the runner, at the end of a standup (minutes after the record was
|
|
15
|
+
// created, so its own lookup cannot race the record into a cached miss), and the
|
|
16
|
+
// liveness sweep, whose SSRF gate already resolves every active address. Sticky once
|
|
17
|
+
// seen; a changed domain clears it (updateInstanceDomain in ./provisioning.js).
|
|
18
|
+
// Its own file because ./provisioning.js sits at the 1500-line size ratchet.
|
|
19
|
+
|
|
20
|
+
const dns = require('node:dns').promises;
|
|
21
|
+
|
|
22
|
+
// The projection publicInstance carries. PURE. No domain ⇒ nothing to resolve.
|
|
23
|
+
function dnsResolves(row) {
|
|
24
|
+
return !!(row && row.domain && row.dns_resolved_at);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Record that a lookup answered. Narrow like recordDnsNote: no `status`, no
|
|
28
|
+
// `updated_at`, and only the FIRST sighting writes, so a sweep never rewrites it.
|
|
29
|
+
async function recordDnsResolved(db, id) {
|
|
30
|
+
const { rows } = await db.query(
|
|
31
|
+
`UPDATE provisioning_instances SET dns_resolved_at = now()
|
|
32
|
+
WHERE id = $1 AND dns_resolved_at IS NULL RETURNING id, dns_resolved_at`,
|
|
33
|
+
[id]
|
|
34
|
+
);
|
|
35
|
+
return rows[0] || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// One bounded lookup: true when the name resolves to at least one address, false on
|
|
39
|
+
// any failure or after `timeoutMs`. Never throws. `lookup` is injectable for tests.
|
|
40
|
+
async function lookupAnswers(host, { lookup, timeoutMs = 5000 } = {}) {
|
|
41
|
+
if (!host) return false;
|
|
42
|
+
let timer = null;
|
|
43
|
+
const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve(false), timeoutMs); });
|
|
44
|
+
try {
|
|
45
|
+
const ask = Promise.resolve((lookup || dns.lookup)(host, { all: true }))
|
|
46
|
+
.then((addrs) => Array.isArray(addrs) ? addrs.some((a) => a && a.address) : !!(addrs && addrs.address))
|
|
47
|
+
.catch(() => false);
|
|
48
|
+
return await Promise.race([ask, timeout]);
|
|
49
|
+
} finally {
|
|
50
|
+
if (timer) clearTimeout(timer);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { dnsResolves, recordDnsResolved, lookupAnswers };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- provisioning_036_dns_resolved.sql — when the platform's own lookup first saw a
|
|
2
|
+
-- project's address answer (task 1004505, BV2.PS17).
|
|
3
|
+
--
|
|
4
|
+
-- WHY THIS EXISTS. A visitor's resolver caches a "not found" answer for the zone's
|
|
5
|
+
-- negative TTL (30 minutes on cloudbongos.com). The hub used to link a project's
|
|
6
|
+
-- address as soon as the row said 'active', and the founder's first click, made
|
|
7
|
+
-- before the name answered anywhere, poisoned their own home network for half an
|
|
8
|
+
-- hour. The hub now holds every link to the address until this column is set.
|
|
9
|
+
--
|
|
10
|
+
-- WHAT IT ADDS.
|
|
11
|
+
-- provisioning_instances.dns_resolved_at — NULL until a lookup made by the
|
|
12
|
+
-- platform (the runner at the end of a standup, or the liveness sweep's gate)
|
|
13
|
+
-- returned an address for the row's domain. Projected as `dns_resolves`.
|
|
14
|
+
-- A changed domain clears it (updateInstanceDomain), like the liveness overlay.
|
|
15
|
+
--
|
|
16
|
+
-- OVERLAY, NOT A STATE (the provisioning_006 rule): nullable, no default, never
|
|
17
|
+
-- written by the lifecycle. The backfill marks every address the sweep has
|
|
18
|
+
-- already seen answer, so no live project loses its link when this lands.
|
|
19
|
+
--
|
|
20
|
+
-- Additive and namespaced (ADR 0083): no down-migration, idempotent — safe to re-run.
|
|
21
|
+
|
|
22
|
+
BEGIN;
|
|
23
|
+
|
|
24
|
+
ALTER TABLE provisioning_instances ADD COLUMN IF NOT EXISTS dns_resolved_at timestamptz;
|
|
25
|
+
|
|
26
|
+
UPDATE provisioning_instances
|
|
27
|
+
SET dns_resolved_at = last_verified_at
|
|
28
|
+
WHERE dns_resolved_at IS NULL AND last_verified_up IS TRUE AND domain IS NOT NULL;
|
|
29
|
+
|
|
30
|
+
COMMIT;
|
|
@@ -224,6 +224,11 @@ async function sweepOnce(deps = {}) {
|
|
|
224
224
|
}
|
|
225
225
|
const probe = await probeHealthz(url, { timeoutMs: deps.timeoutMs, fetchImpl: deps.fetchImpl, pin });
|
|
226
226
|
const rec = await provisioning.recordLivenessProbe(db, row.id, probe.up);
|
|
227
|
+
// The gate's lookup IS the platform's own lookup (task 1004505): an address it
|
|
228
|
+
// resolved, or one that answered, may now be linked on the hub. First sighting only.
|
|
229
|
+
if (row.domain && !row.dns_resolved_at && (pin || probe.up)) {
|
|
230
|
+
await provisioning.recordDnsResolved(db, row.id).catch((e) => log(`[liveness] dns record failed (non-blocking): ${e && e.message ? e.message : e}`));
|
|
231
|
+
}
|
|
227
232
|
return { row, probed: true, prevUp: rec ? rec.prev_up : null, ...probe };
|
|
228
233
|
}));
|
|
229
234
|
|
|
@@ -24,7 +24,8 @@ const { normalizeModuleSelection, effectiveModules } = require('./starter-bundle
|
|
|
24
24
|
// projection both have to read them.
|
|
25
25
|
const { PLANET_PHYSICS_QUESTIONS, canonicalAnswer } = require('./planet-physics');
|
|
26
26
|
const { softLimitsFor } = require('./soft-limits'); const { demoView } = require('./demo'); // the demo marker's owner view (task 1004420)
|
|
27
|
-
const {
|
|
27
|
+
const { dnsResolves, recordDnsResolved, lookupAnswers } = require('./dns-resolves'); // has the platform's lookup seen the address answer (task 1004505)
|
|
28
|
+
const { LOOK_SETTINGS_VOCAB, LOOK_DEFAULTS, LOOK_COMPANION_ENV } = require('./look'); const { BIRTH_COMPANION_ENV } = require('./birth'); // born into genesis (task 1004506)
|
|
28
29
|
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
30
31
|
// Constants (mirror the CHECK constraints in provisioning_001_tables.sql)
|
|
@@ -466,7 +467,7 @@ function publicInstance(row) {
|
|
|
466
467
|
last_verified_up: row.last_verified_up == null ? null : !!row.last_verified_up,
|
|
467
468
|
// The outstanding manual-DNS step, if the runner had no hook to point the
|
|
468
469
|
// A record (task 1002720 / F16). null = nothing outstanding.
|
|
469
|
-
dns_note: row.dns_note || null,
|
|
470
|
+
dns_note: row.dns_note || null, dns_resolves: dnsResolves(row), // the platform's lookup saw it answer; the hub links it only then (task 1004505)
|
|
470
471
|
cost_estimate_usd: Number(row.cost_estimate_usd || 0),
|
|
471
472
|
error_note: row.error_note || null,
|
|
472
473
|
created_at: row.created_at,
|
|
@@ -518,7 +519,7 @@ function settingsEnvVars(row, prefix) {
|
|
|
518
519
|
// cutoff and every unresolved review counts, which is the right answer for a
|
|
519
520
|
// project that has been strict since it stood up.
|
|
520
521
|
const SETTINGS_COMPANION_ENV = Object.freeze({
|
|
521
|
-
ARTIST_GATE_SINCE: (row) => artistGateStrictSince(row) || '', ...LOOK_COMPANION_ENV, // + accent, logo (look.js)
|
|
522
|
+
ARTIST_GATE_SINCE: (row) => artistGateStrictSince(row) || '', ...LOOK_COMPANION_ENV, ...BIRTH_COMPANION_ENV, // + accent, logo (look.js); birth, demo box (birth.js)
|
|
522
523
|
});
|
|
523
524
|
|
|
524
525
|
// The web.env block the runner writes at STANDUP (task 1003139): a comment that
|
|
@@ -974,7 +975,7 @@ async function updateInstanceDomain(db, id, domain, releasing = null) {
|
|
|
974
975
|
ELSE array_append(released_domains, $3)
|
|
975
976
|
END,
|
|
976
977
|
last_verified_at = CASE WHEN domain IS DISTINCT FROM $2 THEN NULL ELSE last_verified_at END,
|
|
977
|
-
last_verified_up = CASE WHEN domain IS DISTINCT FROM $2 THEN NULL ELSE last_verified_up END,
|
|
978
|
+
last_verified_up = CASE WHEN domain IS DISTINCT FROM $2 THEN NULL ELSE last_verified_up END, dns_resolved_at = CASE WHEN domain IS DISTINCT FROM $2 THEN NULL ELSE dns_resolved_at END,
|
|
978
979
|
updated_at = now()
|
|
979
980
|
WHERE id = $1 RETURNING *`,
|
|
980
981
|
[id, domain, host, MAX_RELEASED_DOMAINS]
|
|
@@ -1004,7 +1005,7 @@ async function clearReleasedDomains(db, id) {
|
|
|
1004
1005
|
// against it would only ever record a false "down". `limit` is a runaway guard.
|
|
1005
1006
|
async function listActiveInstancesForLiveness(db, { limit = 500 } = {}) {
|
|
1006
1007
|
const { rows } = await db.query(
|
|
1007
|
-
`SELECT id, slug, domain, port, settings FROM provisioning_instances
|
|
1008
|
+
`SELECT id, slug, domain, port, settings, dns_resolved_at FROM provisioning_instances
|
|
1008
1009
|
WHERE status = 'active'
|
|
1009
1010
|
ORDER BY id
|
|
1010
1011
|
LIMIT $1`,
|
|
@@ -1486,7 +1487,7 @@ module.exports = {
|
|
|
1486
1487
|
isProvisionedHostname,
|
|
1487
1488
|
getInstanceById, getInstanceBySlug, getInstanceByDomain, listInstancesForOwner, listAllInstances,
|
|
1488
1489
|
// data access — verification overlay (task 1002696)
|
|
1489
|
-
listActiveInstancesForLiveness, recordLivenessProbe, recordDnsNote, clearErrorNote, ownsErrorNote,
|
|
1490
|
+
listActiveInstancesForLiveness, recordLivenessProbe, recordDnsNote, clearErrorNote, ownsErrorNote, dnsResolves, recordDnsResolved, lookupAnswers,
|
|
1490
1491
|
// data access — the owed-push marker (task 1003524)
|
|
1491
1492
|
recordSettingsPushOwed,
|
|
1492
1493
|
createInstance, setInstanceStatus, updateInstanceDomain, clearReleasedDomains, updateInstanceSettings, updateInstanceDetail,
|
|
@@ -64,7 +64,7 @@ const catalogBridge = require('../catalog-bridge');
|
|
|
64
64
|
const capacity = require('../capacity'); // the box's slot ceiling (ADR 0285, task 1003927)
|
|
65
65
|
const { callbackPage } = require('./callback-page');
|
|
66
66
|
// the LOUD body validators (task 1003504) — lifted out when this file hit the size ratchet
|
|
67
|
-
const { badProjectDetail, badModuleSelection, badProjectType, screenedOut } = require('./body-validators'); const demo = require('../demo');
|
|
67
|
+
const { badProjectDetail, badModuleSelection, badProjectType, screenedOut } = require('./body-validators'); const demo = require('../demo'); const birth = require('../birth');
|
|
68
68
|
const { failFrom } = require('../public-refusal'); // every catch: a declared refusal reaches the owner, anything else stays opaque (task 1004126)
|
|
69
69
|
const { settingsPush, failPushConflict } = require('../settings-push'); // a settings save's push + receipt, shared with routes/look.js (task 1004421)
|
|
70
70
|
// the kernel ports this module provides (moved out at the size ratchet, task 1003578)
|
|
@@ -471,6 +471,11 @@ module.exports = function provisioningRoutes() {
|
|
|
471
471
|
if (!created && String(instance.owner_builder_id) !== String(req.builder.id)) {
|
|
472
472
|
return res.fail('slug_taken', { status: 409, message: 'That instance slug is already in use.' });
|
|
473
473
|
}
|
|
474
|
+
// Born into genesis (task 1004506, ../birth.js): only the row this request MADE, and before the
|
|
475
|
+
// provision intent exists, so the runner's web.env already carries the marker and the demo's box.
|
|
476
|
+
// Best-effort: a project that misses its marker opens as an ordinary hall; the create still stands.
|
|
477
|
+
if (created) await birth.stampBirth(pool, instance.id).catch((err) => log.error('[provisioning] birth marker not stamped', err));
|
|
478
|
+
const demoRow = created && demoAsk.demo ? await demo.startDemoProject(pool, provisioning, instance.id, demoAsk.demo) : null;
|
|
474
479
|
// Enqueue the provision intent (idempotent: an already-open intent is returned)
|
|
475
480
|
// — but ONLY when the instance actually needs standing up. Blindly enqueueing on
|
|
476
481
|
// a re-request of an ACTIVE instance left a stale open provision intent that then
|
|
@@ -493,7 +498,6 @@ module.exports = function provisioningRoutes() {
|
|
|
493
498
|
// description exists — the publish gate alone decides that — but the row has to be
|
|
494
499
|
// there for the gate to have anything to decide about, and for the wizard's invite
|
|
495
500
|
// step to have a project to invite into while standup is still running.
|
|
496
|
-
const demoRow = created && demoAsk.demo ? await demo.startDemoProject(pool, provisioning, instance.id, demoAsk.demo) : null;
|
|
497
501
|
await catalogBridge.publishInstanceToCatalog(pool, demoRow || instance).catch(() => {});
|
|
498
502
|
// A re-request cannot change an existing instance's domain — SAY so instead of
|
|
499
503
|
// silently dropping it (task 1002692); the domain route is the real path.
|
|
@@ -234,6 +234,54 @@ await ta('an unresolvable domain still probes and records an honest DOWN (the qa
|
|
|
234
234
|
assert.deepEqual({ blocked: out.blocked, down: out.down }, { blocked: 0, down: 1 });
|
|
235
235
|
assert.deepEqual(out.reasons, ['ghostly:ENOTFOUND']);
|
|
236
236
|
assert.deepEqual(db.updates.map((u) => u.up), [false], 'DNS-less domain = honest down, not a silent skip');
|
|
237
|
+
assert.ok(!db.updates.some((u) => /dns_resolved_at/.test(u.sql)), 'an address that did not resolve is never marked as resolving');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
console.log('\ndns_resolves (task 1004505 — the hub links an address only once the platform saw it answer):');
|
|
241
|
+
|
|
242
|
+
const isDnsWrite = (u) => /SET dns_resolved_at = now\(\)/.test(u.sql);
|
|
243
|
+
|
|
244
|
+
await ta('a domain the gate RESOLVED is recorded as resolving — once, even when the probe itself is down', async () => {
|
|
245
|
+
const db = fakeDb([{ id: 9, slug: 'fresh', domain: 'fresh.example.com', port: null, dns_resolved_at: null }]);
|
|
246
|
+
await liveness.sweepOnce({
|
|
247
|
+
pool: db, log: () => {},
|
|
248
|
+
lookup: async () => [{ address: '93.184.216.34', family: 4 }],
|
|
249
|
+
fetchImpl: async () => { throw Object.assign(new TypeError('fetch failed'), { cause: { code: 'ECONNREFUSED' } }); },
|
|
250
|
+
});
|
|
251
|
+
const writes = db.updates.filter(isDnsWrite);
|
|
252
|
+
assert.equal(writes.length, 1, 'the lookup answered, so the address may be linked');
|
|
253
|
+
assert.equal(writes[0].id, 9);
|
|
254
|
+
assert.match(writes[0].sql, /dns_resolved_at IS NULL/, 'only the first sighting writes');
|
|
255
|
+
assert.ok(!/status|updated_at/.test(writes[0].sql), 'an overlay: no status, no updated_at');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await ta('an address already marked is not written again', async () => {
|
|
259
|
+
const db = fakeDb([{ id: 10, slug: 'old', domain: 'old.example.com', port: null, dns_resolved_at: '2026-10-01T10:00:00Z' }]);
|
|
260
|
+
await liveness.sweepOnce({
|
|
261
|
+
pool: db, log: () => {},
|
|
262
|
+
lookup: async () => [{ address: '93.184.216.34', family: 4 }],
|
|
263
|
+
fetchImpl: async () => new Response('ok', { status: 200 }),
|
|
264
|
+
});
|
|
265
|
+
assert.equal(db.updates.filter(isDnsWrite).length, 0);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
t('publicInstance projects dns_resolves: false without a sighting or a domain, true with both', () => {
|
|
269
|
+
assert.equal(provisioning.publicInstance({ id: 1, slug: 'a', status: 'active', domain: 'a.example.com' }).dns_resolves, false);
|
|
270
|
+
assert.equal(provisioning.publicInstance({ id: 1, slug: 'a', status: 'active', domain: 'a.example.com', dns_resolved_at: '2026-10-01T10:00:00Z' }).dns_resolves, true);
|
|
271
|
+
assert.equal(provisioning.publicInstance({ id: 1, slug: 'a', status: 'active', domain: null, dns_resolved_at: '2026-10-01T10:00:00Z' }).dns_resolves, false);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await ta('lookupAnswers: an address is an answer; a miss, a throw or a hang is not', async () => {
|
|
275
|
+
assert.equal(await provisioning.lookupAnswers('a.example.com', { lookup: async () => [{ address: '1.2.3.4', family: 4 }] }), true);
|
|
276
|
+
assert.equal(await provisioning.lookupAnswers('a.example.com', { lookup: async () => [] }), false);
|
|
277
|
+
assert.equal(await provisioning.lookupAnswers('a.example.com', { lookup: async () => { throw new Error('ENOTFOUND'); } }), false);
|
|
278
|
+
assert.equal(await provisioning.lookupAnswers('a.example.com', { lookup: () => new Promise(() => {}), timeoutMs: 20 }), false);
|
|
279
|
+
assert.equal(await provisioning.lookupAnswers('', {}), false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
t('a changed domain clears the sighting with the rest of the overlay', () => {
|
|
283
|
+
const src = provisioning.updateInstanceDomain.toString();
|
|
284
|
+
assert.match(src, /dns_resolved_at = CASE WHEN domain IS DISTINCT FROM \$2 THEN NULL ELSE dns_resolved_at END/);
|
|
237
285
|
});
|
|
238
286
|
|
|
239
287
|
await ta('the gate hands back the address it vetted, so the probe can pin to it (no re-resolution)', async () => {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "Projects whose address does not resolve yet (task 1004505): the same page, the same stub, the instance list aliased to two projects the platform's own lookup has not seen answer. The mine rows and the manage page say the address is being set up and carry no link to it — a click before the name exists is cached as not found for up to half an hour. Rendered by name: node modules/ui-design/kit/render.js --states public-landing/projects-dns-pending.states.json",
|
|
3
|
+
"page": "/projects",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"modes": [
|
|
6
|
+
"dark"
|
|
7
|
+
],
|
|
8
|
+
"modeQuery": false,
|
|
9
|
+
"stub": {
|
|
10
|
+
"fixtures": {
|
|
11
|
+
"provisioning-instances": "provisioning-instances-dns-pending"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"ignoreRequests": [
|
|
15
|
+
"/api/(gds|bongos)/me$",
|
|
16
|
+
"/my-projects",
|
|
17
|
+
"/provisioning/onboard-plan",
|
|
18
|
+
"/env-manifest$"
|
|
19
|
+
],
|
|
20
|
+
"ignoreConsole": [
|
|
21
|
+
"/api/(gds|bongos)/me",
|
|
22
|
+
"\\b401\\b",
|
|
23
|
+
"\\b404\\b",
|
|
24
|
+
"Unauthorized"
|
|
25
|
+
],
|
|
26
|
+
"ignoreControls": [
|
|
27
|
+
".skip",
|
|
28
|
+
".vh"
|
|
29
|
+
],
|
|
30
|
+
"states": {
|
|
31
|
+
"in-mine-dns-pending": {
|
|
32
|
+
"auth": true,
|
|
33
|
+
"url": "/projects?view=mine",
|
|
34
|
+
"expect": {
|
|
35
|
+
"visible": [
|
|
36
|
+
"#projectsList .proj .addr"
|
|
37
|
+
],
|
|
38
|
+
"count": [
|
|
39
|
+
[
|
|
40
|
+
"#projectsList .proj",
|
|
41
|
+
2
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
"#projectsList .proj .actions a",
|
|
45
|
+
2
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
"#projectsList a[href^=\"https://\"]",
|
|
49
|
+
0
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
"#projectsList a.worldOrb",
|
|
53
|
+
0
|
|
54
|
+
]
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"in-manage-dns-pending": {
|
|
59
|
+
"auth": true,
|
|
60
|
+
"url": "/projects?view=manage&id=106",
|
|
61
|
+
"expect": {
|
|
62
|
+
"visible": [
|
|
63
|
+
"#mMain",
|
|
64
|
+
"#mAddr"
|
|
65
|
+
],
|
|
66
|
+
"count": [
|
|
67
|
+
[
|
|
68
|
+
"#mAddr a",
|
|
69
|
+
0
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
"#mActs a[href^=\"https://\"]",
|
|
73
|
+
0
|
|
74
|
+
]
|
|
75
|
+
],
|
|
76
|
+
"text": [
|
|
77
|
+
[
|
|
78
|
+
"#mName",
|
|
79
|
+
"beacon"
|
|
80
|
+
]
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -3193,8 +3193,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3193
3193
|
'<div class="runs"><span class="rLbl">runs</span>' + tagsHtml(p.tags) + '</div>' +
|
|
3194
3194
|
/* at most two doors (task 1003356): the site, and the hall where the
|
|
3195
3195
|
viewer builds there and the hall answers at a different address */
|
|
3196
|
-
'<div class="rowDoors">' + (
|
|
3197
|
-
? '<a class="rowGo" href="' + esc(
|
|
3196
|
+
'<div class="rowDoors">' + (siteHref(p)
|
|
3197
|
+
? '<a class="rowGo" href="' + esc(siteHref(p)) + '" target="_blank" rel="noopener">view</a>'
|
|
3198
3198
|
: '') + hallGlyphHtml(p, rel) + '</div>' +
|
|
3199
3199
|
'</li>';
|
|
3200
3200
|
}).join(''));
|
|
@@ -3413,8 +3413,12 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3413
3413
|
var HALL_GLYPH = '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M2.8 7.4 10 3.5l7.2 3.9"/><path d="M5.2 8.8v5.6M10 8.8v5.6M14.8 8.8v5.6"/><path d="M3.4 16.3h13.2"/></svg>';
|
|
3414
3414
|
function isMember(rel) { return rel === 'owner' || rel === 'builder'; }
|
|
3415
3415
|
/* the row's hall glyph: a member's door, only where the hall is elsewhere */
|
|
3416
|
+
/* the site door for a map entry: one of the viewer's own projects gets none until
|
|
3417
|
+
its address answers (task 1004505, addressReady) */
|
|
3418
|
+
function siteHref(p) { return p._inst && !addressReady(p._inst) ? '' : safeHttpUrl(p.origin); }
|
|
3416
3419
|
function hallGlyphHtml(p, rel) {
|
|
3417
3420
|
var hall = isMember(rel) ? hallUrlOf(p) : '';
|
|
3421
|
+
if (p._inst && !addressReady(p._inst)) hall = ''; /* your own project's hall waits for its name too (task 1004505) */
|
|
3418
3422
|
return hall
|
|
3419
3423
|
? '<a class="rowHall" href="' + esc(hall) + '" target="_blank" rel="noopener" aria-label="Builders\' hall" title="Builders\' hall">' + HALL_GLYPH + '</a>'
|
|
3420
3424
|
: '';
|
|
@@ -3472,7 +3476,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3472
3476
|
}
|
|
3473
3477
|
|
|
3474
3478
|
function openCard(p, rel, returnTo) {
|
|
3475
|
-
var href = p.isSelf ? '/' :
|
|
3479
|
+
var href = p.isSelf ? '/' : siteHref(p);
|
|
3476
3480
|
/* the art: catalog text an Archon typed, scheme-checked server-side and
|
|
3477
3481
|
again here before it becomes a src */
|
|
3478
3482
|
cardArt(p.isSelf ? '' : (backdropPlate(p.card_backdrop) || safeHttpUrl(p.art_url)));
|
|
@@ -6937,7 +6941,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6937
6941
|
return '<a href="' + (instanceId ? '?view=manage&id=' + encodeURIComponent(instanceId) : '?view=mine') +
|
|
6938
6942
|
'">' + label + '</a>';
|
|
6939
6943
|
}
|
|
6940
|
-
var live = { instStatus: null, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
|
|
6944
|
+
var live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
|
|
6941
6945
|
var planPromises = {}; /* mode|domain → Promise<steps[]> */
|
|
6942
6946
|
var lastStepsHtml = '';
|
|
6943
6947
|
var lastProgHtml = ''; /* same change-only contract, for the progress strip */
|
|
@@ -7083,7 +7087,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7083
7087
|
/* the door opens only when the project ANSWERS and sign-in is wired: a
|
|
7084
7088
|
link to an address that does not resolve yet is the worst first
|
|
7085
7089
|
impression a founder can get of their own project (task 1003232) */
|
|
7086
|
-
if (live.instStatus !== 'active' || live.manifest !== 'done') {
|
|
7090
|
+
if (live.instStatus !== 'active' || !live.addrReady || live.manifest !== 'done') {
|
|
7087
7091
|
return '<li class="slocked" data-onboard-step="first-sign-in"><b>First sign-in</b><p>Unlocks once your project is answering and sign-in above is wired. Then you’ll open <code style="font-family:var(--font-mono)">' + esc(domain) + '</code>, sign in with GitHub once, and be seated as the owner — the founding Archon.</p></li>';
|
|
7088
7092
|
}
|
|
7089
7093
|
return '<li data-onboard-step="first-sign-in"><b>First sign-in</b><p>Open <a href="https://' + esc(domain) + '" target="_blank" rel="noopener"><code style="font-family:var(--font-mono)">' + esc(domain) + '</code></a> and sign in with GitHub once. That seats you as the owner — the founding Archon.</p></li>';
|
|
@@ -7091,7 +7095,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7091
7095
|
|
|
7092
7096
|
function renderStepEnterHall() {
|
|
7093
7097
|
var domain = resolvedDomain();
|
|
7094
|
-
if (domain && (live.instStatus !== 'active' || live.manifest !== 'done')) {
|
|
7098
|
+
if (domain && (live.instStatus !== 'active' || !live.addrReady || live.manifest !== 'done')) {
|
|
7095
7099
|
return '<li class="slocked" data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>Unlocks with the first sign-in — your project’s home will be <code style="font-family:var(--font-mono)">' + esc(domain) + '/builders</code>.</p></li>';
|
|
7096
7100
|
}
|
|
7097
7101
|
return domain
|
|
@@ -7383,6 +7387,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7383
7387
|
projectPageLink('its own page') + '; the steps below that need one unlock then.', '', flow));
|
|
7384
7388
|
return true;
|
|
7385
7389
|
}
|
|
7390
|
+
/* task 1004505: no link until the platform's own lookup has seen the name
|
|
7391
|
+
answer. A founder's first click on a name that does not resolve yet is
|
|
7392
|
+
cached as "not found" by their resolver for up to half an hour. */
|
|
7393
|
+
if (!addressReady(inst)) {
|
|
7394
|
+
setProgress(progressMarkup(2, 2,
|
|
7395
|
+
'The build finished — your address is being set up. This panel links it the moment it answers.', '', flow));
|
|
7396
|
+
return false;
|
|
7397
|
+
}
|
|
7386
7398
|
if (inst.last_verified_up === false) {
|
|
7387
7399
|
setProgress(progressMarkup(2, 2,
|
|
7388
7400
|
'The build finished, but ' + link + ' isn’t answering right now — this panel keeps checking.', '', flow));
|
|
@@ -7471,8 +7483,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7471
7483
|
/* the checklist reflects live state (task 1002694): a status transition —
|
|
7472
7484
|
e.g. the scaffold landing — re-renders the steps (change-only, so the
|
|
7473
7485
|
5s tick never clobbers an open dropdown or mid-flight button) */
|
|
7474
|
-
if (inst && inst.status !== live.instStatus) {
|
|
7475
|
-
live.instStatus = inst.status; renderSteps();
|
|
7486
|
+
if (inst && (inst.status !== live.instStatus || addressReady(inst) !== live.addrReady)) {
|
|
7487
|
+
live.instStatus = inst.status; live.addrReady = addressReady(inst); renderSteps();
|
|
7476
7488
|
if (state.step === 13) paintRail(13); /* the rail finishes only when the project answers */
|
|
7477
7489
|
}
|
|
7478
7490
|
var terminal = renderProgress(inst);
|
|
@@ -7505,7 +7517,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7505
7517
|
instanceId = null;
|
|
7506
7518
|
watchPending = false; manifestPending = false; manifestState = null;
|
|
7507
7519
|
manifestStartedAt = 0; manifestStale = false;
|
|
7508
|
-
live = { instStatus: null, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
|
|
7520
|
+
live = { instStatus: null, addrReady: false, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
|
|
7509
7521
|
lastStepsHtml = '';
|
|
7510
7522
|
lastProgHtml = '';
|
|
7511
7523
|
try { sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
|
|
@@ -7797,6 +7809,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7797
7809
|
so the page can never say "Answering at …" beside a chip that says "live?". */
|
|
7798
7810
|
function verifiedAt(inst) { return inst.last_verified_at ? new Date(inst.last_verified_at).getTime() : 0; }
|
|
7799
7811
|
function freshUp(inst) { var at = verifiedAt(inst); return !!(inst.last_verified_up && at && (Date.now() - at) <= FRESH_MS); }
|
|
7812
|
+
/* an address is LINKED only once the platform's own lookup has seen it answer (task
|
|
7813
|
+
1004505): a click made before the name exists is cached as "not found" by the
|
|
7814
|
+
visitor's resolver for up to half an hour, so the provision status alone is never
|
|
7815
|
+
enough. dns_resolves is the server's sighting; an up probe is the same proof. */
|
|
7816
|
+
function addressReady(inst) { return !!(inst && inst.domain && (inst.dns_resolves || inst.last_verified_up)); }
|
|
7800
7817
|
/* the landing moment (task 1003243): a standup or revive that VERIFIABLY came
|
|
7801
7818
|
up, still recent enough to be news. Two gates, both already on the card —
|
|
7802
7819
|
freshUp() so "live" stays ONE rule (a lit third stage beside a "live?" chip
|
|
@@ -7933,7 +7950,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7933
7950
|
else if (inst.status === 'error' || (inst.status === 'active' && inst.last_verified_up === false)) cls += ' ashen';
|
|
7934
7951
|
else if (inst.status === 'active' && !freshUp(inst)) cls += ' dim';
|
|
7935
7952
|
var attrs = planetAttrs(pl) + ' style="--acc:' + pl.acc + ';--d:64px"';
|
|
7936
|
-
if (inst.status === 'active' && inst
|
|
7953
|
+
if (inst.status === 'active' && addressReady(inst)) {
|
|
7937
7954
|
return '<a class="' + cls + '"' + attrs + ' href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener" title="Open ' + esc(inst.slug) + '" aria-label="Open ' + esc(inst.slug) + '"><span class="disc"></span></a>';
|
|
7938
7955
|
}
|
|
7939
7956
|
return '<span class="' + cls + '"' + attrs + ' aria-hidden="true"><span class="disc"></span></span>';
|
|
@@ -8085,6 +8102,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
8085
8102
|
has. Since task 1003355 the list row acts on nothing, so a row's sentence
|
|
8086
8103
|
sends the owner to the project's own page and the manage page's says
|
|
8087
8104
|
"below" — the ADR 0199 §8 rule that a refusal must name a reachable door. */
|
|
8105
|
+
var ADDRESS_SETTING_UP = 'Your address is being set up — it shows here, with a link, as soon as it answers.';
|
|
8088
8106
|
function addressRow(inst, onManage) {
|
|
8089
8107
|
/* state FIRST, then domain: a torn-down or snagged project still holds its
|
|
8090
8108
|
domain (ADR 0181 keeps port + address), and the standup promise below
|
|
@@ -8099,6 +8117,13 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
8099
8117
|
/* "answering" is a VERIFIED claim (direction §3 / F16): only a FRESH up
|
|
8100
8118
|
probe earns it — never the runner finishing its script, and never a
|
|
8101
8119
|
probe the chip has already aged into "live?" */
|
|
8120
|
+
/* task 1004505: no link — and no name to type — until the platform's own
|
|
8121
|
+
lookup has seen it answer; the manual-DNS note still names the address,
|
|
8122
|
+
because pointing it is the owner's own step */
|
|
8123
|
+
if (!addressReady(inst)) {
|
|
8124
|
+
return inst.dns_note ? 'Its address is <code>' + esc(inst.domain) + '</code>. ' + esc(inst.dns_note)
|
|
8125
|
+
: ADDRESS_SETTING_UP;
|
|
8126
|
+
}
|
|
8102
8127
|
var link = '<a href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">' + esc(inst.domain) + '</a>';
|
|
8103
8128
|
if (freshUp(inst)) return 'Answering at ' + link;
|
|
8104
8129
|
/* the runner had no DNS hook (F16): the CAUSE beats the bare symptom —
|
|
@@ -8119,7 +8144,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
8119
8144
|
return 'Its address is ' + link + ' — confirming it answers.';
|
|
8120
8145
|
}
|
|
8121
8146
|
if (inst.status === 'error') return 'Address <code>' + esc(inst.domain) + '</code> — held for this project, but nothing is answering there yet.';
|
|
8122
|
-
return '
|
|
8147
|
+
return inst.dns_note ? 'Its address is <code>' + esc(inst.domain) + '</code>. ' + esc(inst.dns_note) : ADDRESS_SETTING_UP;
|
|
8123
8148
|
}
|
|
8124
8149
|
/* the HONEST no-address state: sign-in on the instance needs a real domain */
|
|
8125
8150
|
return '<span class="none">No address yet</span> — sign-in on this project can’t be set up until it has one. ' +
|
|
@@ -8191,7 +8216,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
8191
8216
|
links, so middle/ctrl-click works; the delegated handler upgrades a plain
|
|
8192
8217
|
click on Manage (task 1003041). */
|
|
8193
8218
|
var acts = [];
|
|
8194
|
-
if (inst.status === 'active' && inst
|
|
8219
|
+
if (inst.status === 'active' && addressReady(inst)) acts.push('<a class="btn small ghost" href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">View</a>');
|
|
8195
8220
|
acts.push('<a class="btn small ghost" data-manage="' + iid + '" href="?view=manage&id=' + iid + '">Manage</a>');
|
|
8196
8221
|
body += '<div class="actions">' + acts.join('') + '</div>';
|
|
8197
8222
|
/* sig() buckets recency per minute, so a live row re-renders on the
|
|
@@ -8514,7 +8539,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
8514
8539
|
$('mErr').innerHTML = snag ? errMarkup(inst) : '';
|
|
8515
8540
|
$('mErr').hidden = !snag;
|
|
8516
8541
|
var acts = [];
|
|
8517
|
-
if (inst.status === 'active' && inst
|
|
8542
|
+
if (inst.status === 'active' && addressReady(inst)) acts.push('<a class="btn small ghost" href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">Open site →</a>');
|
|
8518
8543
|
if (canRetry(inst)) acts.push('<button type="button" class="btn small" data-m-retry>Try again</button>');
|
|
8519
8544
|
if (canRevive(inst)) acts.push('<button type="button" class="btn small" data-m-revive>Bring it back</button>');
|
|
8520
8545
|
if (canRestart(inst)) acts.push('<button type="button" class="btn small" data-m-restart>Restart it</button>');
|
|
@@ -9319,7 +9344,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
9319
9344
|
/* what the project RUNS: its own public manifest, read cross-origin (the
|
|
9320
9345
|
same GET /instance the platform's runner reads after a restart) */
|
|
9321
9346
|
function loadVisRuns(inst) {
|
|
9322
|
-
if (!inst || inst.status !== 'active' || !inst
|
|
9347
|
+
if (!inst || inst.status !== 'active' || !addressReady(inst)) { visRuns(''); return; } /* the browser's own lookup waits for the name too (task 1004505) */
|
|
9323
9348
|
var asked = manageId;
|
|
9324
9349
|
visRuns('Checking what the project is running…');
|
|
9325
9350
|
ensureManifest(inst.domain).then(function (ans) {
|
|
@@ -9772,7 +9797,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
9772
9797
|
/* what the project RUNS: the SAME shared manifest read the reach card uses
|
|
9773
9798
|
(task 1003554) — a different key out of one answer, not a second GET */
|
|
9774
9799
|
function loadJoinRuns(inst) {
|
|
9775
|
-
if (!inst || inst.status !== 'active' || !inst
|
|
9800
|
+
if (!inst || inst.status !== 'active' || !addressReady(inst)) { joinRuns(''); return; } /* the browser's own lookup waits for the name too (task 1004505) */
|
|
9776
9801
|
var asked = manageId;
|
|
9777
9802
|
joinRuns('Checking what the project is running…');
|
|
9778
9803
|
ensureManifest(inst.domain).then(function (ans) {
|