@bongos/core 1.21.82 → 1.21.84
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 +63 -43
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0369-ship-mode-decides-who-starts-a-ship-not-how-it-lands.md +1 -0
- package/docs/api/openapi.json +8 -1
- package/docs/api-reference.md +2 -2
- package/docs/copy-inventory.md +32 -28
- package/docs/copy-registry.json +70 -34
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +9 -1
- package/docs/page-readings.json +657 -648
- package/modules/platform-identity/invite-notices.js +78 -10
- package/modules/platform-identity/migrations/platform_identity_031_parked_invite_notices.sql +45 -0
- package/modules/platform-identity/routes/my-projects.js +7 -3
- package/modules/provisioning/birth.js +28 -1
- package/modules/provisioning/routes/provisioning.js +3 -1
- package/modules/public-landing/public/account.html +1 -0
- package/modules/public-landing/public/assets/cosmos.css +8 -0
- package/modules/public-landing/public/assets/invite-badge.js +91 -0
- package/modules/public-landing/public/contact.html +1 -0
- package/modules/public-landing/public/guild.html +1 -0
- package/modules/public-landing/public/invitations.html +7 -2
- package/modules/public-landing/public/privacy.html +1 -0
- package/modules/public-landing/public/projects.html +49 -5
- package/modules/public-landing/public/projects.states.json +1 -0
- package/modules/public-landing/public/terms.html +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/project-settings.js +16 -2
- package/src/branding.js +3 -0
- package/src/module-api.js +1 -1
- package/tests/hub_invite_badge.mjs +204 -0
- package/tests/hub_invite_notices.mjs +61 -6
- package/tests/landing_legal_pages.mjs +1 -1
- package/tests/projects_hub.mjs +1 -1
- package/tests/projects_hub_app_step.mjs +1 -1
- package/tests/provision_settings_apply.mjs +4 -1
- package/tests/provisioning_settings_env.mjs +3 -1
- package/tests/wizard_physics_more.mjs +5 -5
- package/tests/wizard_ship_mode.mjs +187 -0
- package/tests/wizard_six_screens.mjs +3 -2
|
@@ -18,31 +18,96 @@
|
|
|
18
18
|
//
|
|
19
19
|
// Both writers touch ONLY a `pending` row, so a notice can never create, downgrade or
|
|
20
20
|
// remove a real membership (a member is left as-is; the row is not theirs to change).
|
|
21
|
-
//
|
|
22
|
-
//
|
|
21
|
+
//
|
|
22
|
+
// A login with no hub account is PARKED by login (task 1004646, migration 031): a
|
|
23
|
+
// platform account is only born when someone signs in to a project THROUGH the hub,
|
|
24
|
+
// so the usual invitee — new to the platform — has none, and dropping the notice
|
|
25
|
+
// meant they never saw it, even after signing in to the hub. The invitations read
|
|
26
|
+
// lists a parked notice for whoever signs in with that login (listParkedInvites).
|
|
23
27
|
//
|
|
24
28
|
// Its own file because platform-identity.js is at the size ratchet; it reuses that
|
|
25
29
|
// file's account lookup and pending-row writers rather than restating them.
|
|
26
30
|
'use strict';
|
|
27
31
|
|
|
32
|
+
const api = require('../../src/module-api');
|
|
28
33
|
const pi = require('./platform-identity');
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
const loginKey = (githubLogin) => String(githubLogin || '').trim().replace(/^@/, '').toLowerCase();
|
|
36
|
+
|
|
37
|
+
// The project noticed an invite for `githubLogin` → a `pending` hub row if the login
|
|
38
|
+
// has a hub account, else a parked notice under the login. Returns { noted } for the
|
|
32
39
|
// route's own logging; the route never tells the caller which case happened (see it).
|
|
33
|
-
async function noteProjectInvite({ clientId, githubLogin }, { pool } = {}) {
|
|
40
|
+
async function noteProjectInvite({ clientId, githubLogin }, { pool = api.pool } = {}) {
|
|
34
41
|
const account = await pi.getAccountByLogin(githubLogin, { pool });
|
|
35
|
-
if (!account)
|
|
42
|
+
if (!account) {
|
|
43
|
+
const login = loginKey(githubLogin);
|
|
44
|
+
if (!login) return { noted: false };
|
|
45
|
+
await pool.query(
|
|
46
|
+
`INSERT INTO platform_identity_parked_invite_notices (client_id, login_lower)
|
|
47
|
+
VALUES ($1, $2)
|
|
48
|
+
ON CONFLICT (client_id, login_lower) DO UPDATE SET noted_at = now()`,
|
|
49
|
+
[clientId, login],
|
|
50
|
+
);
|
|
51
|
+
return { noted: true, parked: true };
|
|
52
|
+
}
|
|
36
53
|
const out = await pi.inviteToProject({ clientId, githubId: account.github_id }, { pool });
|
|
37
54
|
return { noted: !!out.invited };
|
|
38
55
|
}
|
|
39
56
|
|
|
40
|
-
// The project rescinded the invite for `githubLogin` → drop the `pending` hub row
|
|
57
|
+
// The project rescinded the invite for `githubLogin` → drop the `pending` hub row and
|
|
58
|
+
// any parked notice under the login (an account made since the notice keeps both
|
|
59
|
+
// reachable, so both go).
|
|
41
60
|
async function clearProjectInvite({ clientId, githubLogin }, { pool } = {}) {
|
|
61
|
+
const parked = await removeParkedInvite({ clientId, githubLogin }, { pool });
|
|
42
62
|
const account = await pi.getAccountByLogin(githubLogin, { pool });
|
|
43
|
-
if (!account) return { cleared:
|
|
63
|
+
if (!account) return { cleared: parked.removed };
|
|
44
64
|
const out = await pi.removePendingInvite({ clientId, githubId: account.github_id }, { pool });
|
|
45
|
-
return { cleared: out.removed };
|
|
65
|
+
return { cleared: out.removed || parked.removed };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The parked notices for one signed-in login, in the shape listPendingInvitesForAccount
|
|
69
|
+
// returns, minus any past the invite's 30 days (ADR 0356) and any project the reader
|
|
70
|
+
// already holds a real membership in — signing in there accepted it.
|
|
71
|
+
async function listParkedInvites({ githubLogin, githubId }, { pool = api.pool } = {}) {
|
|
72
|
+
const login = loginKey(githubLogin);
|
|
73
|
+
if (!login) return [];
|
|
74
|
+
const { rows } = await pool.query(
|
|
75
|
+
`SELECT n.client_id, n.noted_at AS invited_at,
|
|
76
|
+
c.name AS project_name, c.origin AS project_origin
|
|
77
|
+
FROM platform_identity_parked_invite_notices n
|
|
78
|
+
LEFT JOIN platform_identity_sso_clients c ON c.client_id = n.client_id
|
|
79
|
+
WHERE n.login_lower = $1
|
|
80
|
+
AND n.noted_at > now() - make_interval(days => $2)
|
|
81
|
+
AND NOT EXISTS (
|
|
82
|
+
SELECT 1 FROM platform_identity_project_memberships m
|
|
83
|
+
WHERE m.client_id = n.client_id AND m.github_id = $3 AND m.membership_kind <> 'pending')
|
|
84
|
+
ORDER BY n.noted_at DESC`,
|
|
85
|
+
[login, api.inviteExpiry.INVITE_EXPIRY_DAYS, String(githubId || '')],
|
|
86
|
+
);
|
|
87
|
+
return rows;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The invitee declined, or the project rescinded: drop the parked notice for the login.
|
|
91
|
+
async function removeParkedInvite({ clientId, githubLogin }, { pool = api.pool } = {}) {
|
|
92
|
+
const login = loginKey(githubLogin);
|
|
93
|
+
if (!login) return { removed: false };
|
|
94
|
+
const { rowCount } = await pool.query(
|
|
95
|
+
`DELETE FROM platform_identity_parked_invite_notices WHERE client_id = $1 AND login_lower = $2`,
|
|
96
|
+
[clientId, login],
|
|
97
|
+
);
|
|
98
|
+
return { removed: rowCount > 0 };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Everything waiting on one signed-in builder: their pending rows plus the notices
|
|
102
|
+
// parked under their login, one per project (a pending row wins — it is the same
|
|
103
|
+
// invite, written once the account existed).
|
|
104
|
+
async function listInvitesForBuilder({ githubId, githubLogin }, { pool = api.pool } = {}) {
|
|
105
|
+
const [pending, parked] = await Promise.all([
|
|
106
|
+
pi.listPendingInvitesForAccount(githubId, { pool }),
|
|
107
|
+
listParkedInvites({ githubLogin, githubId }, { pool }),
|
|
108
|
+
]);
|
|
109
|
+
const seen = new Set(pending.map((r) => r.client_id));
|
|
110
|
+
return pending.concat(parked.filter((r) => !seen.has(r.client_id)));
|
|
46
111
|
}
|
|
47
112
|
|
|
48
113
|
// The project's own invite box for `githubLogin`: its hall's watch page, with the login
|
|
@@ -58,4 +123,7 @@ function hallInviteUrl(origin, githubLogin) {
|
|
|
58
123
|
return `${u.origin}/builders/watch${login ? `?invite=${encodeURIComponent(login)}` : ''}`;
|
|
59
124
|
}
|
|
60
125
|
|
|
61
|
-
module.exports = {
|
|
126
|
+
module.exports = {
|
|
127
|
+
noteProjectInvite, clearProjectInvite, hallInviteUrl,
|
|
128
|
+
removeParkedInvite, listInvitesForBuilder,
|
|
129
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
-- platform_identity_031_parked_invite_notices.sql — a project's invite notice for a
|
|
2
|
+
-- GitHub login the hub has no account for yet, kept until that person signs in
|
|
3
|
+
-- (task 1004646; ADR 0353 amended).
|
|
4
|
+
--
|
|
5
|
+
-- WHY. POST /sso/invites/notify resolves the invited login to a platform account and,
|
|
6
|
+
-- before this table, dropped the notice when there was none. A platform account is
|
|
7
|
+
-- only created when someone signs in to a PROJECT through the hub — signing in to the
|
|
8
|
+
-- hub itself creates none — so the usual invitee (new to the platform) never saw the
|
|
9
|
+
-- invite on /invitations, even after signing in. Now the notice is PARKED here by
|
|
10
|
+
-- login, and the invitations read lists it for whoever signs in with that login.
|
|
11
|
+
--
|
|
12
|
+
-- THE SHAPE CARRIES THE RULES:
|
|
13
|
+
-- * THE KEY IS (client_id, login_lower), so a re-invite cannot pile up rows: the
|
|
14
|
+
-- writer's upsert restarts noted_at in place.
|
|
15
|
+
-- * NO STATUS COLUMN (ADR 0201 D1.3, ADR 0353). A notice admits nobody: the
|
|
16
|
+
-- project's own access_requests row does, and signing in there accepts it. A
|
|
17
|
+
-- parked notice is hidden at read time once the reader holds a non-pending
|
|
18
|
+
-- membership in that project.
|
|
19
|
+
-- * THE 30-DAY WINDOW IS READ-TIME (ADR 0356, the doorway's INVITE_EXPIRY_DAYS),
|
|
20
|
+
-- exactly as for a pending membership row. Nothing sweeps the rows.
|
|
21
|
+
-- * KEYED BY LOGIN, like the project's own invited access_requests row, which is
|
|
22
|
+
-- what admits the same person — so the notice can claim no more than the invite.
|
|
23
|
+
--
|
|
24
|
+
-- NOTHING HERE IS AUTHORITY (ADR 0016 / ADR 0141).
|
|
25
|
+
--
|
|
26
|
+
-- Additive + platform_identity_-namespaced (ADR 0083 §5). CREATE only, so the
|
|
27
|
+
-- previous release still runs against this schema. Idempotent.
|
|
28
|
+
|
|
29
|
+
BEGIN;
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS platform_identity_parked_invite_notices (
|
|
32
|
+
client_id text NOT NULL,
|
|
33
|
+
login_lower text NOT NULL,
|
|
34
|
+
noted_at timestamptz NOT NULL DEFAULT now(),
|
|
35
|
+
PRIMARY KEY (client_id, login_lower)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- The reader's side: every notice for one login.
|
|
39
|
+
CREATE INDEX IF NOT EXISTS platform_identity_parked_invite_notices_login
|
|
40
|
+
ON platform_identity_parked_invite_notices (login_lower);
|
|
41
|
+
|
|
42
|
+
COMMENT ON TABLE platform_identity_parked_invite_notices IS
|
|
43
|
+
'A project''s invite notice for a GitHub login with no hub account yet (task 1004646). Listed on /invitations for whoever signs in with that login; a notice only, never a way in (ADR 0353). The 30-day window is applied when read (ADR 0356).';
|
|
44
|
+
|
|
45
|
+
COMMIT;
|
|
@@ -29,6 +29,7 @@ const projectStats = require('../project-stats');
|
|
|
29
29
|
const applicationEcho = require('../application-echo');
|
|
30
30
|
const guilds = require('../guilds');
|
|
31
31
|
const guildEngagements = require('../guild-engagements');
|
|
32
|
+
const inviteNotices = require('../invite-notices');
|
|
32
33
|
const hubKeys = require('../hub-keys');
|
|
33
34
|
const { isAccountActive } = require('../account-visibility');
|
|
34
35
|
const { projectPublishability } = require('../publish-gate');
|
|
@@ -302,10 +303,12 @@ module.exports = function myProjectsRoutes() {
|
|
|
302
303
|
}
|
|
303
304
|
});
|
|
304
305
|
|
|
305
|
-
// rank: any-builder — the caller's OWN pending invitations (ADR 0141 §5)
|
|
306
|
+
// rank: any-builder — the caller's OWN pending invitations (ADR 0141 §5), plus the
|
|
307
|
+
// notices parked under their login before they had a hub account (task 1004646).
|
|
306
308
|
router.get('/my-projects/invites', requireBuilder, async (req, res) => {
|
|
307
309
|
try {
|
|
308
|
-
const invites = await
|
|
310
|
+
const invites = await inviteNotices.listInvitesForBuilder(
|
|
311
|
+
{ githubId: req.builder.github_id, githubLogin: req.builder.github_login }, { pool });
|
|
309
312
|
res.json({ invites: invites.map((i) => ({ client_id: i.client_id, name: i.project_name || i.project_origin || i.client_id, origin: i.project_origin || null, invited_at: i.invited_at })) });
|
|
310
313
|
} catch (err) {
|
|
311
314
|
console.error('[platform-identity] GET /my-projects/invites', err);
|
|
@@ -324,7 +327,8 @@ module.exports = function myProjectsRoutes() {
|
|
|
324
327
|
if (!clientId) return res.fail('bad_client_id', 400);
|
|
325
328
|
try {
|
|
326
329
|
const out = await pi.removePendingInvite({ clientId, githubId: req.builder.github_id }, { pool });
|
|
327
|
-
|
|
330
|
+
const parked = await inviteNotices.removeParkedInvite({ clientId, githubLogin: req.builder.github_login }, { pool });
|
|
331
|
+
if (!out.removed && !parked.removed) return res.fail('no_pending_invite', 404);
|
|
328
332
|
res.json({ ok: true, accepted: false });
|
|
329
333
|
} catch (err) {
|
|
330
334
|
console.error('[platform-identity] decline invite', err);
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
// that came and went would leave a stale line behind.
|
|
22
22
|
|
|
23
23
|
const BIRTH_KEY = 'founding_birth';
|
|
24
|
+
// The wizard's ship-mode answer (task 1004629): auto | manual, stamped beside the birth on
|
|
25
|
+
// the row the create route just made. Reserved like the birth (out of SETTINGS_VOCAB): the
|
|
26
|
+
// instance's own project_settings['ship.mode'] is where the setting LIVES and where the
|
|
27
|
+
// owner changes it later; this is only what the project was born with.
|
|
28
|
+
const SHIP_MODE_KEY = 'ship_mode';
|
|
24
29
|
const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
|
|
25
30
|
|
|
26
31
|
const storedOf = (row) => (row && row.settings && typeof row.settings === 'object' && !Array.isArray(row.settings) ? row.settings : {});
|
|
@@ -43,11 +48,18 @@ function demoBox(row) {
|
|
|
43
48
|
return { people, hours_each: hours };
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
// The closed ship-mode list is the core's own (src/bongos/project-settings.js), never retyped here.
|
|
52
|
+
const parseShipMode = (raw) => require('../../src/module-api').projectSettings.parseShipMode(raw);
|
|
53
|
+
|
|
54
|
+
// bornShipMode(row) — PURE. The ship mode the wizard asked for, or '' for none (or junk).
|
|
55
|
+
const bornShipMode = (row) => parseShipMode(storedOf(row)[SHIP_MODE_KEY]).mode || '';
|
|
56
|
+
|
|
46
57
|
// The companion env (SETTINGS_COMPANION_ENV). src/branding.js maps them onto founding.*.
|
|
47
58
|
const BIRTH_COMPANION_ENV = Object.freeze({
|
|
48
59
|
FOUNDING_BIRTH: bornAt,
|
|
49
60
|
DEMO_PEOPLE: (row) => { const d = demoBox(row); return d ? String(d.people) : ''; },
|
|
50
61
|
DEMO_HOURS: (row) => { const d = demoBox(row); return d ? String(d.hours_each) : ''; },
|
|
62
|
+
SHIP_MODE: bornShipMode,
|
|
51
63
|
});
|
|
52
64
|
|
|
53
65
|
// stampBirth(db, instanceId, now) — the create route's one write, on the row it just
|
|
@@ -61,4 +73,19 @@ async function stampBirth(db, instanceId, now = new Date()) {
|
|
|
61
73
|
);
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
|
|
76
|
+
// stampShipMode(db, instanceId, raw) — the wizard's ship-mode answer, on the row the create
|
|
77
|
+
// route just made. An absent or unusable answer writes nothing, so the instance reads "never
|
|
78
|
+
// chosen" and asks at its first ship (task 1004630). Write-once, like the birth.
|
|
79
|
+
async function stampShipMode(db, instanceId, raw) {
|
|
80
|
+
const { mode } = parseShipMode(raw);
|
|
81
|
+
if (!mode) return false;
|
|
82
|
+
await db.query(
|
|
83
|
+
`UPDATE provisioning_instances
|
|
84
|
+
SET settings = COALESCE(settings, '{}'::jsonb) || jsonb_build_object($2::text, $3::text)
|
|
85
|
+
WHERE id = $1 AND NOT (COALESCE(settings, '{}'::jsonb) ? $2::text)`,
|
|
86
|
+
[instanceId, SHIP_MODE_KEY, mode],
|
|
87
|
+
);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { BIRTH_KEY, BIRTH_COMPANION_ENV, SHIP_MODE_KEY, stampBirth, stampShipMode };
|
|
@@ -359,7 +359,7 @@ module.exports = function provisioningRoutes() {
|
|
|
359
359
|
// exactly the set the panel preselected. An explicit [] is the different
|
|
360
360
|
// answer "none of them". maxItems is a sanity ceiling well above a catalog
|
|
361
361
|
// that is three modules wide today.
|
|
362
|
-
modules: { type: 'array', itemsType: 'string', maxItems: 50 }, terms_acknowledged: { type: 'boolean' }, demo: { type: 'object' }, // the wizard's terms tick (task 1004417, ../terms-ack.js); the demo route's { people, hours_each } (task 1004420, ../demo.js)
|
|
362
|
+
modules: { type: 'array', itemsType: 'string', maxItems: 50 }, terms_acknowledged: { type: 'boolean' }, demo: { type: 'object' }, ship_mode: { type: 'string', enum: ['auto', 'manual'] }, // the wizard's terms tick (task 1004417, ../terms-ack.js); the demo route's { people, hours_each } (task 1004420, ../demo.js)
|
|
363
363
|
})) return;
|
|
364
364
|
const body = req.body || {};
|
|
365
365
|
const detailFault = badProjectDetail(body.detail);
|
|
@@ -475,6 +475,8 @@ module.exports = function provisioningRoutes() {
|
|
|
475
475
|
// provision intent exists, so the runner's web.env already carries the marker and the demo's box.
|
|
476
476
|
// Best-effort: a project that misses its marker opens as an ordinary hall; the create still stands.
|
|
477
477
|
if (created) await birth.stampBirth(pool, instance.id).catch((err) => log.error('[provisioning] birth marker not stamped', err));
|
|
478
|
+
// The wizard's ship mode (task 1004629) rides beside it; a miss means the project asks at its first ship.
|
|
479
|
+
if (created) await birth.stampShipMode(pool, instance.id, body.ship_mode).catch((err) => log.error('[provisioning] ship mode not stamped', err));
|
|
478
480
|
const demoRow = created && demoAsk.demo ? await demo.startDemoProject(pool, provisioning, instance.id, demoAsk.demo) : null;
|
|
479
481
|
// Enqueue the provision intent (idempotent: an already-open intent is returned)
|
|
480
482
|
// — but ONLY when the instance actually needs standing up. Blindly enqueueing on
|
|
@@ -624,6 +624,7 @@
|
|
|
624
624
|
});
|
|
625
625
|
})();
|
|
626
626
|
</script>
|
|
627
|
+
<script src="/assets/invite-badge.js"></script>
|
|
627
628
|
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
628
629
|
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
629
630
|
</body>
|
|
@@ -628,6 +628,14 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
628
628
|
.signinDoor:hover{background:var(--ink);color:var(--bg);}
|
|
629
629
|
/* a long GitHub login truncates instead of pushing Sign out off the screen */
|
|
630
630
|
.who>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:14ch;}
|
|
631
|
+
/* what is waiting on you (task 1004646, assets/invite-badge.js): a count on the chip and on
|
|
632
|
+
the band's Invitations item — the accent fill with --accent-on ink, the .btn pairing */
|
|
633
|
+
.invBadge{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;
|
|
634
|
+
border-radius:var(--radius-pill);background:var(--accent);color:var(--accent-on);
|
|
635
|
+
font-family:var(--font-body);font-size:12px;font-weight:700;line-height:1;letter-spacing:0;vertical-align:middle;}
|
|
636
|
+
.who>.invBadge{max-width:none;overflow:visible;}
|
|
637
|
+
.bandT .invBadge{margin-left:10px;}
|
|
638
|
+
.invWaiting{margin:6px 0 4px;font-size:15.5px;font-weight:600;}
|
|
631
639
|
/* keyboard focus on the two search pills is VISIBLE — the input's own outline
|
|
632
640
|
is off by design, so the pill carries the ring */
|
|
633
641
|
.search:focus-within{box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 40%,transparent);}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* assets/invite-badge.js — WHAT IS WAITING ON YOU, said where you already look
|
|
2
|
+
(task 1004646; the owner's report of 2026-10-08 that invitations "aren't obvious").
|
|
3
|
+
|
|
4
|
+
/invitations lists what asks you in, but the only way there was the profile band,
|
|
5
|
+
with nothing saying anything was waiting. This counts what is waiting — a project's
|
|
6
|
+
invite (GET /my-projects/invites) and a guild's invite or a join request to a guild
|
|
7
|
+
you own (GET /me/guild-requests) — and, when it is more than none, paints:
|
|
8
|
+
- a count on the identity chip (#who) and on the profile band's Invitations item;
|
|
9
|
+
- on /projects?view=mine, a line "You have N invitations waiting →" under the
|
|
10
|
+
view's heading.
|
|
11
|
+
A guild's engagement with a project is left out: it asks nothing of you.
|
|
12
|
+
|
|
13
|
+
Hub only, with no flag of its own: the Invitations item sits inside the apex-only
|
|
14
|
+
region, which a child instance strips, so there this file finds no item and reads
|
|
15
|
+
nothing. Signed out, both reads 401 and nothing is painted. A failed read counts
|
|
16
|
+
as none — a badge is a convenience, never the record.
|
|
17
|
+
|
|
18
|
+
Built from nodes only. window.InviteBadge.refresh() re-reads, for a page
|
|
19
|
+
that has just answered one (the invitations page's Decline and Accept). */
|
|
20
|
+
(function () {
|
|
21
|
+
'use strict';
|
|
22
|
+
var API = '/api/bongos';
|
|
23
|
+
function $(id) { return document.getElementById(id); }
|
|
24
|
+
function item() { var m = $('menuProfile'); return m && m.querySelector('a[href="/invitations"]'); }
|
|
25
|
+
|
|
26
|
+
function readCount(path, pick) {
|
|
27
|
+
return fetch(API + path, { credentials: 'same-origin', headers: { Accept: 'application/json' } })
|
|
28
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
29
|
+
.then(function (d) { return d ? pick(d) : 0; })
|
|
30
|
+
.catch(function () { return 0; });
|
|
31
|
+
}
|
|
32
|
+
function len(x) { return Array.isArray(x) ? x.length : 0; }
|
|
33
|
+
|
|
34
|
+
function badge(host, n, label) {
|
|
35
|
+
if (!host) return;
|
|
36
|
+
var b = host.querySelector('.invBadge');
|
|
37
|
+
if (!n) { if (b) b.remove(); return; }
|
|
38
|
+
if (!b) {
|
|
39
|
+
b = document.createElement('b');
|
|
40
|
+
b.className = 'invBadge';
|
|
41
|
+
var shown = document.createElement('span');
|
|
42
|
+
shown.setAttribute('aria-hidden', 'true');
|
|
43
|
+
var said = document.createElement('span');
|
|
44
|
+
said.className = 'vh';
|
|
45
|
+
b.appendChild(shown);
|
|
46
|
+
b.appendChild(said);
|
|
47
|
+
// before the chip's chevron; at the end of a band item
|
|
48
|
+
host.insertBefore(b, host.querySelector('svg.cv'));
|
|
49
|
+
}
|
|
50
|
+
b.firstChild.textContent = n > 99 ? '99+' : String(n);
|
|
51
|
+
b.lastChild.textContent = ', ' + label;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// at the top of the mine view, under its heading and lede — the list of projects
|
|
55
|
+
// below it can be long, and the Invitations pointer sits at its foot
|
|
56
|
+
function mineLine(n, label) {
|
|
57
|
+
var view = $('view-mine');
|
|
58
|
+
var hero = view && view.querySelector('.hero');
|
|
59
|
+
if (!hero) return;
|
|
60
|
+
var p = $('mineInvitesWaiting');
|
|
61
|
+
if (!n) { if (p) p.remove(); return; }
|
|
62
|
+
if (!p) {
|
|
63
|
+
p = document.createElement('p');
|
|
64
|
+
p.id = 'mineInvitesWaiting';
|
|
65
|
+
p.className = 'invWaiting';
|
|
66
|
+
var a = document.createElement('a');
|
|
67
|
+
a.href = '/invitations';
|
|
68
|
+
p.appendChild(a);
|
|
69
|
+
hero.appendChild(p);
|
|
70
|
+
}
|
|
71
|
+
p.firstChild.textContent = 'You have ' + label + ' waiting →';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function paint(n) {
|
|
75
|
+
var label = n + (n === 1 ? ' invitation' : ' invitations');
|
|
76
|
+
badge($('who'), n, label + ' waiting');
|
|
77
|
+
badge(item(), n, label + ' waiting');
|
|
78
|
+
mineLine(n, label);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function refresh() {
|
|
82
|
+
if (!item()) return Promise.resolve(0);
|
|
83
|
+
return Promise.all([
|
|
84
|
+
readCount('/my-projects/invites', function (d) { return len(d.invites); }),
|
|
85
|
+
readCount('/me/guild-requests', function (d) { return len(d.incoming) + len(d.owner_incoming); }),
|
|
86
|
+
]).then(function (c) { var n = c[0] + c[1]; paint(n); return n; });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
window.InviteBadge = { refresh: refresh, paint: paint };
|
|
90
|
+
refresh();
|
|
91
|
+
})();
|
|
@@ -120,6 +120,7 @@
|
|
|
120
120
|
|
|
121
121
|
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms">Terms</a><a href="/contact" aria-current="page">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
|
|
122
122
|
<script src="/assets/hub-bar.js"></script>
|
|
123
|
+
<script src="/assets/invite-badge.js"></script>
|
|
123
124
|
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
124
125
|
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
125
126
|
</body>
|
|
@@ -629,6 +629,7 @@
|
|
|
629
629
|
load();
|
|
630
630
|
})();
|
|
631
631
|
</script>
|
|
632
|
+
<script src="/assets/invite-badge.js"></script>
|
|
632
633
|
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
633
634
|
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
634
635
|
</body>
|
|
@@ -232,11 +232,13 @@
|
|
|
232
232
|
button on failure. `onRemoved` carries whatever else the caller needs after
|
|
233
233
|
the row is gone (the empty-line flip). The busy/idle label swap the /projects
|
|
234
234
|
copy carries is for its Leave button, which never moved here. */
|
|
235
|
+
/* the bar's waiting count (assets/invite-badge.js, task 1004646) follows an answer here */
|
|
236
|
+
function recount() { if (typeof window !== 'undefined' && window.InviteBadge) window.InviteBadge.refresh(); }
|
|
235
237
|
function postThenRemoveRow(url, row, btn, onRemoved) {
|
|
236
238
|
btn.disabled = true;
|
|
237
239
|
fetch(url, { method: 'POST', credentials: 'same-origin' })
|
|
238
240
|
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
239
|
-
.then(function () { row.remove(); if (onRemoved) onRemoved(); })
|
|
241
|
+
.then(function () { row.remove(); if (onRemoved) onRemoved(); recount(); })
|
|
240
242
|
.catch(function () { btn.disabled = false; });
|
|
241
243
|
}
|
|
242
244
|
|
|
@@ -481,7 +483,7 @@
|
|
|
481
483
|
msg.className = 'projMsg';
|
|
482
484
|
msg.textContent = '';
|
|
483
485
|
guildPost('/guild-requests/' + encodeURIComponent(id) + '/decline').then(function (res) {
|
|
484
|
-
if (res.ok) { row.remove(); paintInvitesSection(); return; }
|
|
486
|
+
if (res.ok) { row.remove(); paintInvitesSection(); recount(); return; }
|
|
485
487
|
btns.forEach(function (b) { b.disabled = false; });
|
|
486
488
|
if (res.status === 401) { sessionExpired(msg); return; }
|
|
487
489
|
msg.className = 'projMsg err';
|
|
@@ -582,6 +584,7 @@
|
|
|
582
584
|
radios.forEach(function (r) { r.disabled = true; });
|
|
583
585
|
page.hidden = false;
|
|
584
586
|
msg.className = 'projMsg ok';
|
|
587
|
+
recount();
|
|
585
588
|
msg.textContent = 'You’ve joined ' + name + (on ? ', and your numbers count toward its totals.' : ', and your numbers are left out of its totals.');
|
|
586
589
|
return;
|
|
587
590
|
}
|
|
@@ -660,6 +663,7 @@
|
|
|
660
663
|
hint.hidden = true;
|
|
661
664
|
msg.className = 'projMsg ok';
|
|
662
665
|
msg.textContent = who + ' is in ' + (r.guild.name || r.guild.slug) + ' now.';
|
|
666
|
+
recount();
|
|
663
667
|
return;
|
|
664
668
|
}
|
|
665
669
|
accept.textContent = 'Accept';
|
|
@@ -961,6 +965,7 @@
|
|
|
961
965
|
});
|
|
962
966
|
})();
|
|
963
967
|
</script>
|
|
968
|
+
<script src="/assets/invite-badge.js"></script>
|
|
964
969
|
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
965
970
|
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
966
971
|
</body>
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
|
|
130
130
|
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy" aria-current="page">Privacy</a><a href="/terms">Terms</a><a href="/contact">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
|
|
131
131
|
<script src="/assets/hub-bar.js"></script>
|
|
132
|
+
<script src="/assets/invite-badge.js"></script>
|
|
132
133
|
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
133
134
|
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
134
135
|
</body>
|
|
@@ -1950,6 +1950,31 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1950
1950
|
</div>
|
|
1951
1951
|
<span class="acts"><button type="button" class="rowChg" data-row-chg aria-expanded="false">Edit</button></span>
|
|
1952
1952
|
</div>
|
|
1953
|
+
|
|
1954
|
+
<!-- how finished work ships (task 1004629, ADR 0369): Auto unless the owner
|
|
1955
|
+
says otherwise. It rides the create as ship_mode and becomes the project's
|
|
1956
|
+
own ship mode, which its settings page can change any time. -->
|
|
1957
|
+
<div class="setRow" data-row="ship">
|
|
1958
|
+
<span class="k">Shipping</span>
|
|
1959
|
+
<div class="v"><span class="rowV" data-row-v></span><small class="rowS" data-row-s></small>
|
|
1960
|
+
<div class="opts" hidden>
|
|
1961
|
+
<fieldset class="appOpts" id="shipOpts">
|
|
1962
|
+
<legend>When the AI finishes a task</legend>
|
|
1963
|
+
<label class="appOpt">
|
|
1964
|
+
<input type="radio" name="shipMode" value="auto" checked>
|
|
1965
|
+
<span><b>Auto (recommended)</b>
|
|
1966
|
+
<span>The AI ships finished work on its own. You don’t run any commands.</span></span>
|
|
1967
|
+
</label>
|
|
1968
|
+
<label class="appOpt">
|
|
1969
|
+
<input type="radio" name="shipMode" value="manual">
|
|
1970
|
+
<span><b>Manual</b>
|
|
1971
|
+
<span>The AI stops before each ship and waits for you to approve it.</span></span>
|
|
1972
|
+
</label>
|
|
1973
|
+
</fieldset>
|
|
1974
|
+
</div>
|
|
1975
|
+
</div>
|
|
1976
|
+
<span class="acts"><button type="button" class="rowChg" data-row-chg aria-expanded="false">Edit</button></span>
|
|
1977
|
+
</div>
|
|
1953
1978
|
</div>
|
|
1954
1979
|
<div class="hint" id="h-review"> </div>
|
|
1955
1980
|
<div class="nav">
|
|
@@ -4476,7 +4501,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4476
4501
|
request omits the field), [] = the owner switched them all off, [...] =
|
|
4477
4502
|
what they picked. Never seed it with the preselect — that would store an
|
|
4478
4503
|
answer nobody gave. */
|
|
4479
|
-
var state = { step: 0, name: '', projType: null, description: '', teamShape: null, modules: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false, appPlatform: 'later', physics: null, demo: null, look: null, termsAck: false, confirmed: {} };
|
|
4504
|
+
var state = { step: 0, name: '', projType: null, description: '', teamShape: null, modules: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false, appPlatform: 'later', shipMode: 'auto', physics: null, demo: null, look: null, termsAck: false, confirmed: {} };
|
|
4480
4505
|
try { Object.assign(state, JSON.parse(sessionStorage.getItem(DRAFT_KEY) || '{}')); } catch (e) {}
|
|
4481
4506
|
var authKnown = false; /* flips true when the boot's /me settles, either way */
|
|
4482
4507
|
|
|
@@ -5956,7 +5981,6 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5956
5981
|
if (appOptsEl) appOptsEl.addEventListener('change', function (ev) {
|
|
5957
5982
|
if (ev.target && ev.target.name === 'appPlatform' && !ev.target.disabled) { state.appPlatform = ev.target.value; saveDraft(); syncRenderSetup(); paintSetup(); }
|
|
5958
5983
|
});
|
|
5959
|
-
|
|
5960
5984
|
/* ── Render (task 1004352, ADR 0348) ──
|
|
5961
5985
|
The flow itself is renderConnect(), shared with an existing project's page (task
|
|
5962
5986
|
1004184). Here it reads and writes the draft (`state`), so every answer but the key
|
|
@@ -6073,7 +6097,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6073
6097
|
return 'Workspace ' + ws + ' — a new app (' + runtimeLabel(state.renderRuntime || 'node') + ', ' + plan + ')' + db + '. Render bills you directly.';
|
|
6074
6098
|
}
|
|
6075
6099
|
|
|
6076
|
-
|
|
6100
|
+
/* the ship row (task 1004629): anything but 'manual' — an older draft has none — is Auto */
|
|
6101
|
+
function shipModeChoice() { return state.shipMode === 'manual' ? 'manual' : 'auto'; }
|
|
6102
|
+
function syncShipStep() {
|
|
6103
|
+
var pick = shipModeChoice();
|
|
6104
|
+
root.querySelectorAll('#shipOpts input[name=shipMode]').forEach(function (i) { i.checked = i.value === pick; });
|
|
6105
|
+
}
|
|
6106
|
+
|
|
6107
|
+
var ROWS = ['code', 'address', 'app', 'extras', 'team', 'rewards', 'run', 'ship'];
|
|
6077
6108
|
function rowEl(key) { return root.querySelector('#reviewRows .setRow[data-row="' + key + '"]'); }
|
|
6078
6109
|
function confirmedRows() {
|
|
6079
6110
|
if (!state.confirmed || typeof state.confirmed !== 'object') state.confirmed = {};
|
|
@@ -6118,6 +6149,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6118
6149
|
if (key === 'extras') renderModules();
|
|
6119
6150
|
if (key === 'code') syncHome();
|
|
6120
6151
|
if (key === 'app') { syncAppStep(); syncRenderSetup(); }
|
|
6152
|
+
if (key === 'ship') syncShipStep();
|
|
6121
6153
|
}
|
|
6122
6154
|
function closeRow(key) {
|
|
6123
6155
|
var row = rowEl(key);
|
|
@@ -6182,6 +6214,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6182
6214
|
controls' own handlers run first (they are on the targets), then the row repaints */
|
|
6183
6215
|
['input', 'change'].forEach(function (type) {
|
|
6184
6216
|
$('reviewRows').addEventListener(type, function (ev) {
|
|
6217
|
+
/* the ship row's radios have no handler of their own (task 1004629) */
|
|
6218
|
+
if (type === 'change' && ev.target.name === 'shipMode') { state.shipMode = ev.target.value; saveDraft(); }
|
|
6185
6219
|
if (ev.target.closest && ev.target.closest('.setRow.open .opts')) {
|
|
6186
6220
|
var row = ev.target.closest('.setRow'), t = rowText(row.getAttribute('data-row'));
|
|
6187
6221
|
row.querySelector('[data-row-v]').textContent = t.v;
|
|
@@ -6247,6 +6281,11 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6247
6281
|
if (a.framing === 'open-source') return { v: 'Free core only', s: 'The free core, on your own machines.' };
|
|
6248
6282
|
return { v: 'Everything, for you', s: 'Hosting, payouts and the starter kit, managed.' };
|
|
6249
6283
|
}
|
|
6284
|
+
if (key === 'ship') {
|
|
6285
|
+
return shipModeChoice() === 'manual'
|
|
6286
|
+
? { v: 'Manual', s: 'The AI waits for you to approve each ship. You can change this later in your project’s settings.' }
|
|
6287
|
+
: { v: 'Auto (recommended)', s: 'The AI ships finished work on its own. You can change this later in your project’s settings.' };
|
|
6288
|
+
}
|
|
6250
6289
|
return { v: '', s: '' };
|
|
6251
6290
|
}
|
|
6252
6291
|
/* paint every row from the draft, and the locked Continue under them */
|
|
@@ -6407,7 +6446,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6407
6446
|
existing history instead of pushing a fresh tree, which a repo that already
|
|
6408
6447
|
has commits rejects as unrelated history. state.mode already tracked
|
|
6409
6448
|
greenfield|adopt for the UI — it has to reach the server too. */
|
|
6410
|
-
onboard_mode: state.mode === 'adopt' ? 'adopt' : 'greenfield'
|
|
6449
|
+
onboard_mode: state.mode === 'adopt' ? 'adopt' : 'greenfield',
|
|
6450
|
+
/* the ship row (task 1004629): becomes the new project's own ship mode */
|
|
6451
|
+
ship_mode: shipModeChoice()
|
|
6411
6452
|
})
|
|
6412
6453
|
});
|
|
6413
6454
|
}).then(function (res) {
|
|
@@ -7175,7 +7216,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
7175
7216
|
is three-valued and null is its "nothing said" sentinel; leaving the key out
|
|
7176
7217
|
left it UNDEFINED, which is not null — so pickedModules() reached .slice()
|
|
7177
7218
|
on it and the second project's Modules step threw. */
|
|
7178
|
-
state = { step: 1, name: '', projType: null, description: '', teamShape: null, modules: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false, appPlatform: 'later', physics: null, demo: null, look: null, termsAck: false, confirmed: {} };
|
|
7219
|
+
state = { step: 1, name: '', projType: null, description: '', teamShape: null, modules: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false, appPlatform: 'later', shipMode: 'auto', physics: null, demo: null, look: null, termsAck: false, confirmed: {} };
|
|
7179
7220
|
$('newName').value = ''; $('f-owner').value = ''; $('f-repo').value = ''; $('f-slug').value = ''; $('f-domain').value = '';
|
|
7180
7221
|
$('f-noaddr').checked = false; $('f-desc').value = '';
|
|
7181
7222
|
$('termsAck').checked = false; paintDoors(); demoShow('intro'); checksShow('main');
|
|
@@ -11142,6 +11183,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
11142
11183
|
.then(function (data) {
|
|
11143
11184
|
me = data.builder || data.me || data;
|
|
11144
11185
|
renderWho();
|
|
11186
|
+
/* renderWho rebuilds the chip, so the waiting count is painted again on it (task 1004646) */
|
|
11187
|
+
if (window.InviteBadge) window.InviteBadge.refresh();
|
|
11145
11188
|
paintShellIdentity();
|
|
11146
11189
|
wizard.onAuth();
|
|
11147
11190
|
$('gate').hidden = true;
|
|
@@ -11174,6 +11217,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
11174
11217
|
.then(loadSelfShipped, loadSelfShipped);
|
|
11175
11218
|
})();
|
|
11176
11219
|
</script>
|
|
11220
|
+
<script src="/assets/invite-badge.js"></script>
|
|
11177
11221
|
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
11178
11222
|
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
11179
11223
|
</body>
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"in-new-review-extras": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizStart"],["wait",400],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#panel-what button[type=submit]"],["wait",400],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["click","#physNext"],["wait",700],["click","#reviewRows .setRow[data-row=\"extras\"] [data-row-chg]"],["wait",900]],"expect":{"visible":["#modQ","#modCards .tpill","#modCoreNote"]}},
|
|
56
56
|
"in-new-review-team": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizStart"],["wait",400],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#panel-what button[type=submit]"],["wait",400],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["click","#physNext"],["wait",700],["click","#reviewRows .setRow[data-row=\"team\"] [data-row-chg]"],["wait",300],["click","[data-phys-solo=\"many\"]"],["wait",600]],"expect":{"visible":["#physTeam","#physGuessLine","#teamCards","#reviewRows .setRow[data-row=\"rewards\"]"]}},
|
|
57
57
|
"in-new-review-run": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizStart"],["wait",400],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#panel-what button[type=submit]"],["wait",400],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["click","#physNext"],["wait",700],["click","#reviewRows .setRow[data-row=\"run\"] [data-row-chg]"],["wait",300],["click","[data-phys=\"framing\"][data-val=\"hybrid\"]"],["wait",600]],"expect":{"visible":["#physSuite"]}},
|
|
58
|
+
"in-new-review-ship": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizStart"],["wait",400],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#panel-what button[type=submit]"],["wait",400],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["click","#physNext"],["wait",700],["click","#reviewRows .setRow[data-row=\"ship\"] [data-row-chg]"],["wait",300],["click","#shipOpts input[value=\"manual\"]"],["wait",300]],"expect":{"visible":["#shipOpts"]}},
|
|
58
59
|
"in-new-review-confirmed": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizStart"],["wait",400],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#panel-what button[type=submit]"],["wait",400],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["click","#physNext"],["wait",700],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200]],"expect":{"visible":["#reviewGo","#reviewRows .setRow.done"],"text":[["#reviewGo","Looks right, continue"]]}},
|
|
59
60
|
"in-new-look": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizStart"],["wait",400],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#panel-what button[type=submit]"],["wait",400],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["click","#physNext"],["wait",700],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["click","#reviewGo"],["wait",500]],"expect":{"visible":["#panel-look","#wizLook","#wizRecap","#doCreate"],"hidden":["#panel-review"],"attr":[["#wizRoom","data-screen","look"]]}},
|
|
60
61
|
"in-new-done": {"auth":true,"url":"/projects?view=new","actions":[["wait",600],["click","#termsAck"],["wait",100],["click","#wizStart"],["wait",400],["fill","#newName","Mercury"],["click","#typeCards .tpill[data-type=\"game\"]"],["fill","#f-desc","A co-op farming game where the seasons never repeat."],["click","#panel-what button[type=submit]"],["wait",400],["select","#physCountry","US"],["wait",100],["click","[data-phys=\"builder_geography\"][data-val=\"global\"]"],["click","[data-phys=\"minors_on_board\"][data-val=\"false\"]"],["click","[data-phys=\"entity\"][data-val=\"intent-solo\"]"],["click","#physNext"],["wait",700],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["fill","#f-slug","mercury"],["wait",900],["fill","#f-domain","mercury.example.com"],["wait",200],["click","#reviewRows .setRow[data-row=\"address\"] [data-row-chg]"],["wait",200],["click","#reviewGo"],["wait",500],["click","#doCreate"],["wait",1500]],"expect":{"visible":["#panel-done","#hop","#hopCanvas","#hopStatus","#hopJump"],"hidden":["#panel-look","#qWrap","#wizBack","#wizInvite","#liveProgress","#humanSteps","#wizInviteLater","#stepPager"]}},
|
|
@@ -134,6 +134,7 @@
|
|
|
134
134
|
|
|
135
135
|
<footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms" aria-current="page">Terms</a><a href="/contact">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
|
|
136
136
|
<script src="/assets/hub-bar.js"></script>
|
|
137
|
+
<script src="/assets/invite-badge.js"></script>
|
|
137
138
|
<!-- the Artist review button: signed-in builders only (task 1004499) -->
|
|
138
139
|
<script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
|
|
139
140
|
</body>
|