@bongos/core 1.19.53 → 1.19.55
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 +13 -23
- package/modules/hall-ui/public/index.html +0 -3
- package/modules/hall-ui/public/style.css +2 -20
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +5 -1
- package/tests/onboard.mjs +6 -6
- package/modules/hall-ui/public/instances-panel.js +0 -198
- package/modules/hall-ui/public/new-project-wizard.js +0 -199
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.55",
|
|
6
|
+
"core_contract": "1.19.55",
|
|
7
|
+
"source_commit": "70c27fe9a074e260e720860e06905fc626d4f2c2",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-11T20:41:26.500Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 291,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1217,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1529,
|
|
20
|
+
"tree_sha256": "911e16dd941f52958189cbcdaab6b68d02011450d0242220d6b5326212e74b5a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -3117,12 +3117,7 @@
|
|
|
3117
3117
|
{
|
|
3118
3118
|
"path": "modules/hall-ui/public/index.html",
|
|
3119
3119
|
"mode": "0000644",
|
|
3120
|
-
"sha256": "
|
|
3121
|
-
},
|
|
3122
|
-
{
|
|
3123
|
-
"path": "modules/hall-ui/public/instances-panel.js",
|
|
3124
|
-
"mode": "0000644",
|
|
3125
|
-
"sha256": "d42ddfb865d8fa928286be158a19a93299050754366b2d625fe2f92dbbd75ade"
|
|
3120
|
+
"sha256": "d26633abd6e6e0b4675357c0537296c05532df94272c6033e72d9eddedce1bab"
|
|
3126
3121
|
},
|
|
3127
3122
|
{
|
|
3128
3123
|
"path": "modules/hall-ui/public/md-reader.js",
|
|
@@ -3154,11 +3149,6 @@
|
|
|
3154
3149
|
"mode": "0000644",
|
|
3155
3150
|
"sha256": "39252c26f1ca28fce403b195868ceae01041daf064507fc5760378c134e9eee5"
|
|
3156
3151
|
},
|
|
3157
|
-
{
|
|
3158
|
-
"path": "modules/hall-ui/public/new-project-wizard.js",
|
|
3159
|
-
"mode": "0000644",
|
|
3160
|
-
"sha256": "ebd0192e70e01cbf36be4eb977039c2368cc325a6cd64f8de9c7530d25f357c4"
|
|
3161
|
-
},
|
|
3162
3152
|
{
|
|
3163
3153
|
"path": "modules/hall-ui/public/not-ready.html",
|
|
3164
3154
|
"mode": "0000644",
|
|
@@ -3292,7 +3282,7 @@
|
|
|
3292
3282
|
{
|
|
3293
3283
|
"path": "modules/hall-ui/public/style.css",
|
|
3294
3284
|
"mode": "0000644",
|
|
3295
|
-
"sha256": "
|
|
3285
|
+
"sha256": "aec0a07f929b88d95d4ec2e7809f0b8cba24855441c8d719a137f2d2f184adf6"
|
|
3296
3286
|
},
|
|
3297
3287
|
{
|
|
3298
3288
|
"path": "modules/hall-ui/public/task-detail.js",
|
|
@@ -4042,12 +4032,12 @@
|
|
|
4042
4032
|
{
|
|
4043
4033
|
"path": "package-lock.json",
|
|
4044
4034
|
"mode": "0000644",
|
|
4045
|
-
"sha256": "
|
|
4035
|
+
"sha256": "f7fb6d01950d858aaeefd581b47bd278c43f32ff4257ba353a790afaf8ce91d5"
|
|
4046
4036
|
},
|
|
4047
4037
|
{
|
|
4048
4038
|
"path": "package.json",
|
|
4049
4039
|
"mode": "0000644",
|
|
4050
|
-
"sha256": "
|
|
4040
|
+
"sha256": "30f7fe88cf022bede5c4f57466b5386414f54ef0d25bc808293544a596c0450a"
|
|
4051
4041
|
},
|
|
4052
4042
|
{
|
|
4053
4043
|
"path": "public-docs/index.html",
|
|
@@ -5667,7 +5657,7 @@
|
|
|
5667
5657
|
{
|
|
5668
5658
|
"path": "src/module-api.js",
|
|
5669
5659
|
"mode": "0000644",
|
|
5670
|
-
"sha256": "
|
|
5660
|
+
"sha256": "5ad5187a73a1fe3812b0fe8dddb01bc1c179fea6ecc7f54cc3549ec5d154fcfb"
|
|
5671
5661
|
},
|
|
5672
5662
|
{
|
|
5673
5663
|
"path": "src/module-loader/loader.js",
|
|
@@ -7032,7 +7022,7 @@
|
|
|
7032
7022
|
{
|
|
7033
7023
|
"path": "tests/onboard.mjs",
|
|
7034
7024
|
"mode": "0000644",
|
|
7035
|
-
"sha256": "
|
|
7025
|
+
"sha256": "61f445855391bfc0e4ab44f8a3f03e074d911e75ed7dbae31293f38314cd3d2e"
|
|
7036
7026
|
},
|
|
7037
7027
|
{
|
|
7038
7028
|
"path": "tests/onboarding_gate.mjs",
|
|
@@ -117,9 +117,6 @@
|
|
|
117
117
|
<!-- ADR 0112 (task 1954): the home-page goal-membership inbox — invitations to
|
|
118
118
|
accept + join-requests to approve. Self-registers; hidden when empty. -->
|
|
119
119
|
<script src="/builders/goal-inbox.js?v=2026-07-10-goal-inbox" defer></script>
|
|
120
|
-
<!-- BV1-prov-P6 (task 1947): the Instances widget — self-registers, gated on the provisioning module. -->
|
|
121
|
-
<script src="/builders/new-project-wizard.js?v=2026-07-04-goal35" defer></script>
|
|
122
|
-
<script src="/builders/instances-panel.js?v=2026-07-03-prov-p6" defer></script>
|
|
123
120
|
<script src="/builders/builders.js?v=2026-06-27-r75" defer></script>
|
|
124
121
|
<script src="/builders/copy-on-click.js?v=2026-06-01-copy-1" defer></script>
|
|
125
122
|
</body>
|
|
@@ -137,7 +137,7 @@ html[data-theme="dark"] body { background: var(--bg); }
|
|
|
137
137
|
onboarding checklist · welcome panel + setup selector · landing
|
|
138
138
|
goals · work-board task cards · ranks page
|
|
139
139
|
security / roster / admin leftovers (dead — removed by task 2179)
|
|
140
|
-
toast + overlays · misc shared bits · widget one-offs (
|
|
140
|
+
toast + overlays · misc shared bits · widget one-offs (triage,
|
|
141
141
|
goal inbox) · hall-kit components (END — the shared kit, task 2166).
|
|
142
142
|
New page-SPECIFIC styles belong in that page's own <page>.css (the
|
|
143
143
|
primer.css precedent), not here; new SHARED components join the kit
|
|
@@ -1860,21 +1860,6 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
1860
1860
|
/* primer's article images should never overflow its column */
|
|
1861
1861
|
.primer-article img { max-width: 100%; }
|
|
1862
1862
|
|
|
1863
|
-
/* Start-a-project wizard (new-project-wizard.js, provisioning module, task 1983) */
|
|
1864
|
-
.np-form { display: grid; gap: 0.75rem; max-width: 32rem; margin-top: 0.5rem; }
|
|
1865
|
-
.np-field { display: grid; gap: 0.25rem; font-size: 0.9rem; color: var(--ink-soft); }
|
|
1866
|
-
.np-field input { padding: 0.5rem 0.6rem; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-card); color: var(--ink); font: inherit; }
|
|
1867
|
-
.np-field input:focus { outline: 2px solid var(--accent-weak); border-color: var(--accent); }
|
|
1868
|
-
.np-error { color: var(--danger); font-size: 0.9rem; min-height: 1.2em; margin: 0; }
|
|
1869
|
-
.np-create-btn { justify-self: start; padding: 0.55rem 1.1rem; border: 0; border-radius: 6px; background: var(--accent); color: var(--accent-text); font: inherit; font-weight: 600; cursor: pointer; }
|
|
1870
|
-
.np-create-btn:hover:not(:disabled) { background: var(--accent-deep); }
|
|
1871
|
-
.np-create-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1872
|
-
.np-steps { display: grid; gap: 0.6rem; padding-left: 1.2rem; margin: 0.5rem 0; }
|
|
1873
|
-
.np-kv { display: flex; gap: 0.5rem; align-items: baseline; margin-top: 0.25rem; flex-wrap: wrap; }
|
|
1874
|
-
.np-kv span { color: var(--ink-faint); font-size: 0.85rem; min-width: 12rem; }
|
|
1875
|
-
.np-kv code, .np-steps code { background: var(--surface-2); padding: 0.1rem 0.35rem; border-radius: 4px; word-break: break-all; }
|
|
1876
|
-
.np-done { margin-top: 0.5rem; }
|
|
1877
|
-
|
|
1878
1863
|
/* ---- task 1471: idea-inbox triage cue (Home widget, Metic+) ---- */
|
|
1879
1864
|
/* Shared .count-pill — work.html defines its own page-scoped copy; the hall
|
|
1880
1865
|
index widget needs the same look, so it lives in the shared sheet now. */
|
|
@@ -2021,15 +2006,12 @@ a.stat-tile:hover {
|
|
|
2021
2006
|
border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
|
|
2022
2007
|
background: color-mix(in srgb, var(--accent) 3%, var(--surface));
|
|
2023
2008
|
}
|
|
2024
|
-
/* Projects card header: title left, "Start a project" disclosure right. */
|
|
2025
|
-
.proj-head { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
|
2026
|
-
.proj-head .scroll__h { margin: 0; }
|
|
2027
2009
|
/* The over-budget "More" row — one quiet line, not a card. */
|
|
2028
2010
|
.home-more { margin: 0; font-size: 13px; color: var(--ink-soft); }
|
|
2029
2011
|
|
|
2030
2012
|
/* task 2184: two-column main zone on wide screens — Home reads as a
|
|
2031
2013
|
dashboard, not a stack. The attention inbox (and the More row) span both
|
|
2032
|
-
columns; the
|
|
2014
|
+
columns; the home-main cards sit side by side. Mirrors the
|
|
2033
2015
|
.view-zone--band grid idiom (cards drop their own margins inside a grid). */
|
|
2034
2016
|
#zone-home-main { display: grid; grid-template-columns: minmax(0, 1fr); gap: 20px; align-items: start; }
|
|
2035
2017
|
#zone-home-main > .scroll { margin: 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.55",
|
|
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.55",
|
|
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.55",
|
|
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",
|
package/src/module-api.js
CHANGED
|
@@ -533,8 +533,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
533
533
|
// landed since 1.19.51 with no explicit bump. run 31526131183. (task 1002620)
|
|
534
534
|
// 1.19.53 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
535
535
|
// landed since 1.19.52 with no explicit bump. run 31527057565. (task 1002620)
|
|
536
|
+
// 1.19.54 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
537
|
+
// landed since 1.19.53 with no explicit bump. run 31531157867. (task 1002620)
|
|
538
|
+
// 1.19.55 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
539
|
+
// landed since 1.19.54 with no explicit bump. run 31534150701. (task 1002620)
|
|
536
540
|
// ---------------------------------------------------------------------------
|
|
537
|
-
const CORE_VERSION = '1.19.
|
|
541
|
+
const CORE_VERSION = '1.19.55'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
538
542
|
|
|
539
543
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
540
544
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/onboard.mjs
CHANGED
|
@@ -97,18 +97,19 @@ test('onboardPlan is the SAME function the provisioning module owns (single sour
|
|
|
97
97
|
assert.equal(ob.onboardPlan, canonical, 'scripts/gds/onboard.js must re-export the module’s onboardPlan');
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
test('the web
|
|
100
|
+
test('the web form (/create) declares EXACTLY the canonical human steps — drift guard', () => {
|
|
101
101
|
// goal 35 / criterion 165: "the canonical step sequence is the source of truth;
|
|
102
|
-
// skill/CLI/
|
|
102
|
+
// skill/CLI/web are forms over it." The web form tags its human-step <li>s with
|
|
103
103
|
// data-onboard-step="<key>"; this asserts that set equals the plan's human-step keys,
|
|
104
104
|
// so adding/removing/renaming a human step in the single source fails CI until every
|
|
105
105
|
// form is updated (and vice-versa) — the sequence can no longer drift between forms.
|
|
106
|
+
// (The hall new-project wizard was removed in task 1002675 — project creation lives
|
|
107
|
+
// on the apex /create flow; the skill + CLI + /create remain the canonical forms.)
|
|
106
108
|
const { readFileSync } = require('node:fs');
|
|
107
109
|
const path = require('node:path');
|
|
108
110
|
const humanKeys = ob.onboardPlan().filter((s) => s.kind === 'human').map((s) => s.key).sort();
|
|
109
111
|
const surfaces = {
|
|
110
112
|
'/create landing page': 'public-landing/create.html',
|
|
111
|
-
'hall new-project wizard': 'modules/hall-ui/public/new-project-wizard.js',
|
|
112
113
|
};
|
|
113
114
|
for (const [name, rel] of Object.entries(surfaces)) {
|
|
114
115
|
const src = readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');
|
|
@@ -120,13 +121,12 @@ test('the web forms (/create + hall wizard) declare EXACTLY the canonical human
|
|
|
120
121
|
|
|
121
122
|
test('the one-click GitHub-App manifest path surfaces the CLI-only Enable-Device-Flow step (task 2129)', () => {
|
|
122
123
|
// GitHub defaults Device Flow OFF and the manifest can't enable it, but `bongos login` needs
|
|
123
|
-
// it — so
|
|
124
|
+
// it — so the manifest surface must tell the owner to enable it after the one-click approval.
|
|
124
125
|
// Assert on a phrase DISTINCT to the one-click caveat: the manual OAuth path already mentions
|
|
125
126
|
// Device Flow (task 2127), so a bare "Device Flow" match wouldn't prove the manifest path carries it.
|
|
127
|
+
// (`bongos onboard` is the only manifest surface since the hall wizard's removal, task 1002675.)
|
|
126
128
|
const { readFileSync } = require('node:fs');
|
|
127
129
|
const path = require('node:path');
|
|
128
130
|
const onboardSrc = readFileSync(path.join(import.meta.dirname, '..', 'scripts/gds/onboard.js'), 'utf8');
|
|
129
|
-
const wizardSrc = readFileSync(path.join(import.meta.dirname, '..', 'modules/hall-ui/public/new-project-wizard.js'), 'utf8');
|
|
130
131
|
assert.match(onboardSrc, /cannot enable Device Flow via the manifest/i, 'bongos onboard one-click caveat missing');
|
|
131
|
-
assert.match(wizardSrc, /Terminal CLI only: after approving, enable/i, 'hall wizard one-click caveat missing');
|
|
132
132
|
});
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
// instances-panel.js — the "Instances" hall widget (provisioning module, ADR 0111
|
|
2
|
-
// §1, task 1947 / BV1-prov-P6).
|
|
3
|
-
//
|
|
4
|
-
// Surfaces provisioning_instances STATE + cost estimate: an owner sees their own
|
|
5
|
-
// instance(s) + request status; an Archon additionally sees the fleet roster + the
|
|
6
|
-
// cost ledger total. One cohesive panel — the caller's own instances always, the
|
|
7
|
-
// fleet appended only when /me returns rank 'archon'.
|
|
8
|
-
//
|
|
9
|
-
// GATED on the provisioning module: registered with module:'provisioning', so
|
|
10
|
-
// contributeHallWidget (dom-utils.js) drops it entirely on an instance that hasn't
|
|
11
|
-
// enabled the module — the belt-and-suspenders to window.__MODULES__ (ADR 0062 §4).
|
|
12
|
-
//
|
|
13
|
-
// Reuses the dev-box hall pattern: a self-contained, self-fetching widget (the
|
|
14
|
-
// goals.js precedent), living in the hall's own served dir so it serves on both the
|
|
15
|
-
// apex and the builders subdomain. Reads the P1 routes only:
|
|
16
|
-
// GET /provisioning/instances (own)
|
|
17
|
-
// GET /provisioning/fleet (archon)
|
|
18
|
-
// GET /provisioning/fleet/cost-ledger (archon)
|
|
19
|
-
// READ/STATE only — no mutations. Creating a request is `bongos init` (P5) or the
|
|
20
|
-
// provisioning API; request/teardown buttons here are a deliberate follow-up.
|
|
21
|
-
(function () {
|
|
22
|
-
'use strict';
|
|
23
|
-
if (!window.OTB || typeof window.OTB.contributeHallWidget !== 'function') return;
|
|
24
|
-
const { escapeHtml } = window.OTB;
|
|
25
|
-
const API = '/api/bongos';
|
|
26
|
-
// Generated typed client (task 1996 / R09), served as window.BongosClient by
|
|
27
|
-
// /lib/bongos-client.js. baseUrl:'' passes the full same-origin paths this file
|
|
28
|
-
// builds through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
|
|
29
|
-
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
30
|
-
|
|
31
|
-
async function getJSON(path, { softAuth = false } = {}) {
|
|
32
|
-
const res = await api.request('GET', path);
|
|
33
|
-
if (res.status === 401) {
|
|
34
|
-
if (softAuth) { const e = new Error('auth-needed'); e.code = 'auth-needed'; throw e; }
|
|
35
|
-
window.location.assign(`${API}/auth/web/start`);
|
|
36
|
-
throw new Error('redirecting to auth');
|
|
37
|
-
}
|
|
38
|
-
if (!res.ok) throw new Error(`${path} → ${res.status}`);
|
|
39
|
-
return res.data;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let host = null;
|
|
43
|
-
|
|
44
|
-
// Lifecycle status → a badge class from the shared goal-badge family (achieved =
|
|
45
|
-
// live/green, open = in-flight/neutral, archived = gone/muted). Mirrors the states
|
|
46
|
-
// in provisioning_001_tables.sql / provisioning.INSTANCE_STATES.
|
|
47
|
-
const STATUS_CLASS = {
|
|
48
|
-
active: 'goal-badge--achieved',
|
|
49
|
-
requested: 'goal-badge--open',
|
|
50
|
-
provisioning: 'goal-badge--open',
|
|
51
|
-
tearing_down: 'goal-badge--archived',
|
|
52
|
-
torn_down: 'goal-badge--archived',
|
|
53
|
-
error: 'goal-badge--archived',
|
|
54
|
-
};
|
|
55
|
-
function statusBadge(status) {
|
|
56
|
-
const cls = STATUS_CLASS[status] || 'goal-badge--open';
|
|
57
|
-
return `<span class="goal-badge ${cls}">${escapeHtml(status || 'unknown')}</span>`;
|
|
58
|
-
}
|
|
59
|
-
// Monthly cost — co-tenant is $0 (shares an existing box); a dedicated droplet
|
|
60
|
-
// carries its tier's estimate. VISIBILITY only (ADR 0111 §6), never a bill.
|
|
61
|
-
function usdMonthly(n) {
|
|
62
|
-
const v = Number(n) || 0;
|
|
63
|
-
return v > 0 ? `$${v.toFixed(2)}/mo` : '$0';
|
|
64
|
-
}
|
|
65
|
-
function openIntentNote(inst) {
|
|
66
|
-
if (!inst.open_intent) return '';
|
|
67
|
-
return ` <span class="ledger__note">(${escapeHtml(inst.open_intent.action)} ${escapeHtml(inst.open_intent.state)})</span>`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function ownRow(inst) {
|
|
71
|
-
return `<tr>
|
|
72
|
-
<td>${escapeHtml(inst.slug || '—')}</td>
|
|
73
|
-
<td>${statusBadge(inst.status)}${openIntentNote(inst)}</td>
|
|
74
|
-
<td>${escapeHtml(inst.hosting_shape || '—')}</td>
|
|
75
|
-
<td>${inst.domain ? escapeHtml(inst.domain) : '—'}</td>
|
|
76
|
-
<td>${usdMonthly(inst.cost_estimate_usd)}</td>
|
|
77
|
-
</tr>`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function fleetRow(inst) {
|
|
81
|
-
const owner = inst.owner || {};
|
|
82
|
-
return `<tr>
|
|
83
|
-
<td>${escapeHtml(inst.slug || '—')}</td>
|
|
84
|
-
<td>${escapeHtml(owner.github_login || owner.display_name || '—')}</td>
|
|
85
|
-
<td>${statusBadge(inst.status)}</td>
|
|
86
|
-
<td>${escapeHtml(inst.hosting_shape || '—')}</td>
|
|
87
|
-
<td>${inst.tier ? escapeHtml(inst.tier) : '—'}</td>
|
|
88
|
-
<td>${usdMonthly(inst.cost_estimate_usd)}</td>
|
|
89
|
-
</tr>`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Paint the caller's own instances into #instances-own-body.
|
|
93
|
-
function renderOwn(instances) {
|
|
94
|
-
const body = host.querySelector('#instances-own-body');
|
|
95
|
-
if (!body) return;
|
|
96
|
-
if (!instances.length) {
|
|
97
|
-
body.innerHTML = '<p class="goal-empty">No instances yet. Create one with <code>bongos init</code> or the provisioning API — it’ll show here with its status and cost.</p>';
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
body.innerHTML =
|
|
101
|
-
'<table class="ledger"><thead><tr><th>Instance</th><th>Status</th><th>Shape</th><th>Domain</th><th>Cost</th></tr></thead>' +
|
|
102
|
-
`<tbody>${instances.map(ownRow).join('')}</tbody></table>`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Paint the fleet + cost-ledger total into #instances-fleet (Archon only). Reveals
|
|
106
|
-
// the section (hidden by default in the skeleton) once data is in hand.
|
|
107
|
-
function renderFleet(fleet, ledger) {
|
|
108
|
-
const wrap = host.querySelector('#instances-fleet');
|
|
109
|
-
const body = host.querySelector('#instances-fleet-body');
|
|
110
|
-
if (!wrap || !body) return;
|
|
111
|
-
wrap.hidden = false;
|
|
112
|
-
if (!fleet.length) {
|
|
113
|
-
body.innerHTML = '<p class="goal-empty">No provisioned instances across the fleet yet.</p>';
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const total = ledger && typeof ledger.total_monthly_usd === 'number' ? ledger.total_monthly_usd : null;
|
|
117
|
-
body.innerHTML =
|
|
118
|
-
'<table class="ledger"><thead><tr><th>Instance</th><th>Owner</th><th>Status</th><th>Shape</th><th>Tier</th><th>Cost</th></tr></thead>' +
|
|
119
|
-
`<tbody>${fleet.map(fleetRow).join('')}</tbody></table>` +
|
|
120
|
-
(total !== null ? `<p class="ledger__note">Fleet estimate: <strong>${usdMonthly(total)}</strong> across ${fleet.length} instance${fleet.length === 1 ? '' : 's'} (co-tenant instances are $0).</p>` : '');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function load() {
|
|
124
|
-
if (!host) return;
|
|
125
|
-
try {
|
|
126
|
-
// Own instances (any authenticated builder).
|
|
127
|
-
const mine = await getJSON(`${API}/provisioning/instances`);
|
|
128
|
-
renderOwn(Array.isArray(mine.instances) ? mine.instances : []);
|
|
129
|
-
} catch (err) {
|
|
130
|
-
const body = host.querySelector('#instances-own-body');
|
|
131
|
-
if (body) body.innerHTML = '<p class="goal-empty">Your instances couldn’t be loaded just now. Refresh to try again.</p>';
|
|
132
|
-
console.error('[hall] instances load failed', err);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Archon-only: the fleet roster + cost ledger. Read /me softly; a non-Archon (or
|
|
136
|
-
// an auth hiccup) simply never reveals the fleet section.
|
|
137
|
-
try {
|
|
138
|
-
const me = await getJSON(`${API}/me`, { softAuth: true }).catch(() => null);
|
|
139
|
-
const rank = me && me.builder ? me.builder.rank : null;
|
|
140
|
-
// ADR 0157: `provisioning.fleet.manage` moved archon→metic, so the fleet section
|
|
141
|
-
// reveals for Metic+ too. Cosmetic — the route gates are the wall.
|
|
142
|
-
if (rank !== 'metic' && rank !== 'archon') return;
|
|
143
|
-
const [fleet, ledger] = await Promise.all([
|
|
144
|
-
getJSON(`${API}/provisioning/fleet`).catch(() => ({ instances: [] })),
|
|
145
|
-
getJSON(`${API}/provisioning/fleet/cost-ledger`).catch(() => null),
|
|
146
|
-
]);
|
|
147
|
-
renderFleet(Array.isArray(fleet.instances) ? fleet.instances : [], ledger);
|
|
148
|
-
} catch (err) {
|
|
149
|
-
console.error('[hall] instances fleet load failed', err);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// task 2169: this is the "Projects" card — instances state + the
|
|
154
|
-
// Start-a-project wizard merged. The wizard (an idle multi-step form, the
|
|
155
|
-
// page's worst length offender as its own card) renders ON DEMAND behind the
|
|
156
|
-
// header button, via window.OTBNewProject (new-project-wizard.js).
|
|
157
|
-
const SKELETON =
|
|
158
|
-
'<div class="proj-head">' +
|
|
159
|
-
'<h2 id="instances-h" class="scroll__h">Projects</h2>' +
|
|
160
|
-
'<button type="button" class="btn-ghost" id="new-project-toggle" aria-expanded="false" aria-controls="new-project-mount">+ Start a project</button>' +
|
|
161
|
-
'</div>' +
|
|
162
|
-
'<p class="scroll__lede">Hosting you’ve provisioned through Cloud Bongos — each instance’s status and estimated monthly cost. Provision one right here, or with <code>bongos init</code>.</p>' +
|
|
163
|
-
'<div id="new-project-mount" hidden></div>' +
|
|
164
|
-
'<div id="instances-own-body"><div class="profile__placeholder">Loading your instances…</div></div>' +
|
|
165
|
-
'<div id="instances-fleet" hidden>' +
|
|
166
|
-
'<h3 class="scroll__h" style="font-size:1rem;margin-top:1rem">Fleet</h3>' +
|
|
167
|
-
'<p class="scroll__lede">Every owner’s instances and the fleet cost estimate (Archon view).</p>' +
|
|
168
|
-
'<div id="instances-fleet-body"><div class="profile__placeholder">Loading the fleet…</div></div>' +
|
|
169
|
-
'</div>';
|
|
170
|
-
|
|
171
|
-
function mount(hostEl) {
|
|
172
|
-
host = hostEl;
|
|
173
|
-
host.innerHTML = SKELETON;
|
|
174
|
-
const toggle = host.querySelector('#new-project-toggle');
|
|
175
|
-
const wizardMount = host.querySelector('#new-project-mount');
|
|
176
|
-
let wizardRendered = false;
|
|
177
|
-
toggle?.addEventListener('click', () => {
|
|
178
|
-
if (!wizardRendered && window.OTBNewProject) {
|
|
179
|
-
window.OTBNewProject.renderInto(wizardMount);
|
|
180
|
-
wizardRendered = true;
|
|
181
|
-
}
|
|
182
|
-
const open = wizardMount.hidden;
|
|
183
|
-
wizardMount.hidden = !open;
|
|
184
|
-
toggle.setAttribute('aria-expanded', String(open));
|
|
185
|
-
toggle.textContent = open ? '− Start a project' : '+ Start a project';
|
|
186
|
-
});
|
|
187
|
-
load();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// summary: true (task 2177) — a module-contributed home-main card must be
|
|
191
|
-
// row-capped with no pager/filter. This one lists the builder's instances and
|
|
192
|
-
// links out to the full surface, which is exactly the summary shape.
|
|
193
|
-
window.OTB.contributeHallWidget({
|
|
194
|
-
id: 'instances-scroll', label: 'Projects', order: 33,
|
|
195
|
-
module: 'provisioning', view: 'home', zone: 'home-main', summary: true,
|
|
196
|
-
labelledby: 'instances-h', mount,
|
|
197
|
-
});
|
|
198
|
-
})();
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
// new-project-wizard.js — the "Start a project" flow (provisioning module,
|
|
2
|
-
// ADR 0111 / BONGOS-V1 goal 35, task 1983).
|
|
3
|
-
//
|
|
4
|
-
// The most ACCESSIBLE form of the canonical onboarding runbook (the /new-project skill,
|
|
5
|
-
// task 1981; the `bongos onboard` CLI, task 1982) — a point-and-click wizard for a
|
|
6
|
-
// non-technical owner. It files the provisioning REQUEST (the web tier only ENQUEUES —
|
|
7
|
-
// POST /provisioning/instances; the control-plane runner does the real standup, trust
|
|
8
|
-
// boundary intact, ADR 0111 §2) as a STANDALONE instance (ADR 0108), then renders the
|
|
9
|
-
// four irreducibly-human steps UI-first (create the repo, the OAuth app + secret, first
|
|
10
|
-
// sign-in) with the exact pinned values, and points at the Instances panel for status.
|
|
11
|
-
//
|
|
12
|
-
// NOT a hall widget — it registers nothing with contributeHallWidget and owns no
|
|
13
|
-
// zone. It exposes window.OTBNewProject, which instances-panel.js's header button
|
|
14
|
-
// opens as a modal; that panel carries the provisioning gate + the home-main budget
|
|
15
|
-
// for both. (Correcting a stale header, task 2177: this file used to describe itself
|
|
16
|
-
// as a registered widget, which is why the "wizard" in that task's acceptance list
|
|
17
|
-
// could not be found in the registry.)
|
|
18
|
-
(function () {
|
|
19
|
-
'use strict';
|
|
20
|
-
if (!window.OTB || typeof window.OTB.contributeHallWidget !== 'function') return;
|
|
21
|
-
const { escapeHtml } = window.OTB;
|
|
22
|
-
const API = '/api/bongos';
|
|
23
|
-
|
|
24
|
-
// Mirror provisioning.isValidSlug (DNS/db-safe): 2–40 of [a-z0-9-], starts+ends
|
|
25
|
-
// alnum, no double dash. Client-side pre-check; the server re-validates.
|
|
26
|
-
function validSlug(s) {
|
|
27
|
-
const v = String(s || '');
|
|
28
|
-
return v.length >= 2 && v.length <= 40 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(v) && !v.includes('--');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// The pinned GitHub OAuth-app values for a domain (auth.js WEB_REDIRECT_ORIGIN —
|
|
32
|
-
// callback is the apex /api/gds/auth/web/callback). Mirrors onboard.oauthAppInstructions.
|
|
33
|
-
function oauthAppValues(domain) {
|
|
34
|
-
const origin = `https://${domain}`;
|
|
35
|
-
return { homepageUrl: origin, callbackUrl: `${origin}/api/gds/auth/web/callback` };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let host = null;
|
|
39
|
-
|
|
40
|
-
async function postJSON(path, body) {
|
|
41
|
-
const res = await fetch(path, {
|
|
42
|
-
method: 'POST', credentials: 'same-origin',
|
|
43
|
-
headers: { 'Content-Type': 'application/json' },
|
|
44
|
-
body: JSON.stringify(body),
|
|
45
|
-
});
|
|
46
|
-
if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); throw new Error('redirecting to auth'); }
|
|
47
|
-
const data = await res.json().catch(() => ({}));
|
|
48
|
-
if (!res.ok) { const e = new Error((data.error && (data.error.message || data.error.code)) || `HTTP ${res.status}`); e.data = data; throw e; }
|
|
49
|
-
return data;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// File a GitHub-App-Manifest onboarding link for the just-created instance (task 2080,
|
|
53
|
-
// ADR 0133) and NAVIGATE the browser to GitHub's pre-filled "create App" screen. The
|
|
54
|
-
// manifest rides a hidden form field; GitHub echoes our one-time `state`. After the
|
|
55
|
-
// owner approves, GitHub redirects to the mothership callback and the control-plane
|
|
56
|
-
// runner places the creds automatically — no secret is ever pasted. `account` is
|
|
57
|
-
// 'personal' | 'org'.
|
|
58
|
-
async function launchGithubApp(instanceId, account) {
|
|
59
|
-
const data = await postJSON(`${API}/provisioning/instances/${instanceId}/github-app`, account ? { account } : {});
|
|
60
|
-
const form = document.createElement('form');
|
|
61
|
-
form.method = 'POST';
|
|
62
|
-
form.action = data.post_url; // https://github.com/…/settings/apps/new?state=<one-time>
|
|
63
|
-
const input = document.createElement('input');
|
|
64
|
-
input.type = 'hidden';
|
|
65
|
-
input.name = 'manifest';
|
|
66
|
-
input.value = JSON.stringify(data.manifest);
|
|
67
|
-
form.appendChild(input);
|
|
68
|
-
document.body.appendChild(form);
|
|
69
|
-
form.submit(); // navigates away to GitHub
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// After the request is filed, render the human-only next steps + a pointer to the
|
|
73
|
-
// Instances panel for status. The FIRST step branches on greenfield vs adopt (ADR
|
|
74
|
-
// 0121 §Decision 1 / task 2046). GitHub sign-in is now ONE CLICK via the App Manifest
|
|
75
|
-
// flow (task 2080) — the manual OAuth-app path is kept as a collapsible fallback.
|
|
76
|
-
function renderNextSteps(instanceId, slug, domain, mode) {
|
|
77
|
-
const adopt = mode === 'adopt';
|
|
78
|
-
const oa = domain ? oauthAppValues(domain) : null;
|
|
79
|
-
const dnsNote = domain ? '' : '<li>Add a domain you own, then re-run to wire the address (a domain is needed before sign-in works).</li>';
|
|
80
|
-
const repoStep = adopt
|
|
81
|
-
? `<li data-onboard-step="github-repo"><strong>Layer onto your existing repo</strong> — in your repo run <code>bongos init --adopt</code>. It shows a conflict pre-flight (migrations, CI, protected paths) and writes <em>nothing</em> until you accept; it never overwrites your files, and it imports your existing backlog (GitHub issues / a TODO/ROADMAP file). Commit + push.</li>`
|
|
82
|
-
: '<li data-onboard-step="github-repo"><strong>Create the empty GitHub repo</strong> — github.com → New repository, leave it empty.</li>';
|
|
83
|
-
// The one-click GitHub sign-in step + a collapsible manual fallback (only when a
|
|
84
|
-
// domain is set — the App needs a real callback host).
|
|
85
|
-
const signInStep = oa ? `<li data-onboard-step="oauth-app"><strong>Set up GitHub sign-in — one click</strong>. We’ll open GitHub with a pre-filled app; approve it, and we place the credentials on your instance automatically (no secret to copy).
|
|
86
|
-
<div class="np-signin-actions">
|
|
87
|
-
<button id="np-github-app" type="button" class="np-create-btn" data-instance="${escapeHtml(String(instanceId))}">Set up GitHub sign-in</button>
|
|
88
|
-
<span id="np-github-app-msg" class="ledger__note"></span>
|
|
89
|
-
</div>
|
|
90
|
-
<p class="ledger__note">Terminal CLI only: after approving, enable <strong>Device Flow</strong> on the app (GitHub → Settings → Developer settings → GitHub Apps → your app → “Enable Device Flow”) so <code>bongos login</code> works. Web sign-in needs nothing.</p>
|
|
91
|
-
<details class="np-manual"><summary class="ledger__note">Prefer to do it by hand?</summary>
|
|
92
|
-
<ol class="np-steps">
|
|
93
|
-
<li><strong>Create the GitHub OAuth app</strong> — GitHub → <em>your account's</em> Settings → Developer settings → OAuth Apps → New OAuth App.
|
|
94
|
-
<div class="np-kv"><span>Homepage URL</span><code>${escapeHtml(oa.homepageUrl)}</code></div>
|
|
95
|
-
<div class="np-kv"><span>Authorization callback URL</span><code>${escapeHtml(oa.callbackUrl)}</code></div>
|
|
96
|
-
<div class="np-kv"><span>Enable Device Flow</span><code>✅ required — the terminal login uses it (off by default)</code></div>
|
|
97
|
-
</li>
|
|
98
|
-
<li><strong>Place the secret — verified</strong> (never hand-edit the env file): on the control plane run
|
|
99
|
-
<code>bongos … oauth-secret place ${escapeHtml(slug)} --client-id <id> --client-secret <secret> --apply</code>.</li>
|
|
100
|
-
</ol>
|
|
101
|
-
</details>
|
|
102
|
-
</li>` : '';
|
|
103
|
-
return `<div class="np-done">
|
|
104
|
-
<p class="scroll__lede">✓ Provisioning requested for <strong>${escapeHtml(slug)}</strong> (standalone${adopt ? ', adopting your existing repo' : ''}). The control plane stands it up — watch its status in <strong>Instances</strong> below.</p>
|
|
105
|
-
<h3 class="scroll__h" style="font-size:1rem">Your steps (the parts only you can do):</h3>
|
|
106
|
-
<ol class="np-steps">
|
|
107
|
-
${repoStep}
|
|
108
|
-
${dnsNote}
|
|
109
|
-
${signInStep}
|
|
110
|
-
${oa ? `<li data-onboard-step="first-sign-in"><strong>First sign-in</strong> — once the instance is live, open <code>${escapeHtml(oa.homepageUrl)}</code> and sign in with GitHub; that makes you the owner (Archon).</li>
|
|
111
|
-
<li><strong>Verify</strong> — <code>${escapeHtml(oa.homepageUrl)}/healthz</code> should report <code>auth_configured: true</code>.</li>` : ''}
|
|
112
|
-
</ol>
|
|
113
|
-
<p class="ledger__note">This is the same sequence the <code>/new-project</code> assistant and <code>bongos onboard</code> use.</p>
|
|
114
|
-
</div>`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function submit() {
|
|
118
|
-
const q = (id) => host.querySelector(id);
|
|
119
|
-
const productName = q('#np-name').value.trim();
|
|
120
|
-
const owner = q('#np-owner').value.trim();
|
|
121
|
-
const repo = q('#np-repo').value.trim();
|
|
122
|
-
const slug = q('#np-slug').value.trim().toLowerCase();
|
|
123
|
-
const domain = q('#np-domain').value.trim();
|
|
124
|
-
const mode = q('#np-mode') ? q('#np-mode').value : 'greenfield'; // greenfield | adopt (ADR 0121)
|
|
125
|
-
const err = q('#np-error');
|
|
126
|
-
err.textContent = '';
|
|
127
|
-
|
|
128
|
-
if (!productName) { err.textContent = 'Give the project a name.'; return; }
|
|
129
|
-
if (!owner || !repo) { err.textContent = 'Give the GitHub repo (owner and name).'; return; }
|
|
130
|
-
if (!validSlug(slug)) { err.textContent = 'Slug must be 2–40 lowercase letters/numbers/dashes (no leading/trailing/double dash).'; return; }
|
|
131
|
-
|
|
132
|
-
const btn = q('#np-create');
|
|
133
|
-
btn.disabled = true; btn.textContent = 'Requesting…';
|
|
134
|
-
try {
|
|
135
|
-
const data = await postJSON(`${API}/provisioning/instances`, {
|
|
136
|
-
slug,
|
|
137
|
-
hosting_shape: 'standalone',
|
|
138
|
-
domain: domain || undefined,
|
|
139
|
-
target_ref: `${owner}/${repo}`,
|
|
140
|
-
// The mode is LOAD-BEARING for the control-plane runner, not just for which
|
|
141
|
-
// checklist we render (task 1002562): 'adopt' makes it clone + layer onto the
|
|
142
|
-
// owner's existing history instead of pushing a fresh tree, which an existing
|
|
143
|
-
// repo rejects. This form already tracked `mode` purely for renderNextSteps —
|
|
144
|
-
// it has to reach the server too.
|
|
145
|
-
onboard_mode: mode,
|
|
146
|
-
});
|
|
147
|
-
const instanceId = data && data.instance ? data.instance.id : null;
|
|
148
|
-
q('#np-form').hidden = true;
|
|
149
|
-
q('#np-result').innerHTML = renderNextSteps(instanceId, slug, domain, mode);
|
|
150
|
-
// Wire the one-click GitHub sign-in button (task 2080), if a domain gave us one.
|
|
151
|
-
const gh = q('#np-github-app');
|
|
152
|
-
if (gh) gh.addEventListener('click', async () => {
|
|
153
|
-
const msg = q('#np-github-app-msg');
|
|
154
|
-
gh.disabled = true; gh.textContent = 'Opening GitHub…'; if (msg) msg.textContent = '';
|
|
155
|
-
try {
|
|
156
|
-
await launchGithubApp(gh.getAttribute('data-instance'));
|
|
157
|
-
} catch (e2) {
|
|
158
|
-
gh.disabled = false; gh.textContent = 'Set up GitHub sign-in';
|
|
159
|
-
if (msg) msg.textContent = e2 && e2.message ? `Couldn’t start it: ${e2.message}` : 'Couldn’t start it — try again.';
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
} catch (e) {
|
|
163
|
-
err.textContent = e && e.message ? `Couldn’t request it: ${e.message}` : 'Couldn’t request it — try again.';
|
|
164
|
-
btn.disabled = false; btn.textContent = 'Create project';
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const SKELETON =
|
|
169
|
-
'<h2 id="np-h" class="scroll__h">Start a project</h2>' +
|
|
170
|
-
'<p class="scroll__lede">Stand up a <strong>standalone</strong> Cloud Bongos instance — its own repo running a pinned copy of the platform. Start fresh, or adopt a repo you already have. We file the request and walk you through the few steps only you can do.</p>' +
|
|
171
|
-
'<div id="np-form" class="np-form">' +
|
|
172
|
-
'<label class="np-field"><span>Project name</span><input id="np-name" type="text" placeholder="Mercury" autocomplete="off"></label>' +
|
|
173
|
-
'<label class="np-field"><span>Starting from</span><select id="np-mode">' +
|
|
174
|
-
'<option value="greenfield">A new, empty repo (greenfield)</option>' +
|
|
175
|
-
'<option value="adopt">A repo I already have (adopt)</option>' +
|
|
176
|
-
'</select></label>' +
|
|
177
|
-
'<label class="np-field"><span>GitHub repo owner</span><input id="np-owner" type="text" placeholder="acme" autocomplete="off"></label>' +
|
|
178
|
-
'<label class="np-field"><span>GitHub repo name</span><input id="np-repo" type="text" placeholder="mercury" autocomplete="off"></label>' +
|
|
179
|
-
'<label class="np-field"><span>Instance slug</span><input id="np-slug" type="text" placeholder="mercury" autocomplete="off"></label>' +
|
|
180
|
-
'<label class="np-field"><span>Domain you own <span class="ledger__note">(optional now)</span></span><input id="np-domain" type="text" placeholder="mercury.example.com" autocomplete="off"></label>' +
|
|
181
|
-
'<p id="np-error" class="np-error" role="alert"></p>' +
|
|
182
|
-
'<button id="np-create" type="button" class="np-create-btn">Create project</button>' +
|
|
183
|
-
'</div>' +
|
|
184
|
-
'<div id="np-result"></div>';
|
|
185
|
-
|
|
186
|
-
function mount(hostEl) {
|
|
187
|
-
host = hostEl;
|
|
188
|
-
host.innerHTML = SKELETON;
|
|
189
|
-
const btn = host.querySelector('#np-create');
|
|
190
|
-
if (btn) btn.addEventListener('click', submit);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// task 2169: the wizard no longer registers its own always-open Home card —
|
|
194
|
-
// an idle multi-step form was the page's worst length offender. It renders
|
|
195
|
-
// on demand into the Projects card (instances-panel.js) behind its
|
|
196
|
-
// "Start a project" disclosure button. Module gating is inherited: only the
|
|
197
|
-
// provisioning-gated Projects card ever calls this.
|
|
198
|
-
window.OTBNewProject = { renderInto: mount };
|
|
199
|
-
})();
|