@bongos/core 1.19.999 → 1.19.1001
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 +116 -91
- package/.claude/skills/collab-review/SKILL.md +24 -13
- 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/0330-an-ask-is-answered-where-it-was-asked.md +155 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +155 -5
- package/docs/api-reference.md +6 -4
- package/docs/copy-inventory.md +18 -13
- package/docs/copy-registry.json +70 -15
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/collab-lib.js +79 -0
- package/modules/hall-ui/public/collab.css +60 -0
- package/modules/hall-ui/public/collab.js +134 -5
- package/modules/lifecycle/help-requests.js +118 -8
- package/modules/lifecycle/migrations/lifecycle_011_help_request_replies.sql +113 -0
- package/modules/lifecycle/routes/help-requests.js +93 -13
- package/modules/provisioning/capacity.js +3 -3
- package/modules/provisioning/control-plane-guard.js +1 -1
- package/modules/provisioning/core-upgrade.js +7 -7
- package/modules/provisioning/cost-ledger.js +74 -0
- package/modules/provisioning/migrations/provisioning_027_cloud_host_shape_rename.sql +58 -0
- package/modules/provisioning/onboard-plan.js +1 -1
- package/modules/provisioning/paid-shape-gate.js +28 -15
- package/modules/provisioning/provisioning.js +20 -39
- package/modules/provisioning/routes/provisioning.js +4 -3
- package/modules/provisioning/routes/repo-private.js +1 -1
- package/modules/provisioning/tests/liveness.mjs +16 -8
- package/modules/provisioning/tests/provisioning.mjs +1 -1
- package/modules/public-landing/public/projects.html +26 -27
- package/modules/ui-design/kit/fixtures/provisioning-instances.json +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/init.js +12 -4
- package/scripts/gds/onboard.js +1 -1
- package/scripts/gds/provision-capacity.js +6 -1
- package/scripts/gds/provision-config.js +6 -3
- package/scripts/gds/provision-core-upgrade.js +8 -8
- package/scripts/gds/provision-disconnect.js +2 -2
- package/scripts/gds/provision-process.js +2 -2
- package/scripts/gds/provision-repo-private.js +1 -1
- package/scripts/gds/provision-repo.js +13 -3
- package/scripts/gds/provision-teardown.js +2 -2
- package/scripts/gds/provision-units.js +2 -2
- package/scripts/gds/provision.js +6 -6
- package/src/module-api.js +1 -1
- package/tests/collab_page.mjs +140 -7
- package/tests/core_upgrade_door.mjs +1 -1
- package/tests/core_upgrade_runner.mjs +20 -20
- package/tests/deploy_page_projects.mjs +5 -5
- package/tests/hall_collab_world.mjs +6 -2
- package/tests/help_requests.mjs +201 -6
- package/tests/hub_disconnect_ui.mjs +4 -1
- package/tests/init.mjs +6 -6
- package/tests/onboard.mjs +5 -4
- package/tests/project_catalog_backfill.mjs +1 -1
- package/tests/project_catalog_bridge.mjs +2 -2
- package/tests/project_catalog_routes.mjs +4 -4
- package/tests/projects_hub.mjs +1 -1
- package/tests/projects_hub_pre_uat.mjs +22 -5
- package/tests/projects_hub_repo_private.mjs +1 -1
- package/tests/provision.mjs +32 -32
- package/tests/provision_byo_host.mjs +1 -1
- package/tests/provision_caddy_wiring.mjs +1 -1
- package/tests/provision_disconnect.mjs +3 -3
- package/tests/provision_repo_private.mjs +1 -1
- package/tests/provision_restart.mjs +5 -5
- package/tests/provision_settings_apply.mjs +4 -4
- package/tests/provision_teardown_address.mjs +2 -2
- package/tests/provisioning_capacity.mjs +40 -34
- package/tests/provisioning_capacity_gate.mjs +5 -12
- package/tests/provisioning_cost_ledger.mjs +98 -0
- package/tests/provisioning_default_domain.mjs +1 -1
- package/tests/provisioning_domain_attach.mjs +4 -4
- package/tests/provisioning_domain_unique.mjs +2 -2
- package/tests/provisioning_error_note_scope.mjs +1 -1
- package/tests/provisioning_module_selection.mjs +5 -5
- package/tests/provisioning_onboard_mode.mjs +2 -2
- package/tests/provisioning_paid_shape_gate.mjs +57 -44
- package/tests/provisioning_repo_private_route.mjs +1 -1
- package/tests/provisioning_restart.mjs +11 -4
- package/tests/provisioning_settings_apply.mjs +12 -5
- package/tests/provisioning_settings_env.mjs +1 -1
- package/tests/provisioning_teardown_intent.mjs +3 -3
- package/tests/public_landing_projects.mjs +13 -6
- package/tests/upgrade.mjs +2 -2
|
@@ -75,11 +75,11 @@ function parseMeminfo(text) {
|
|
|
75
75
|
// consume a slot that a cloud-host would otherwise get — which is what returning null
|
|
76
76
|
// here buys, since PLACED_STATES counting keys off the shapes a box declares.
|
|
77
77
|
function placementBox(hostingShape, { remoteHost = null } = {}) {
|
|
78
|
-
if (hostingShape === '
|
|
78
|
+
if (hostingShape === 'byo-host') return null;
|
|
79
79
|
if (hostingShape === 'co-tenant' && remoteHost) {
|
|
80
80
|
return { box: 'overflow', shapes: ['co-tenant'], fixedMb: FIXED_MB.overflow };
|
|
81
81
|
}
|
|
82
|
-
return { box: 'control-plane', shapes: remoteHost ? ['
|
|
82
|
+
return { box: 'control-plane', shapes: remoteHost ? ['cloud-host'] : ['cloud-host', 'co-tenant'], fixedMb: FIXED_MB['control-plane'] };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// ceilingFor — the slot count for a box: an explicit override wins, else the ADR
|
|
@@ -148,7 +148,7 @@ function readLocalMeminfo() {
|
|
|
148
148
|
// for tests; `remoteHost` mirrors the runner's PROVISION_REMOTE_HOST so the two
|
|
149
149
|
// count the same shapes (set it in web.env too if co-tenants ever run remotely).
|
|
150
150
|
async function fleetCapacity(db, { meminfo = readLocalMeminfo(), remoteHost = process.env.PROVISION_REMOTE_HOST || null } = {}) {
|
|
151
|
-
const target = placementBox('
|
|
151
|
+
const target = placementBox('cloud-host', { remoteHost });
|
|
152
152
|
const { override, floorMb } = envOverrides();
|
|
153
153
|
const mem = parseMeminfo(meminfo);
|
|
154
154
|
const { ceiling, source } = ceilingFor({ override, memTotalMb: mem.memTotalMb, fixedMb: target.fixedMb });
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
//
|
|
12
12
|
// It also puts a 'teardown' button one query away from the platform itself. That
|
|
13
13
|
// is the risk this file exists to close, and it is the reason the shape is a
|
|
14
|
-
// distinct VALUE rather than a reuse of '
|
|
14
|
+
// distinct VALUE rather than a reuse of 'cloud-host': a distinct value is something
|
|
15
15
|
// every destructive leg can refuse BY NAME.
|
|
16
16
|
//
|
|
17
17
|
// AN ALLOW-LIST, NOT A DENY-LIST. Listing today's destructive actions and refusing
|
|
@@ -38,13 +38,13 @@ const CORE_UPGRADE_FROM = Object.freeze(new Set(['active']));
|
|
|
38
38
|
// The shapes whose box the runner can actually reach to perform an upgrade.
|
|
39
39
|
// Mirrors RESTART_SHAPES and adds the control plane (which the runner IS on).
|
|
40
40
|
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
const UPGRADE_SHAPES = Object.freeze(new Set(['co-tenant', '
|
|
41
|
+
// 'byo-host' is the shape this set excludes: the owner runs it, so there is nothing
|
|
42
|
+
// for the runner to upgrade and queueing one would be the F16 lie — a request that is
|
|
43
|
+
// accepted and never lands. This is a SEPARATE question from the control-plane guard,
|
|
44
|
+
// which asks what may be aimed at the platform itself; a first cut conflated the two.
|
|
45
|
+
// ('dedicated' was excluded here for the same reachability reason until ADR 0327
|
|
46
|
+
// decision 4 removed the shape and provisioning_027 folded its value into 'cloud-host'.)
|
|
47
|
+
const UPGRADE_SHAPES = Object.freeze(new Set(['co-tenant', 'cloud-host', 'control-plane']));
|
|
48
48
|
|
|
49
49
|
// The shape of a core version as the pin records it. Deliberately strict and
|
|
50
50
|
// three-part: the column's own CHECK carries the same pattern, so a value that
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// The fleet cost ledger (GET /provisioning/fleet/cost-ledger) — VISIBILITY only (ADR 0111 §6).
|
|
3
|
+
//
|
|
4
|
+
// It asks two questions, one column each (task 1004148, ADR 0327). Before, one column
|
|
5
|
+
// asked both and reported a confident $0 for every project:
|
|
6
|
+
// platform_monthly_usd — what a row costs US. A cloud-host runs on its owner's own
|
|
7
|
+
// account, so $0 is correct; the one thing we pay for is the control plane's box.
|
|
8
|
+
// owner_monthly_usd — an ESTIMATE of the owner's own hosting bill, never ours.
|
|
9
|
+
// null in either column means UNPRICED — the row names no plan we can price — and is
|
|
10
|
+
// never folded into a total as 0.
|
|
11
|
+
|
|
12
|
+
const { HOSTING_SHAPES, TIER_MONTHLY_USD } = require('./provisioning');
|
|
13
|
+
|
|
14
|
+
// What a cloud-host's owner pays Render per month, keyed by the plan stored in `tier`:
|
|
15
|
+
// web service + Postgres, ~$13 at the default plans (ADR 0327). No row names a plan yet;
|
|
16
|
+
// the Render standup (task 1004183) writes one, and the pilot (task 1004142) confirms
|
|
17
|
+
// these names and prices.
|
|
18
|
+
const RENDER_PLAN_MONTHLY_USD = Object.freeze({
|
|
19
|
+
'starter+basic-256mb': 13,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function priceOf(table, key) {
|
|
23
|
+
return typeof key === 'string' && Object.hasOwn(table, key) ? table[key] : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function platformMonthlyUsd({ hostingShape, tier } = {}) {
|
|
27
|
+
// The box the projects share today is priced once, here, by its droplet size.
|
|
28
|
+
if (hostingShape === 'control-plane') return priceOf(TIER_MONTHLY_USD, tier);
|
|
29
|
+
return HOSTING_SHAPES.includes(hostingShape) ? 0 : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ownerMonthlyUsd({ hostingShape, tier } = {}) {
|
|
33
|
+
if (hostingShape === 'cloud-host') return priceOf(RENDER_PLAN_MONTHLY_USD, tier);
|
|
34
|
+
// co-tenant shares our box and the control plane is ours: no owner is billed for either.
|
|
35
|
+
if (hostingShape === 'co-tenant' || hostingShape === 'control-plane') return 0;
|
|
36
|
+
return null; // byo-host runs on the owner's own machine, whose bill we cannot see
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function columnTotal(instances, key) {
|
|
40
|
+
const priced = instances.map((i) => i[key]).filter((v) => v !== null);
|
|
41
|
+
const sum = priced.reduce((a, v) => a + v, 0);
|
|
42
|
+
return { monthly_usd: Number(sum.toFixed(4)), unpriced: instances.length - priced.length };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Per-instance costs + a total per column, over the instances not torn down.
|
|
46
|
+
async function costLedger(db, { limit = 500 } = {}) {
|
|
47
|
+
const { rows } = await db.query(
|
|
48
|
+
`SELECT pi.id, pi.slug, pi.hosting_shape, pi.tier, pi.status, b.github_login
|
|
49
|
+
FROM provisioning_instances pi
|
|
50
|
+
JOIN builders b ON b.id = pi.owner_builder_id
|
|
51
|
+
WHERE pi.status <> 'torn_down'
|
|
52
|
+
ORDER BY pi.updated_at DESC
|
|
53
|
+
LIMIT $1`,
|
|
54
|
+
[Math.min(Math.max(Number(limit) || 500, 1), 2000)]
|
|
55
|
+
);
|
|
56
|
+
const instances = rows.map((r) => {
|
|
57
|
+
const shape = { hostingShape: r.hosting_shape, tier: r.tier };
|
|
58
|
+
return {
|
|
59
|
+
id: r.id, slug: r.slug, owner: r.github_login,
|
|
60
|
+
hosting_shape: r.hosting_shape, tier: r.tier, status: r.status,
|
|
61
|
+
platform_monthly_usd: platformMonthlyUsd(shape),
|
|
62
|
+
owner_monthly_usd: ownerMonthlyUsd(shape),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
instances,
|
|
67
|
+
totals: {
|
|
68
|
+
platform: columnTotal(instances, 'platform_monthly_usd'),
|
|
69
|
+
owner: columnTotal(instances, 'owner_monthly_usd'),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { RENDER_PLAN_MONTHLY_USD, platformMonthlyUsd, ownerMonthlyUsd, costLedger };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
-- provisioning_027 — the stored hosting shapes take their owner-facing names
|
|
2
|
+
-- (ADR 0323 §1, ADR 0327 decision 4, task 1004182).
|
|
3
|
+
--
|
|
4
|
+
-- provisioning_021 deliberately left the two managed shapes at their old stored
|
|
5
|
+
-- spellings, on the grounds that task 1004142 would rename them "with those files
|
|
6
|
+
-- already open". That task has since split; the rename is task 1004182 and this is it.
|
|
7
|
+
--
|
|
8
|
+
-- 'standalone' -> 'cloud-host' the shape ADR 0323 renamed owner-facing
|
|
9
|
+
-- 'dedicated' -> 'cloud-host' the shape ADR 0327 decision 4 REMOVED
|
|
10
|
+
-- 'co-tenant' unchanged retired, but still storable -- see below
|
|
11
|
+
-- 'byo-host' unchanged the free front door, and the column DEFAULT
|
|
12
|
+
-- 'control-plane' unchanged the platform's own row, unrequestable
|
|
13
|
+
--
|
|
14
|
+
-- WHY 'dedicated' FOLDS INTO 'cloud-host' RATHER THAN BEING DROPPED. ADR 0327 decision 4
|
|
15
|
+
-- removes the shape because "on the owner's own platform account a larger plan is a
|
|
16
|
+
-- setting they choose, not a hosting shape we model", and its consequences say the value
|
|
17
|
+
-- "must be migrated, not merely orphaned". A dedicated instance was a cloud-host on a
|
|
18
|
+
-- bigger box, so cloud-host is where it belongs. Measured on the live fleet 2026-09-23
|
|
19
|
+
-- there are ZERO 'dedicated' rows, so this is expected to update nothing -- but it is
|
|
20
|
+
-- written to do the right thing rather than to assume the measurement still holds at
|
|
21
|
+
-- the moment it runs, which is the whole reason it is an UPDATE and not an assertion.
|
|
22
|
+
--
|
|
23
|
+
-- 'co-tenant' STAYS IN THE SET, for the reason provisioning_021 gave and this migration
|
|
24
|
+
-- does not revisit: retirement closes the door, not the record. One torn-down row still
|
|
25
|
+
-- carries it, and dropping it from the constraint would make a historical row violate
|
|
26
|
+
-- its own table. The door stays closed in code (paid-shape-gate.js lists it
|
|
27
|
+
-- UNREQUESTABLE), never in the constraint.
|
|
28
|
+
--
|
|
29
|
+
-- THIS IS A NARROWING, WHICH THE PRECEDING FOUR CONSTRAINT REWRITES WERE NOT. fitness.js
|
|
30
|
+
-- (isConstraintWidening) permits a `DROP CONSTRAINT IF EXISTS` paired with a re-`ADD
|
|
31
|
+
-- CONSTRAINT` of the same name, and its comment justifies that as additive -- "no
|
|
32
|
+
-- existing row changes and no data is lost". That justification does NOT describe this
|
|
33
|
+
-- migration: rows change, and two values leave the set. It is safe for a different
|
|
34
|
+
-- reason, which is the ORDER below.
|
|
35
|
+
--
|
|
36
|
+
-- THE ORDER IS DROP -> UPDATE -> ADD, AND ALL THREE POSITIONS MATTER. The obvious
|
|
37
|
+
-- arrangement -- move the rows onto a legal value first, then narrow the constraint over
|
|
38
|
+
-- them -- does not work, and fails immediately rather than subtly: the OLD constraint is
|
|
39
|
+
-- still in force during that UPDATE and it does not admit 'cloud-host', so the very first
|
|
40
|
+
-- row rewritten violates it. (Verified: that arrangement was written first and refused on
|
|
41
|
+
-- a probe database with `new row ... violates check constraint`.) Dropping first leaves a
|
|
42
|
+
-- window with NO constraint, which is exactly what the UPDATE needs; adding last closes it
|
|
43
|
+
-- over rows that are already legal. Postgres runs a migration file in one implicit
|
|
44
|
+
-- transaction, so the unconstrained window is never visible to another session.
|
|
45
|
+
|
|
46
|
+
-- 1. Take the old constraint off. Nothing may constrain the column while it is rewritten.
|
|
47
|
+
ALTER TABLE provisioning_instances DROP CONSTRAINT IF EXISTS provisioning_instances_hosting_shape_check;
|
|
48
|
+
|
|
49
|
+
-- 2. Move the rows. Both old spellings land on the one surviving managed shape.
|
|
50
|
+
UPDATE provisioning_instances SET hosting_shape = 'cloud-host'
|
|
51
|
+
WHERE hosting_shape IN ('standalone', 'dedicated');
|
|
52
|
+
|
|
53
|
+
-- 3. Put the narrowed constraint back, now that nothing violates it.
|
|
54
|
+
ALTER TABLE provisioning_instances ADD CONSTRAINT provisioning_instances_hosting_shape_check
|
|
55
|
+
CHECK (hosting_shape IN ('byo-host', 'co-tenant', 'cloud-host', 'control-plane'));
|
|
56
|
+
|
|
57
|
+
-- The DEFAULT is already 'byo-host' (provisioning_021) and stays valid, so it is not
|
|
58
|
+
-- touched here. Naming it is cheaper than the next reader re-deriving that it is safe.
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
// for one thing is how a filter starts silently matching nothing. The shape is absent
|
|
45
45
|
// from CONTROL_PLANE_SHAPES, which is what drops the runner-performed legs from its plan
|
|
46
46
|
// BY CONSTRUCTION rather than by a list someone has to remember to update.
|
|
47
|
-
const CONTROL_PLANE_SHAPES = ['
|
|
47
|
+
const CONTROL_PLANE_SHAPES = ['cloud-host', 'co-tenant'];
|
|
48
48
|
const ALL_SHAPES = [...CONTROL_PLANE_SHAPES, 'byo-host'];
|
|
49
49
|
|
|
50
50
|
function onboardPlan(mode = 'greenfield', opts = {}) {
|
|
@@ -22,17 +22,29 @@
|
|
|
22
22
|
// co-tenant — RETIRED (ADR 0323). Was the free front door; ran on the platform's
|
|
23
23
|
// own box, which is why the name could not be carried over to
|
|
24
24
|
// bring-your-own-host. Now UNREQUESTABLE — see below.
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
25
|
+
// cloud-host — runs the caller's OWN repo on a host the platform stands up. TODAY
|
|
26
|
+
// that host is still the platform's own box, and that is what this
|
|
27
|
+
// entry gates: the unit's WorkingDirectory is that checkout
|
|
28
|
+
// (`scripts/gds/provision-units.js`) and the module loader discovers
|
|
29
|
+
// modules from `resolveInstanceRoot()`, which is process.cwd()
|
|
30
|
+
// (`src/module-loader/loader.js`). So the caller names a repo and the
|
|
31
|
+
// platform require()s its `modules/` — arbitrary code execution on the
|
|
32
|
+
// control plane's own machine. Task 1003369 gave each instance its own
|
|
33
|
+
// uid, which bounds the blast radius to that account; it does not make
|
|
34
|
+
// "any Xenos may run code here" an acceptable default.
|
|
35
|
+
//
|
|
36
|
+
// Once ADR 0327 lands, a cloud-host stands up on the OWNER's platform
|
|
37
|
+
// account instead and that specific argument dissolves — but the gate
|
|
38
|
+
// does NOT automatically follow, because the platform is still doing
|
|
39
|
+
// the standing-up and still spending its own runner on it. Re-decide
|
|
40
|
+
// this entry when the move happens (task 1004184); do not assume the
|
|
41
|
+
// shape becomes open just because the box stops being ours.
|
|
42
|
+
//
|
|
43
|
+
// Stored as 'standalone' until task 1004182 renamed it to match the
|
|
44
|
+
// owner-facing name (migration provisioning_027). The old 'dedicated'
|
|
45
|
+
// value folded into this one: ADR 0327 decision 4 deleted that shape,
|
|
46
|
+
// because on someone else's account a bigger plan is a setting, not a
|
|
47
|
+
// hosting shape we model.
|
|
36
48
|
// control-plane — the platform's OWN row (ADR 0293 D2, task 1003159), enrolled by
|
|
37
49
|
// `scripts/gds/enroll-control-plane.js` so the owner's deploy door has
|
|
38
50
|
// something to point at. Never a thing a builder asks for. See below.
|
|
@@ -45,10 +57,11 @@
|
|
|
45
57
|
// The set named below is the set that needs NO authority. Every other shape, including
|
|
46
58
|
// one added after this file was last read, requires the permission by omission. The
|
|
47
59
|
// previous shape of this gate listed the one shape that was gated and admitted the rest,
|
|
48
|
-
// and that is exactly how `
|
|
49
|
-
// because it was reasoned about as "$0, therefore harmless"
|
|
50
|
-
// of the
|
|
51
|
-
// written. Permitted-by-omission is the failure mode this
|
|
60
|
+
// and that is exactly how `cloud-host` and `control-plane` stayed open: `cloud-host`
|
|
61
|
+
// (then stored as `standalone`) because it was reasoned about as "$0, therefore harmless"
|
|
62
|
+
// — true of the invoice, false of the trust — and `control-plane` because it did not
|
|
63
|
+
// exist yet when the gate was written. Permitted-by-omission is the failure mode this
|
|
64
|
+
// inversion removes.
|
|
52
65
|
//
|
|
53
66
|
// The atom is `provisioning.fleet.manage` (floor: metic), deliberately the same one that
|
|
54
67
|
// already guards the fleet cost-ledger and force-teardown. Spending on the fleet and
|
|
@@ -24,8 +24,9 @@ const { normalizeModuleSelection, effectiveModules } = require('./starter-bundle
|
|
|
24
24
|
// Constants (mirror the CHECK constraints in provisioning_001_tables.sql)
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
26
26
|
const INSTANCE_STATES = ['requested', 'provisioning', 'active', 'tearing_down', 'torn_down', 'error'];
|
|
27
|
-
// Shapes
|
|
28
|
-
|
|
27
|
+
// Shapes + why 'co-tenant' is storable but unrequestable: this module's CLAUDE.md.
|
|
28
|
+
// Stored values match the owner-facing names since task 1004182 (provisioning_027).
|
|
29
|
+
const HOSTING_SHAPES = ['byo-host', 'co-tenant', 'cloud-host', 'control-plane'];
|
|
29
30
|
// How the instance's repo was onboarded (migration provisioning_004, ADR 0121):
|
|
30
31
|
// greenfield — a fresh EMPTY repo; the runner scaffolds it and pushes the first commit
|
|
31
32
|
// adopt — a repo the owner ALREADY has; the runner must LAYER onto it, preserving
|
|
@@ -102,11 +103,9 @@ const MAX_RELEASED_DOMAINS = 8;
|
|
|
102
103
|
// provision intent cannot stand in for it: PROVISION_ENQUEUE_FROM deliberately
|
|
103
104
|
// excludes 'active' (task 1002691).
|
|
104
105
|
const SETTINGS_APPLY_FROM = new Set(['active']);
|
|
105
|
-
// The hosting shapes the runner has a write path to.
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
// rather than queue one that never lands.
|
|
109
|
-
const SETTINGS_APPLY_SHAPES = new Set(['co-tenant', 'standalone']);
|
|
106
|
+
// The hosting shapes the runner has a write path to. 'byo-host' is absent (the owner runs
|
|
107
|
+
// it, we reach into nothing) and so is 'control-plane' (the platform's own row).
|
|
108
|
+
const SETTINGS_APPLY_SHAPES = new Set(['co-tenant', 'cloud-host']);
|
|
110
109
|
// The ONE state a restart is meaningful from (task 1003238). Liveness is an
|
|
111
110
|
// OVERLAY, not a seventh state — an instance the heartbeat cannot reach is still
|
|
112
111
|
// 'active', and 'active' is exactly where a restart applies. Every earlier state
|
|
@@ -116,10 +115,9 @@ const SETTINGS_APPLY_SHAPES = new Set(['co-tenant', 'standalone']);
|
|
|
116
115
|
// SETTINGS_APPLY_FROM: each action declaring its own gate is this module's
|
|
117
116
|
// convention, and the two answer different questions.
|
|
118
117
|
const RESTART_FROM = new Set(['active']);
|
|
119
|
-
// Same reachability rule as the settings push, for the same reason:
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
const RESTART_SHAPES = new Set(['co-tenant', 'standalone']);
|
|
118
|
+
// Same reachability rule as the settings push, and the same set for the same reason:
|
|
119
|
+
// the runner can only restart what it runs.
|
|
120
|
+
const RESTART_SHAPES = new Set(['co-tenant', 'cloud-host']);
|
|
123
121
|
|
|
124
122
|
// Per-project settings (task 1003040, goal 1000058): the known keys + their
|
|
125
123
|
// vocabularies, enforced at the API layer — the jsonb `settings` column
|
|
@@ -303,9 +301,16 @@ function isValidSlug(slug) {
|
|
|
303
301
|
return true;
|
|
304
302
|
}
|
|
305
303
|
|
|
306
|
-
// Monthly USD estimate
|
|
307
|
-
//
|
|
308
|
-
//
|
|
304
|
+
// Monthly USD estimate of what a shape costs THE PLATFORM. PURE.
|
|
305
|
+
//
|
|
306
|
+
// SINCE task 1004182 THIS RETURNS 0 FOR EVERY STORED ROW, and that is correct, not broken:
|
|
307
|
+
// 'dedicated' was the only shape that priced above zero and provisioning_027 folded it into
|
|
308
|
+
// 'cloud-host', so no row can carry it. ADR 0327 says a ledger of OUR spend "should read
|
|
309
|
+
// ~$0 per cloud-host and be *correct*"; what an owner is worth telling is THEIR platform
|
|
310
|
+
// cost, a different column — ownerMonthlyUsd in cost-ledger.js, which the fleet ledger
|
|
311
|
+
// reads instead of this stored value. The 'dedicated' arm and the tier table stay because
|
|
312
|
+
// this is pure and its tests call it with that value; the droplet code path is task
|
|
313
|
+
// 1004185's to delete.
|
|
309
314
|
function costEstimateUsd({ hostingShape, tier } = {}) {
|
|
310
315
|
if (hostingShape !== 'dedicated') return 0;
|
|
311
316
|
// An ABSENT tier is not an unknown one. The runner substitutes its own default
|
|
@@ -1430,30 +1435,6 @@ async function planetChoicesForHosts(db, hosts) {
|
|
|
1430
1435
|
return out;
|
|
1431
1436
|
}
|
|
1432
1437
|
|
|
1433
|
-
// Fleet cost ledger (Archon): per-instance monthly estimate + a total, over the
|
|
1434
|
-
// instances that still cost something (not torn_down). VISIBILITY only (ADR 0111 §6).
|
|
1435
|
-
async function costLedger(db, { limit = 500 } = {}) {
|
|
1436
|
-
const { rows } = await db.query(
|
|
1437
|
-
`SELECT pi.id, pi.slug, pi.hosting_shape, pi.tier, pi.status,
|
|
1438
|
-
pi.cost_estimate_usd, b.github_login
|
|
1439
|
-
FROM provisioning_instances pi
|
|
1440
|
-
JOIN builders b ON b.id = pi.owner_builder_id
|
|
1441
|
-
WHERE pi.status <> 'torn_down'
|
|
1442
|
-
ORDER BY pi.cost_estimate_usd DESC, pi.updated_at DESC
|
|
1443
|
-
LIMIT $1`,
|
|
1444
|
-
[Math.min(Math.max(Number(limit) || 500, 1), 2000)]
|
|
1445
|
-
);
|
|
1446
|
-
const totalMonthlyUsd = rows.reduce((a, r) => a + Number(r.cost_estimate_usd || 0), 0);
|
|
1447
|
-
return {
|
|
1448
|
-
instances: rows.map((r) => ({
|
|
1449
|
-
id: r.id, slug: r.slug, owner: r.github_login,
|
|
1450
|
-
hosting_shape: r.hosting_shape, tier: r.tier, status: r.status,
|
|
1451
|
-
cost_estimate_usd: Number(r.cost_estimate_usd || 0),
|
|
1452
|
-
})),
|
|
1453
|
-
total_monthly_usd: Number(totalMonthlyUsd.toFixed(4)),
|
|
1454
|
-
};
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
1438
|
module.exports = {
|
|
1458
1439
|
// constants
|
|
1459
1440
|
INSTANCE_STATES, HOSTING_SHAPES, ONBOARD_MODES, PROJECT_TYPES, INTENT_ACTIONS, TEARDOWN_FROM,
|
|
@@ -1484,7 +1465,7 @@ module.exports = {
|
|
|
1484
1465
|
recordSettingsPushOwed,
|
|
1485
1466
|
createInstance, setInstanceStatus, updateInstanceDomain, clearReleasedDomains, updateInstanceSettings, updateInstanceDetail,
|
|
1486
1467
|
enqueueIntent, enqueueTeardown, enqueueSettingsApply, enqueueRestart, getOpenIntent, claimNextIntent, resolveIntent,
|
|
1487
|
-
recordEvent, instanceStateForSlug, planetChoicesForHosts, projectOwnedBy,
|
|
1468
|
+
recordEvent, instanceStateForSlug, planetChoicesForHosts, projectOwnedBy,
|
|
1488
1469
|
// data access — GitHub App Manifest attempts (task 2080)
|
|
1489
1470
|
createOAuthManifest, getOAuthManifestByState, attachManifestCode,
|
|
1490
1471
|
getLatestOAuthManifestForInstance, claimNextOAuthManifest, resolveOAuthManifest,
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// GET /provisioning/instances/:id/invite-suggestions — who is worth inviting (R28, ADR 0251)
|
|
30
30
|
// Archon-scoped (requireRank('archon') — the fleet, the /boxes precedent):
|
|
31
31
|
// GET /provisioning/fleet — the full instance roster + the box's slot `capacity` (ADR 0285)
|
|
32
|
-
// GET /provisioning/fleet/cost-ledger — per-instance +
|
|
32
|
+
// GET /provisioning/fleet/cost-ledger — per-instance cost to us + to its owner, with totals
|
|
33
33
|
// POST /provisioning/instances/:id/force-teardown — tear down ANY instance
|
|
34
34
|
// public:
|
|
35
35
|
// GET /provisioning/health — liveness + core version
|
|
@@ -49,6 +49,7 @@ const express = require('express');
|
|
|
49
49
|
const api = require('../../../src/module-api');
|
|
50
50
|
const auth = api, { pool, validateOrRespond, parseId } = api;
|
|
51
51
|
const provisioning = require('../provisioning');
|
|
52
|
+
const { costLedger } = require('../cost-ledger');
|
|
52
53
|
const { onboardPlan, stepsForShape } = require('../onboard-plan');
|
|
53
54
|
const { ALWAYS_ON_CORE, OPTIONAL_MODULES, starterBundleAnswerTable, optionalModules, effectiveModules } = require('../starter-bundles');
|
|
54
55
|
const { recommendationsForDetails } = require('../recommendations');
|
|
@@ -214,14 +215,14 @@ module.exports = function provisioningRoutes() {
|
|
|
214
215
|
}
|
|
215
216
|
});
|
|
216
217
|
|
|
217
|
-
// GET /provisioning/fleet/cost-ledger — per-instance monthly
|
|
218
|
+
// GET /provisioning/fleet/cost-ledger — per-instance monthly cost to us + estimated cost to its owner, with totals.
|
|
218
219
|
// rank: archon — cost oversight across the fleet (the /boxes/cost-ledger precedent).
|
|
219
220
|
// Declared among the archon routes (archon-first) for correct auditor classification.
|
|
220
221
|
router.get('/provisioning/fleet/cost-ledger', auth.requireBuilder, auth.requirePermission('provisioning.fleet.manage'), async (req, res) => {
|
|
221
222
|
try {
|
|
222
223
|
// R14 (#2001 / ADR 0119): shared helper; per-endpoint cap preserved (default 500, max 2000).
|
|
223
224
|
const { limit } = api.parsePagination(req, { defaultLimit: 500, maxLimit: 2000 });
|
|
224
|
-
const out = await
|
|
225
|
+
const out = await costLedger(pool, { limit });
|
|
225
226
|
res.json(out);
|
|
226
227
|
} catch (err) {
|
|
227
228
|
log.error('[provisioning] GET /provisioning/fleet/cost-ledger', err);
|
|
@@ -26,7 +26,7 @@ function unavailableReason(inst) {
|
|
|
26
26
|
if (inst.hosting_shape === 'byo-host') {
|
|
27
27
|
return 'This project runs on your own host, so the platform never reads its code — you can make the repository private on GitHub directly.';
|
|
28
28
|
}
|
|
29
|
-
if (inst.hosting_shape !== '
|
|
29
|
+
if (inst.hosting_shape !== 'cloud-host') {
|
|
30
30
|
return 'Only a project on a Cloud host can have its repository made private from here.';
|
|
31
31
|
}
|
|
32
32
|
if (inst.status !== 'active') {
|
|
@@ -307,15 +307,23 @@ await ta('a redirecting healthz is DOWN — the probe never follows a redirect (
|
|
|
307
307
|
|
|
308
308
|
console.log('\nshape-aware onboard plan (direction §7 — no promises a shape cannot keep):');
|
|
309
309
|
|
|
310
|
-
t('runner-performed legs are absent from the
|
|
311
|
-
|
|
310
|
+
t('runner-performed legs are absent from the byo-host plan; control-plane shapes keep everything', () => {
|
|
311
|
+
// The shape here was 'self-host' until task 1004182 noticed it had stopped meaning
|
|
312
|
+
// anything. ADR 0323 (task 1004141) replaced that plan-only spelling with 'byo-host' in
|
|
313
|
+
// ALL_SHAPES, so 'self-host' became an UNKNOWN shape — and an unknown shape is
|
|
314
|
+
// deliberately unfiltered (the last assertion in this case), which handed the full plan
|
|
315
|
+
// back and made every "absent on self-host" assertion below fail. It has been red since,
|
|
316
|
+
// masked because the DB-backed cases in this file fail first on any machine without a
|
|
317
|
+
// local Postgres. Pinning the live spelling is what makes the filter actually tested.
|
|
318
|
+
const { onboardPlan, stepsForShape, ALL_SHAPES } = require('../onboard-plan.js');
|
|
319
|
+
assert.ok(ALL_SHAPES.includes('byo-host'), 'the filtered shape must be one the plan knows, or this case tests nothing');
|
|
312
320
|
const all = onboardPlan('greenfield');
|
|
313
|
-
const
|
|
314
|
-
assert.equal(
|
|
315
|
-
const selfHost = stepsForShape(all, '
|
|
321
|
+
const cloudHost = stepsForShape(all, 'cloud-host');
|
|
322
|
+
assert.equal(cloudHost.length, all.length, 'a control-plane shape gets the full sequence');
|
|
323
|
+
const selfHost = stepsForShape(all, 'byo-host');
|
|
316
324
|
const selfKeys = selfHost.map((s) => s.key);
|
|
317
325
|
for (const runnerLeg of ['provision', 'oauth-place', 'dns-preflight']) {
|
|
318
|
-
assert.ok(!selfKeys.includes(runnerLeg), `${runnerLeg} assumes the platform runner — absent on
|
|
326
|
+
assert.ok(!selfKeys.includes(runnerLeg), `${runnerLeg} assumes the platform runner — absent on byo-host`);
|
|
319
327
|
}
|
|
320
328
|
for (const human of ['github-repo', 'oauth-app', 'first-sign-in', 'enter-hall']) {
|
|
321
329
|
assert.ok(selfKeys.includes(human), `${human} is the owner's own leg — present on every shape`);
|
|
@@ -374,7 +382,7 @@ console.log('\nAPI exposure — the overlay rides the instance reads:');
|
|
|
374
382
|
|
|
375
383
|
await ta('GET /provisioning/instances carries last_verified_at + last_verified_up', async () => {
|
|
376
384
|
instance = {
|
|
377
|
-
id: 18, owner_builder_id: 7, slug: 'mercury', status: 'active', hosting_shape: '
|
|
385
|
+
id: 18, owner_builder_id: 7, slug: 'mercury', status: 'active', hosting_shape: 'cloud-host',
|
|
378
386
|
domain: 'mercury.cloudbongos.com', port: 3101,
|
|
379
387
|
last_verified_at: '2026-08-12T10:00:00.000Z', last_verified_up: true,
|
|
380
388
|
};
|
|
@@ -403,7 +411,7 @@ t("PROVISION_ENQUEUE_FROM allows 'error' (the state a snagged card is in)", () =
|
|
|
403
411
|
await ta("POST /provisioning/instances on an 'error' instance re-enqueues a provision intent", async () => {
|
|
404
412
|
enqueueCalls.length = 0;
|
|
405
413
|
createResult = {
|
|
406
|
-
instance: { id: 20, owner_builder_id: 7, slug: 'snagged', status: 'error', hosting_shape: '
|
|
414
|
+
instance: { id: 20, owner_builder_id: 7, slug: 'snagged', status: 'error', hosting_shape: 'cloud-host', domain: null },
|
|
407
415
|
created: false,
|
|
408
416
|
};
|
|
409
417
|
const res = await req('POST', '/provisioning/instances', { slug: 'snagged' });
|
|
@@ -172,7 +172,7 @@ t('an UNRECOGNISED tier is still $0 — unpriced rather than wrong', () => {
|
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
t('every non-dedicated shape is $0 — it co-tenants on infrastructure already paid for', () => {
|
|
175
|
-
for (const shape of ['
|
|
175
|
+
for (const shape of ['cloud-host', 'co-tenant', 'shared', undefined]) {
|
|
176
176
|
assert.equal(P.costEstimateUsd({ hostingShape: shape, tier: 's-4vcpu-8gb' }), 0,
|
|
177
177
|
`shape ${JSON.stringify(shape)} is never metered, whatever tier rides along`);
|
|
178
178
|
}
|
|
@@ -3959,8 +3959,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3959
3959
|
['Address', resolvedDomain()
|
|
3960
3960
|
? '<code>' + esc(resolvedDomain()) + '</code>'
|
|
3961
3961
|
: '<em style="font-weight:600;color:var(--warn)">none — sign-in stays locked until it has one</em>'],
|
|
3962
|
-
/* The wizard creates hosting_shape '
|
|
3963
|
-
|
|
3962
|
+
/* The wizard creates hosting_shape 'cloud-host' — we stand it up and run it for
|
|
3963
|
+
them. Since task 1004182 the stored value IS the owner-facing name (ADR 0323). */
|
|
3964
3964
|
['Shape', 'Cloud host — we stand it up and run it for you, on the platform’s current release']
|
|
3965
3965
|
];
|
|
3966
3966
|
$('reviewList').innerHTML = rows.map(function (r) {
|
|
@@ -4050,7 +4050,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4050
4050
|
the server then applies the SAME starter bundle the panel showed,
|
|
4051
4051
|
so a skip and an accept land the same modules */
|
|
4052
4052
|
modules: modulesForCreate(),
|
|
4053
|
-
hosting_shape: '
|
|
4053
|
+
hosting_shape: 'cloud-host',
|
|
4054
4054
|
/* ONLY a bring-your-own domain travels (task 1002898 N2). The server owns
|
|
4055
4055
|
the assigned <slug>.<apex> default (task 1002704) and applies it to an
|
|
4056
4056
|
omitted domain, so re-sending our own derived copy could only disagree
|
|
@@ -4144,9 +4144,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4144
4144
|
var domain = resolvedDomain() || '';
|
|
4145
4145
|
var key = state.mode + '|' + domain;
|
|
4146
4146
|
if (!planPromises[key]) {
|
|
4147
|
-
/* shape=
|
|
4147
|
+
/* shape=cloud-host: this wizard only creates control-plane cloud-host
|
|
4148
4148
|
instances, and the plan is shape-filtered server-side (direction §7) */
|
|
4149
|
-
planPromises[key] = fetch(API + '/provisioning/onboard-plan?mode=' + encodeURIComponent(state.mode) + '&shape=
|
|
4149
|
+
planPromises[key] = fetch(API + '/provisioning/onboard-plan?mode=' + encodeURIComponent(state.mode) + '&shape=cloud-host' + (domain ? '&domain=' + encodeURIComponent(domain) : ''))
|
|
4150
4150
|
.then(function (res) { if (!res.ok) throw new Error('plan'); return res.json(); })
|
|
4151
4151
|
.then(function (d) { return (d && d.steps) || []; })
|
|
4152
4152
|
.catch(function () {
|
|
@@ -4601,8 +4601,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4601
4601
|
} else if (s === 'tearing_down') {
|
|
4602
4602
|
/* a deliberate end is calm, never styled as a failure (direction §5) —
|
|
4603
4603
|
and it speaks the teardown vocabulary, not the standup's (F22) */
|
|
4604
|
-
setProgress(progressMarkup(1, 1, 'Taking the server offline. The repository stays yours; '
|
|
4605
|
-
(inst.hosting_shape === 'dedicated' ? 'its database goes with the machine.' : 'the database is kept.'), '', 'teardown'));
|
|
4604
|
+
setProgress(progressMarkup(1, 1, 'Taking the server offline. The repository stays yours; the database is kept.', '', 'teardown'));
|
|
4606
4605
|
} else if (s === 'torn_down') {
|
|
4607
4606
|
setProgress(progressMarkup(-1, 3, 'This instance was torn down. Bring it back any time from ' +
|
|
4608
4607
|
projectPageLink('its own page') + ', or start another below.', '', 'teardown'));
|
|
@@ -5063,8 +5062,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5063
5062
|
brand-new project never grows a Restart button it has no use for. A door on
|
|
5064
5063
|
every healthy card would only invite needless downtime. */
|
|
5065
5064
|
function canRestart(inst) {
|
|
5066
|
-
return inst.status === 'active' && inst.last_verified_up === false &&
|
|
5067
|
-
inst.hosting_shape !== 'dedicated' && !inst.open_intent;
|
|
5065
|
+
return inst.status === 'active' && inst.last_verified_up === false && !inst.open_intent;
|
|
5068
5066
|
}
|
|
5069
5067
|
/* keep watching while the platform is doing something to this instance */
|
|
5070
5068
|
function shouldWatch(inst) {
|
|
@@ -5172,8 +5170,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5172
5170
|
: 'Raising the server, database, and address — nothing needed from you.');
|
|
5173
5171
|
}
|
|
5174
5172
|
if (s === 'tearing_down') {
|
|
5175
|
-
return stripMarkup('teardown', 1, 1, 'Taking the server offline. The repository stays yours; '
|
|
5176
|
-
(inst.hosting_shape === 'dedicated' ? 'its database goes with the machine.' : 'the database is kept.'));
|
|
5173
|
+
return stripMarkup('teardown', 1, 1, 'Taking the server offline. The repository stays yours; the database is kept.');
|
|
5177
5174
|
}
|
|
5178
5175
|
if (inst.open_intent) {
|
|
5179
5176
|
var a = inst.open_intent.action;
|
|
@@ -5242,16 +5239,22 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5242
5239
|
/* ── rendering ───────────────────────────────────────────────────────── */
|
|
5243
5240
|
|
|
5244
5241
|
/* The owner-facing name of each hosting shape (ADR 0323). This map IS the translation
|
|
5245
|
-
between the stored value and the word the owner reads
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5242
|
+
between the stored value and the word the owner reads. Read as a noun phrase — it
|
|
5243
|
+
lands in a meta list and in the "server" ledger row.
|
|
5244
|
+
|
|
5245
|
+
Since task 1004182 (migration provisioning_027) the stored values ARE the
|
|
5246
|
+
owner-facing words, so most of this map is now an identity. It is kept, rather than
|
|
5247
|
+
collapsed into a bare `inst.hosting_shape`, for the two entries that still translate
|
|
5248
|
+
— 'byo-host' and 'co-tenant' — and because the callers already fall through to the
|
|
5249
|
+
raw value for an unknown shape, which is the behaviour a future shape should get.
|
|
5250
|
+
|
|
5251
|
+
'co-tenant' is retired and unrequestable, but one torn-down row still carries it, so
|
|
5252
|
+
it keeps a label rather than falling through. 'dedicated' is gone entirely (ADR 0327
|
|
5253
|
+
decision 4) and its rows folded into 'cloud-host', so it needs no label: a row that
|
|
5254
|
+
could show one no longer exists. */
|
|
5251
5255
|
var SHAPE_LABEL = {
|
|
5252
5256
|
'byo-host': 'your own host',
|
|
5253
|
-
|
|
5254
|
-
dedicated: 'dedicated cloud host',
|
|
5257
|
+
'cloud-host': 'cloud host',
|
|
5255
5258
|
'co-tenant': 'shared server (retired)',
|
|
5256
5259
|
'control-plane': 'the control plane'
|
|
5257
5260
|
};
|
|
@@ -5269,9 +5272,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5269
5272
|
and provisioning keep the built-world text on purpose — a run that failed
|
|
5270
5273
|
partway may already have a database, a port and a DNS record. */
|
|
5271
5274
|
if (inst && inst.status === 'requested') return 'Nothing has been stood up yet — this cancels the standup. Anything already on GitHub is untouched, and the name and web address stay reserved for this project.';
|
|
5272
|
-
return
|
|
5273
|
-
? 'This project has a machine to itself, so the machine goes away and its database goes with it. Your repository is untouched. Its web address stays reserved for this project, and nothing answers there until you bring it back. Taking it offline erases nothing.'
|
|
5274
|
-
: 'The server goes away. Your repository is untouched. The database is kept, so bringing it back later restores its memory. Its web address stays reserved for this project, and nothing answers there until you bring it back. Taking it offline erases nothing.';
|
|
5275
|
+
return 'The server goes away. Your repository is untouched. The database is kept, so bringing it back later restores its memory. Its web address stays reserved for this project, and nothing answers there until you bring it back. Taking it offline erases nothing.';
|
|
5275
5276
|
}
|
|
5276
5277
|
|
|
5277
5278
|
/* onManage: the same facts, but the pointers name the controls THIS surface
|
|
@@ -5371,9 +5372,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5371
5372
|
/* with a revive queued, the strip tells the story — this paragraph would
|
|
5372
5373
|
contradict it (F22, the same guard the error paragraph has) */
|
|
5373
5374
|
body += '<div class="projMsg">Torn down' + (inst.torn_down_at ? ' ' + esc(shortDate(inst.torn_down_at)) : '') +
|
|
5374
|
-
|
|
5375
|
-
? '. 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>'
|
|
5376
|
-
: '. 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>');
|
|
5375
|
+
'. 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>';
|
|
5377
5376
|
}
|
|
5378
5377
|
var lm = lastMsg[inst.id];
|
|
5379
5378
|
/* a live region: an action's result is announced where the pointer is,
|
|
@@ -5663,7 +5662,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5663
5662
|
var box = $('mMsg');
|
|
5664
5663
|
box.className = 'projMsg'; box.textContent = verb === 'revive' ? 'Asking the platform to bring it back…' : 'Asking the platform to try again…';
|
|
5665
5664
|
$('mActs').querySelectorAll('button').forEach(function (b) { b.disabled = true; });
|
|
5666
|
-
var body = { slug: inst.slug, hosting_shape: inst.hosting_shape || '
|
|
5665
|
+
var body = { slug: inst.slug, hosting_shape: inst.hosting_shape || 'cloud-host' };
|
|
5667
5666
|
if (inst.onboard_mode) body.onboard_mode = inst.onboard_mode;
|
|
5668
5667
|
if (inst.target_ref) body.target_ref = inst.target_ref;
|
|
5669
5668
|
if (inst.domain) body.domain = inst.domain;
|
|
@@ -5983,7 +5982,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5983
5982
|
function repoPrivDoor(inst, repo) {
|
|
5984
5983
|
if (!inst) return '';
|
|
5985
5984
|
var why = inst.hosting_shape === 'byo-host' ? 'This project runs on your own host, so the platform never reads its code — you can make the repository private on GitHub directly.'
|
|
5986
|
-
: inst.hosting_shape !== '
|
|
5985
|
+
: inst.hosting_shape !== 'cloud-host' ? 'Only a project on a Cloud host can have its repository made private from here.'
|
|
5987
5986
|
: inst.status !== 'active' ? 'You can make it private once the project is running.'
|
|
5988
5987
|
: !repo.admin ? 'Your GitHub account can’t change this repository’s settings.'
|
|
5989
5988
|
: '';
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"status": "provisioning", "planet_template": "blob-frost-tint", "planet_accent": "sun-lit",
|
|
26
26
|
"domain": null,
|
|
27
27
|
"target_ref": "builder/harbor",
|
|
28
|
-
"hosting_shape": "
|
|
28
|
+
"hosting_shape": "cloud-host",
|
|
29
29
|
"onboard_mode": "scaffold",
|
|
30
30
|
"created_at": "$now-6m",
|
|
31
31
|
"provisioned_at": null,
|