@bongos/core 1.19.1081 → 1.20.2
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 +528 -193
- package/.claude/skills/planning-session/SKILL.md +6 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +32 -0
- package/clients/bongos-client/index.cjs +32 -0
- package/clients/bongos-client/index.d.ts +51 -6
- package/clients/bongos-client/index.mjs +32 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
- package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
- package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +851 -19
- package/docs/api-reference.md +33 -9
- package/docs/architecture.md +6 -2
- package/docs/copy-inventory.md +620 -553
- package/docs/copy-registry.json +1467 -820
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +8 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +1666 -1558
- package/migrations/core_260_goals_working_area.sql +63 -0
- package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
- package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
- package/modules/agents/lib/validate.js +43 -0
- package/modules/autonomy/gauge.js +38 -2
- package/modules/grading/grader-subagent.js +37 -2
- package/modules/grading/grader-workers/reader.js +65 -5
- package/modules/hall-ui/public/approval-queue.css +33 -7
- package/modules/hall-ui/public/approval-queue.js +9 -2
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/brand-holes.js +121 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +6 -1
- package/modules/hall-ui/public/deploy.js +22 -8
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/dom-utils.js +20 -0
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/idea.js +28 -0
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/ideas.js +19 -3
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile-nudge.js +21 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +19 -30
- package/modules/hall-ui/public/settings.js +24 -177
- package/modules/hall-ui/public/settings.states.json +1 -1
- package/modules/hall-ui/public/shell.js +4 -2
- package/modules/hall-ui/public/studio.css +14 -8
- package/modules/hall-ui/public/studio.html +5 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
- package/modules/hall-ui/public/tweak-editor.css +142 -11
- package/modules/hall-ui/public/tweak-editor.html +25 -9
- package/modules/hall-ui/public/tweak-editor.js +243 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/approval-queue.md +6 -0
- package/modules/hall-ui/records/tweak-editor.md +6 -0
- package/modules/ideas/ratify.js +36 -8
- package/modules/ideas/routes/ratify-goal.js +6 -0
- package/modules/lifecycle/db-analytics.js +79 -10
- package/modules/lifecycle/db-claims.js +48 -1
- package/modules/lifecycle/db-deps-criteria.js +94 -40
- package/modules/lifecycle/db-goals.js +4 -1
- package/modules/lifecycle/db-grade.js +9 -1
- package/modules/lifecycle/db-relevance-flags.js +183 -0
- package/modules/lifecycle/db-versions.js +4 -3
- package/modules/lifecycle/db.js +19 -0
- package/modules/lifecycle/done-when.js +5 -3
- package/modules/lifecycle/est-advisory.js +31 -4
- package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
- package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
- package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/claims.js +8 -1
- package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
- package/modules/lifecycle/routes/goals.js +1 -1
- package/modules/lifecycle/routes/tasks.js +27 -24
- package/modules/lifecycle/routes/visuals.js +9 -0
- package/modules/lifecycle/ship-preflight.js +7 -11
- package/modules/lifecycle/task-visuals.js +109 -5
- package/modules/npm-release/module.json +2 -1
- package/modules/npm-release/preview/commands.js +81 -0
- package/modules/npm-release/preview/divert.js +95 -0
- package/modules/npm-release/preview/env.js +78 -0
- package/modules/npm-release/preview/proxy.js +107 -0
- package/modules/npm-release/preview/runtime.js +83 -0
- package/modules/npm-release/preview/supervisor.js +175 -0
- package/modules/npm-release/public/work.js +65 -1
- package/modules/npm-release/routes/preview.js +133 -0
- package/modules/provisioning/app-status.js +83 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/pollers/app-liveness.js +110 -0
- package/modules/provisioning/render-lookup.js +68 -0
- package/modules/provisioning/render-standup.js +44 -1
- package/modules/provisioning/routes/render-standup.js +81 -6
- package/modules/provisioning/starter-bundles.js +4 -1
- package/modules/public-landing/public/account-private.states.json +14 -0
- package/modules/public-landing/public/account.html +513 -0
- package/modules/public-landing/public/account.probes.json +37 -0
- package/modules/public-landing/public/account.states.json +16 -0
- package/modules/public-landing/public/index.html +5 -2
- package/modules/public-landing/public/projects.html +661 -18
- package/modules/public-landing/public/projects.states.json +8 -1
- package/modules/render-deploy/deploys.js +203 -0
- package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
- package/modules/render-deploy/module.json +25 -0
- package/modules/render-deploy/public/deploy.css +10 -0
- package/modules/render-deploy/public/deploy.js +249 -0
- package/modules/render-deploy/render.js +56 -0
- package/modules/render-deploy/routes/act.js +88 -0
- package/modules/render-deploy/routes/app.js +65 -0
- package/modules/render-deploy/routes/history.js +38 -0
- package/modules/specialities/db.js +27 -8
- package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
- package/modules/specialities/routes/specialities.js +51 -3
- package/modules/specialities/skills.js +94 -0
- package/modules/specialities/specialities.js +56 -9
- package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
- package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
- package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
- package/modules/ui-design/kit/lib.js +3 -1
- package/modules/ui-design/kit/serve.js +96 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +169 -0
- package/scripts/gds/agents-sync.js +13 -3
- package/scripts/gds/autobongos-run.js +80 -2
- package/scripts/gds/autobongos-service.cmd +12 -0
- package/scripts/gds/copy-apply.js +14 -0
- package/scripts/gds/dev-box-guard.js +3 -2
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/grade-correlation-audit.js +42 -12
- package/scripts/gds/grade-replay.js +1 -1
- package/scripts/gds/provision-core-upgrade.js +38 -7
- package/scripts/gds/provision-pin-key.js +176 -0
- package/scripts/gds/provision-render.js +62 -31
- package/scripts/gds/provision-teardown.js +7 -2
- package/scripts/gds/provision-units.js +31 -0
- package/scripts/gds/provision.js +10 -10
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/render-api.js +19 -0
- package/scripts/gds/render-payload.js +39 -65
- package/scripts/gds/run-unit-tests.js +10 -0
- package/scripts/gds/ship-finish.js +19 -20
- package/scripts/gds/smoke-dependencies.sh +37 -8
- package/scripts/gds/status.js +12 -4
- package/scripts/gds/upgrade.js +2 -2
- package/scripts/public-mirror-export.js +7 -1
- package/src/bongos/module-scope-map.js +13 -0
- package/src/bongos/serve-internal.js +3 -1
- package/src/module-api.js +10 -1
- package/src/platform-server.js +39 -0
- package/tests/account_privacy_flags.mjs +10 -6
- package/tests/agents_authoring.mjs +15 -1
- package/tests/agents_sync.mjs +106 -5
- package/tests/agents_validate.mjs +45 -0
- package/tests/api_path_404.mjs +6 -0
- package/tests/autobongos_cadence.mjs +7 -0
- package/tests/autobongos_loop.mjs +136 -1
- package/tests/autonomy_gauge.mjs +62 -0
- package/tests/blocker_hall_live_proof.mjs +275 -0
- package/tests/claim_gate_ci_unblock.mjs +116 -0
- package/tests/claim_gate_rebase.mjs +15 -3
- package/tests/collab_page.mjs +10 -0
- package/tests/conductor_main_e2e.mjs +97 -0
- package/tests/core_262_drop_box_blocked_db.mjs +143 -0
- package/tests/core_upgrade_runner.mjs +1 -0
- package/tests/dependency_writes_atomic.mjs +191 -0
- package/tests/deploy_page_projects.mjs +2 -1
- package/tests/effective_visibility_predicate.mjs +15 -0
- package/tests/est_advisory.mjs +27 -0
- package/tests/goal_map_page.mjs +26 -0
- package/tests/goal_task_relevance.mjs +518 -0
- package/tests/goal_working_area.mjs +146 -0
- package/tests/grade_attempts.mjs +61 -0
- package/tests/grade_attribution.mjs +23 -0
- package/tests/grade_correlation_audit.mjs +54 -1
- package/tests/grader_reader_lens.mjs +55 -2
- package/tests/grader_root_outage.mjs +6 -1
- package/tests/grader_subagent_tools_arg.mjs +102 -0
- package/tests/hall_approval_queue.mjs +54 -2
- package/tests/hall_settings_world.mjs +3 -1
- package/tests/hall_tweak_editor.mjs +316 -11
- package/tests/hub_account_page.mjs +394 -0
- package/tests/idea_detail_page.mjs +64 -0
- package/tests/idea_goal_ratification.mjs +64 -0
- package/tests/idea_spark_hall.mjs +60 -9
- package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
- package/tests/landing_page.mjs +6 -3
- package/tests/module-scope-map.mjs +31 -8
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/no_phantom_mirror_workflow.mjs +24 -0
- package/tests/npm_release_preview_commands.mjs +89 -0
- package/tests/npm_release_preview_divert.mjs +164 -0
- package/tests/npm_release_preview_env.mjs +100 -0
- package/tests/npm_release_preview_proxy.mjs +162 -0
- package/tests/npm_release_preview_routes.mjs +208 -0
- package/tests/npm_release_preview_supervisor.mjs +203 -0
- package/tests/pin_write_key.mjs +383 -0
- package/tests/planning_session_skill.mjs +27 -0
- package/tests/platform_boot.mjs +71 -0
- package/tests/profile_nudge_links.mjs +86 -0
- package/tests/profile_ui_cross_project.mjs +1 -1
- package/tests/projects_hub.mjs +2 -2
- package/tests/projects_hub_app_status.mjs +249 -0
- package/tests/projects_hub_app_step.mjs +282 -52
- package/tests/projects_hub_render_connect.mjs +295 -0
- package/tests/provision_render.mjs +121 -28
- package/tests/provision_settings_apply.mjs +62 -0
- package/tests/provisioning_app_status.mjs +204 -0
- package/tests/provisioning_render_route.mjs +126 -6
- package/tests/render_check.mjs +2 -1
- package/tests/render_deploy.mjs +495 -0
- package/tests/rot_inbox.mjs +8 -0
- package/tests/ship_preflight.mjs +61 -8
- package/tests/smoke_dependencies_witness.mjs +46 -0
- package/tests/speciality_skills.mjs +214 -0
- package/tests/studio_room_height.mjs +70 -0
- package/tests/task_detail_includes.mjs +10 -0
- package/tests/task_visuals_instance_root.mjs +150 -0
- package/tests/tweak_batch_apply.mjs +21 -0
- package/tests/ui_design_kit.mjs +36 -4
- package/tests/upgrade.mjs +1 -1
- package/tests/upgrade_persist_pin.mjs +24 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-mode="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<!-- THE ACCOUNT PAGE (task 1004259, ADR 0337 D4.3; spec
|
|
7
|
+
docs/specs/hub-account-settings-inventory.md). The hub account is the
|
|
8
|
+
person, a hall account is their seat in one project — so what belongs to
|
|
9
|
+
the person (handle, name, bio, links, who can find them) is edited HERE,
|
|
10
|
+
once, and every project's hall links out to it instead of keeping a copy.
|
|
11
|
+
Hub-only: platform-server.js serves /account only where platform-identity
|
|
12
|
+
is on, because every read and write below is one of that module's own-scoped
|
|
13
|
+
routes (GET/PATCH /me/cross-project, PATCH /me/handle, PATCH /me/profile).
|
|
14
|
+
Server-stamped like the footer doors: {{…}} tokens filled by landingPage(). -->
|
|
15
|
+
<title>{{productName}} — Your account</title>
|
|
16
|
+
<meta name="description" content="Your {{productName}} account: your handle, your name, your bio and links, and who can find you.">
|
|
17
|
+
<meta name="robots" content="noindex">
|
|
18
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
19
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
20
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400&family=Manrope:wght@300;400;500;600&display=swap">
|
|
21
|
+
<link rel="stylesheet" href="/assets/cosmos.css">
|
|
22
|
+
<!-- Dark only, like every apex page (ADR 0204): data-mode="dark" is fixed on <html>. -->
|
|
23
|
+
<style>
|
|
24
|
+
/* The account page's own layout, in the hub's manage-view voice (a lowercase
|
|
25
|
+
light heading, hairline sections with the title left and the controls right,
|
|
26
|
+
hairline fields, the choice cells with the dot). No :root block — the pack's
|
|
27
|
+
injected theme owns the tokens; everything below reads them. */
|
|
28
|
+
.acct{position:relative;z-index:1;max-width:1060px;margin:0 auto;padding:clamp(6px,1.5vw,18px) clamp(16px,4vw,40px) 56px;}
|
|
29
|
+
.acct h1{font-family:var(--font-display);font-weight:300;font-size:clamp(28px,3.4vw,44px);letter-spacing:-.035em;line-height:1.04;text-transform:lowercase;color:var(--ink);margin:18px 0 0;}
|
|
30
|
+
.acct .lede{margin:8px 0 18px;max-width:62ch;font-size:15.5px;line-height:1.55;color:var(--ink-soft);}
|
|
31
|
+
.acct .loading{font-size:15px;color:var(--ink-soft);}
|
|
32
|
+
.aWho{display:flex;flex-wrap:wrap;align-items:center;gap:4px 14px;margin:0 0 26px;font-size:14px;color:var(--ink-soft);}
|
|
33
|
+
.aWho b{font-weight:500;color:var(--ink);overflow-wrap:anywhere;}
|
|
34
|
+
.aWho a{display:inline-flex;align-items:center;min-height:24px;color:var(--ink);text-decoration:underline;}
|
|
35
|
+
.aDoor{border-top:1px solid var(--rule);padding:30px 0 10px;max-width:62ch;}
|
|
36
|
+
.aDoor p{margin:0 0 18px;font-size:15.5px;line-height:1.55;color:var(--ink-soft);}
|
|
37
|
+
.aDoor .gh{width:18px;height:18px;fill:currentColor;}
|
|
38
|
+
.aDoor .btn{text-transform:none;}
|
|
39
|
+
.aSec{display:grid;grid-template-columns:minmax(200px,300px) 1fr;grid-template-rows:auto 1fr;gap:6px 40px;border-top:1px solid var(--rule);padding:30px 0 34px;}
|
|
40
|
+
.aSec>h2{grid-column:1;font-family:var(--font-display);font-weight:300;font-size:clamp(22px,2.4vw,28px);letter-spacing:-.03em;line-height:1.08;text-transform:lowercase;color:var(--ink);margin:0;}
|
|
41
|
+
.aSec>.aSide{grid-column:1;align-self:start;margin:4px 0 0;max-width:34ch;font-size:13.5px;line-height:1.5;color:var(--ink-faint);}
|
|
42
|
+
.aSec>.aBody{grid-column:2;grid-row:1/span 2;display:grid;gap:18px;align-content:start;min-width:0;}
|
|
43
|
+
.aField{display:grid;gap:4px;min-width:0;}
|
|
44
|
+
.aLabel{font-family:var(--label-face);font-size:var(--label-size);letter-spacing:var(--label-space);text-transform:uppercase;color:var(--ink-faint);}
|
|
45
|
+
.aField input,.aField textarea{width:100%;box-sizing:border-box;min-height:44px;font-family:var(--font-body);font-size:15.5px;font-weight:400;line-height:1.5;color:var(--ink);background:none;border:0;border-bottom:1px solid var(--ctl-line);border-radius:0;padding:8px 0;}
|
|
46
|
+
.aField textarea{min-height:92px;resize:vertical;}
|
|
47
|
+
.aField input::placeholder,.aField textarea::placeholder{color:var(--ink-faint);}
|
|
48
|
+
.aField input:focus,.aField textarea:focus{outline:none;border-bottom-color:var(--ink);box-shadow:0 1px 0 var(--ink);}
|
|
49
|
+
.aHint{margin:0;font-size:12.5px;line-height:1.45;color:var(--ink-faint);}
|
|
50
|
+
.aLinks{display:grid;grid-template-columns:1fr 1fr;gap:18px 24px;}
|
|
51
|
+
.aBand{display:grid;gap:8px;}
|
|
52
|
+
.aBand [role="radiogroup"]{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);}
|
|
53
|
+
.aOpt{position:relative;display:block;margin:0;padding:16px 18px 18px;border-left:1px solid var(--rule-soft);cursor:pointer;}
|
|
54
|
+
.aOpt:first-child{border-left:0;padding-left:0;}
|
|
55
|
+
.aOpt input{position:absolute;width:1px;height:1px;margin:0;overflow:hidden;clip-path:inset(50%);}
|
|
56
|
+
.aOpt>span{display:block;font-size:13px;line-height:1.45;color:var(--ink-faint);}
|
|
57
|
+
.aOpt b{display:flex;align-items:center;gap:10px;margin-bottom:6px;font-family:var(--font-body);font-weight:500;font-size:15px;color:var(--ink);}
|
|
58
|
+
.aOpt b::before{content:"";width:7px;height:7px;border-radius:50%;border:1px solid var(--rule);background:var(--bg);flex:none;}
|
|
59
|
+
.aOpt:has(input:checked) b::before{background:var(--accent);border-color:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
|
|
60
|
+
.aOpt:focus-within{outline:2px solid var(--accent);outline-offset:3px;}
|
|
61
|
+
.aWhat{margin:0;font-size:13.5px;line-height:1.5;color:var(--ink-soft);max-width:70ch;}
|
|
62
|
+
.aTag{font-size:12.5px;color:var(--ink-faint);}
|
|
63
|
+
.acct .actions{margin:0;gap:14px;}
|
|
64
|
+
.acct .btn{min-height:44px;padding:0 22px;font-family:var(--font-body);font-weight:500;font-size:15px;text-transform:lowercase;white-space:nowrap;box-shadow:none;}
|
|
65
|
+
.acct .btn:hover,.acct .btn:active{transform:none;box-shadow:none;}
|
|
66
|
+
.acct .projMsg{margin:0;min-height:0;font-weight:400;}
|
|
67
|
+
.acct .finePrint{margin:0;font-weight:400;font-size:13.5px;}
|
|
68
|
+
@media (max-width:900px){
|
|
69
|
+
.aSec{grid-template-columns:1fr;gap:12px;}
|
|
70
|
+
.aSec>h2,.aSec>.aSide,.aSec>.aBody{grid-column:1;grid-row:auto;}
|
|
71
|
+
.aSec>.aSide{max-width:none;}
|
|
72
|
+
}
|
|
73
|
+
@media (max-width:420px){
|
|
74
|
+
.top{padding:16px;}
|
|
75
|
+
.top .mark{font-size:19px;}
|
|
76
|
+
.top-right{gap:8px;}
|
|
77
|
+
.top .btn-space{padding:7px 12px;}
|
|
78
|
+
}
|
|
79
|
+
@media (max-width:560px){
|
|
80
|
+
.aLinks{grid-template-columns:1fr;}
|
|
81
|
+
.aBand [role="radiogroup"]{grid-template-columns:1fr;}
|
|
82
|
+
.aOpt{padding:14px 0 16px;border-left:0;border-top:1px solid var(--rule-soft);}
|
|
83
|
+
.aOpt:first-child{border-top:0;}
|
|
84
|
+
}
|
|
85
|
+
</style>
|
|
86
|
+
</head>
|
|
87
|
+
<body>
|
|
88
|
+
<header class="top">
|
|
89
|
+
<a class="mark" href="/">{{productName}}</a>
|
|
90
|
+
<div class="top-right">
|
|
91
|
+
<a class="btn-space" href="/projects">Projects</a>
|
|
92
|
+
<a class="btn-space" data-link-hall href="{{buildersOrigin}}">Hall</a>
|
|
93
|
+
</div>
|
|
94
|
+
</header>
|
|
95
|
+
|
|
96
|
+
<main class="acct" id="acct">
|
|
97
|
+
<h1>your account</h1>
|
|
98
|
+
<p class="lede">Who you are across every project on {{productName}}, and who can find you. It is one account, so you change these once, here — each project’s own settings stay in that project’s hall.</p>
|
|
99
|
+
|
|
100
|
+
<!-- signed-out door: revealed on a 401, the /projects gate's shape -->
|
|
101
|
+
<section class="aDoor" id="door" hidden aria-labelledby="door-h">
|
|
102
|
+
<h2 class="vh" id="door-h">Sign in</h2>
|
|
103
|
+
<p>Sign in with GitHub to change your handle, your bio and links, and who can find you.</p>
|
|
104
|
+
<a class="btn" id="signin" href="/api/bongos/auth/web/start?return=/account">
|
|
105
|
+
<!-- keep the spaces inside this path's numbers: same shape, and they are load-bearing (task 1004401) -->
|
|
106
|
+
<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23 .82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2 .82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12 .51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29 .25.54.73 .54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15 .46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
107
|
+
Sign in with GitHub
|
|
108
|
+
</a>
|
|
109
|
+
</section>
|
|
110
|
+
|
|
111
|
+
<!-- who is signed in, and the way out: in the page rather than the bar, so
|
|
112
|
+
the bar keeps the footer doors' two pills and fits a phone on one line -->
|
|
113
|
+
<p class="aWho" id="aWho" hidden><span>Signed in as <b id="whoName"></b></span><a href="/api/bongos/auth/logout" id="signOut">sign out</a></p>
|
|
114
|
+
|
|
115
|
+
<p class="loading" id="loading">Reading your account…</p>
|
|
116
|
+
<div class="alert" id="pageAlert" role="alert"></div>
|
|
117
|
+
|
|
118
|
+
<!-- signed in: ships hidden, revealed once GET /me/cross-project answers -->
|
|
119
|
+
<div id="acctBody" hidden>
|
|
120
|
+
|
|
121
|
+
<!-- YOUR PROFILE — PATCH /me/profile (P02, task 1002798): the account's own
|
|
122
|
+
display name, the bio, the typed links. Only the fields that changed are
|
|
123
|
+
sent; each is nullable server-side, and an empty link slot drops it. -->
|
|
124
|
+
<section class="aSec" id="aProfile" aria-labelledby="aProfile-h">
|
|
125
|
+
<h2 id="aProfile-h">your profile</h2>
|
|
126
|
+
<p class="aSide">Shown wherever your profile appears on {{productName}}. Each project’s hall keeps its own display name — change that one in the hall’s settings.</p>
|
|
127
|
+
<form class="aBody" id="profileForm" novalidate>
|
|
128
|
+
<label class="aField"><span class="aLabel">Display name</span>
|
|
129
|
+
<input type="text" id="fName" maxlength="60" autocomplete="name" spellcheck="false" placeholder="Your name"></label>
|
|
130
|
+
<label class="aField"><span class="aLabel">Bio</span>
|
|
131
|
+
<textarea id="fBio" rows="3" placeholder="What you build, and what you would like to build next."></textarea>
|
|
132
|
+
<span class="aHint" id="bioCount" aria-live="polite">0 of 280 characters</span></label>
|
|
133
|
+
<div class="aLinks">
|
|
134
|
+
<label class="aField"><span class="aLabel">Website</span>
|
|
135
|
+
<input type="url" id="fLinkWebsite" data-link="website" inputmode="url" spellcheck="false" placeholder="https://"></label>
|
|
136
|
+
<label class="aField"><span class="aLabel">X</span>
|
|
137
|
+
<input type="url" id="fLinkX" data-link="x" inputmode="url" spellcheck="false" placeholder="https://x.com/…"></label>
|
|
138
|
+
<label class="aField"><span class="aLabel">LinkedIn</span>
|
|
139
|
+
<input type="url" id="fLinkLinkedin" data-link="linkedin" inputmode="url" spellcheck="false" placeholder="https://linkedin.com/in/…"></label>
|
|
140
|
+
<label class="aField"><span class="aLabel">Discord</span>
|
|
141
|
+
<input type="url" id="fLinkDiscord" data-link="discord" inputmode="url" spellcheck="false" placeholder="https://discord.gg/…"></label>
|
|
142
|
+
</div>
|
|
143
|
+
<p class="finePrint">Your bio is plain text, shown exactly as you write it. Links start with https://, and an X, LinkedIn or Discord link has to point at that site.</p>
|
|
144
|
+
<div class="actions"><button type="submit" class="btn" id="profileSave">save profile</button></div>
|
|
145
|
+
<p class="projMsg" id="profileMsg" role="status" aria-live="polite"></p>
|
|
146
|
+
</form>
|
|
147
|
+
</section>
|
|
148
|
+
|
|
149
|
+
<!-- YOUR HANDLE — PATCH /me/handle (P01, task 1002797): its own save, because
|
|
150
|
+
a rename is rate-limited (once per 7 days) and moves the old handle into
|
|
151
|
+
the single previous-handle slot. -->
|
|
152
|
+
<section class="aSec" id="aHandle" aria-labelledby="aHandle-h">
|
|
153
|
+
<h2 id="aHandle-h">your handle</h2>
|
|
154
|
+
<p class="aSide">The short name people find you by, the same in every project.</p>
|
|
155
|
+
<form class="aBody" id="handleForm" novalidate>
|
|
156
|
+
<label class="aField"><span class="aLabel">Handle</span>
|
|
157
|
+
<input type="text" id="fHandle" maxlength="39" autocomplete="off" autocapitalize="none" spellcheck="false" placeholder="your-handle"></label>
|
|
158
|
+
<p class="finePrint">Lowercase letters, numbers and hyphens, up to 39. You can change it once a week, and your old handle keeps leading to you until you change it again.</p>
|
|
159
|
+
<div class="actions"><button type="submit" class="btn" id="handleSave">change handle</button></div>
|
|
160
|
+
<p class="projMsg" id="handleMsg" role="status" aria-live="polite"></p>
|
|
161
|
+
</form>
|
|
162
|
+
</section>
|
|
163
|
+
|
|
164
|
+
<!-- WHO CAN FIND YOU — account_visibility + recruiter_discoverable (privacy
|
|
165
|
+
spec D1/D3, task 1002966), PATCH /me/cross-project. Ported from the hall's
|
|
166
|
+
old "Profile privacy" section. The recruiter copy must name EXACTLY what
|
|
167
|
+
being discoverable grants (the D3 sliver) — tests/account_privacy_flags.mjs
|
|
168
|
+
checks it against the module's own key lists. -->
|
|
169
|
+
<section class="aSec" id="aPrivacy" aria-labelledby="aPrivacy-h">
|
|
170
|
+
<h2 id="aPrivacy-h">who can find you</h2>
|
|
171
|
+
<p class="aSide">Your profile page can be public or private, and recruiters are let in separately — so a private profile can still be reachable for work, and a public one can stay out of recruiting entirely.</p>
|
|
172
|
+
<form class="aBody" id="privacyForm" novalidate>
|
|
173
|
+
<div class="aBand">
|
|
174
|
+
<span class="aLabel" id="visLabel">Your profile page</span>
|
|
175
|
+
<div role="radiogroup" aria-labelledby="visLabel">
|
|
176
|
+
<label class="aOpt"><input type="radio" name="visibility" value="public"><span><b>Public</b>Anyone can open your profile page and find you by name.</span></label>
|
|
177
|
+
<label class="aOpt"><input type="radio" name="visibility" value="private"><span><b>Private</b>Private hides the page from everyone — including you: it answers “not found” to every visitor, and you read your own record here instead.</span></label>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="aBand">
|
|
181
|
+
<span class="aLabel" id="recLabel">Recruiters</span>
|
|
182
|
+
<div role="radiogroup" aria-labelledby="recLabel">
|
|
183
|
+
<label class="aOpt"><input type="radio" name="recruiters" value="on"><span><b>Can find me</b>Recruiters looking for builders can find you.</span></label>
|
|
184
|
+
<label class="aOpt"><input type="radio" name="recruiters" value="off"><span><b>Can’t find me</b>You stay out of the recruiting directory and a recruiter’s look-up.</span></label>
|
|
185
|
+
</div>
|
|
186
|
+
<p class="aWhat" id="recWhat"></p>
|
|
187
|
+
</div>
|
|
188
|
+
<p class="finePrint">Your profile page setting applies now — private already answers “not found” everywhere your profile is read. Recruiter discoverability applies now too — switching it off removes you from both recruiting surfaces immediately, and back on puts you there again.</p>
|
|
189
|
+
<div class="actions"><button type="submit" class="btn" id="privacySave">save</button></div>
|
|
190
|
+
<p class="projMsg" id="privacyMsg" role="status" aria-live="polite"></p>
|
|
191
|
+
</form>
|
|
192
|
+
</section>
|
|
193
|
+
|
|
194
|
+
<!-- YOUR RECORD — hide_stats (ADR 0171 D4, task 1002279), PATCH
|
|
195
|
+
/me/cross-project. Ported from the hall's old "Cross-project record"
|
|
196
|
+
section. Enforcement is in the module's read functions, so it applies at
|
|
197
|
+
once. The owner always sees their own numbers here. -->
|
|
198
|
+
<section class="aSec" id="aRecord" aria-labelledby="aRecord-h">
|
|
199
|
+
<h2 id="aRecord-h">your record</h2>
|
|
200
|
+
<p class="aSide">Your track record across every project you build on. Hiding it takes effect immediately everywhere — your profile, the recruiting directory, and a recruiter’s look-up of one builder.</p>
|
|
201
|
+
<form class="aBody" id="recordForm" novalidate>
|
|
202
|
+
<p class="aWhat" id="recordLine"></p>
|
|
203
|
+
<div class="aBand">
|
|
204
|
+
<span class="aLabel" id="hideLabel">Show my cross-project record</span>
|
|
205
|
+
<div role="radiogroup" aria-labelledby="hideLabel">
|
|
206
|
+
<label class="aOpt"><input type="radio" name="record" value="show"><span><b>Show it</b>Your numbers appear on your profile and in the directories.</span></label>
|
|
207
|
+
<label class="aOpt"><input type="radio" name="record" value="hide"><span><b>Hide it</b>Your numbers leave your profile and the directories. You always see them here.</span></label>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="actions"><button type="submit" class="btn" id="recordSave">save</button></div>
|
|
211
|
+
<p class="projMsg" id="recordMsg" role="status" aria-live="polite"></p>
|
|
212
|
+
</form>
|
|
213
|
+
</section>
|
|
214
|
+
</div>
|
|
215
|
+
</main>
|
|
216
|
+
|
|
217
|
+
<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">Contact</a></nav></footer>
|
|
218
|
+
|
|
219
|
+
<script>
|
|
220
|
+
(function () {
|
|
221
|
+
'use strict';
|
|
222
|
+
var API = '/api/bongos';
|
|
223
|
+
function $(id) { return document.getElementById(id); }
|
|
224
|
+
|
|
225
|
+
// The server's own view of the account — the last GET /me/cross-project
|
|
226
|
+
// answer. Every save re-renders from it, never from what was typed.
|
|
227
|
+
var acct = null;
|
|
228
|
+
|
|
229
|
+
// Same caps and link pins as modules/platform-identity/platform-identity.js
|
|
230
|
+
// (BIO_MAX_CHARS, PROFILE_LINK_TYPES, PROFILE_LINK_HOSTS) — boundary-forced
|
|
231
|
+
// duplication (ADR 0091 §2): a page cannot import a module. The server stays
|
|
232
|
+
// the authority; these only let a mistake be named at the field it is in.
|
|
233
|
+
// tests/hub_account_page.mjs holds the copies to the module's.
|
|
234
|
+
var BIO_MAX = 280;
|
|
235
|
+
var LINK_TYPES = ['website', 'x', 'linkedin', 'discord'];
|
|
236
|
+
var LINK_HOSTS = { x: ['x.com', 'twitter.com'], linkedin: ['linkedin.com'], discord: ['discord.gg', 'discord.com'] };
|
|
237
|
+
var LINK_NAMES = { website: 'website', x: 'X', linkedin: 'LinkedIn', discord: 'Discord' };
|
|
238
|
+
|
|
239
|
+
function request(method, path, body) {
|
|
240
|
+
var init = { method: method, credentials: 'same-origin', headers: { Accept: 'application/json' } };
|
|
241
|
+
if (body !== undefined) { init.headers['Content-Type'] = 'application/json'; init.body = JSON.stringify(body); }
|
|
242
|
+
return fetch(API + path, init).then(function (res) {
|
|
243
|
+
return res.json().catch(function () { return null; }).then(function (data) {
|
|
244
|
+
return { status: res.status, ok: res.ok, data: data };
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
// A refusal in the server's own words, with its first letter raised.
|
|
249
|
+
function serverWords(res, fallback) {
|
|
250
|
+
var m = res && res.data && res.data.error && res.data.error.message;
|
|
251
|
+
if (typeof m !== 'string' || !m) return fallback;
|
|
252
|
+
return m.charAt(0).toUpperCase() + m.slice(1) + (/[.!?]$/.test(m) ? '' : '.');
|
|
253
|
+
}
|
|
254
|
+
function say(el, text, kind) {
|
|
255
|
+
el.className = 'projMsg' + (kind ? ' ' + kind : '');
|
|
256
|
+
el.textContent = text;
|
|
257
|
+
}
|
|
258
|
+
function sessionGone(el) {
|
|
259
|
+
say(el, 'Your session expired — sign in again to save.', 'err');
|
|
260
|
+
showDoor();
|
|
261
|
+
}
|
|
262
|
+
function codePoints(s) { return Array.from(s).length; }
|
|
263
|
+
|
|
264
|
+
// ---- the page's three states: reading, signed out, signed in ----
|
|
265
|
+
function showDoor() {
|
|
266
|
+
$('loading').hidden = true;
|
|
267
|
+
$('acctBody').hidden = true;
|
|
268
|
+
$('aWho').hidden = true;
|
|
269
|
+
$('door').hidden = false;
|
|
270
|
+
}
|
|
271
|
+
function showAlert(text) {
|
|
272
|
+
$('loading').hidden = true;
|
|
273
|
+
var a = $('pageAlert');
|
|
274
|
+
a.textContent = text;
|
|
275
|
+
a.className = 'alert on';
|
|
276
|
+
}
|
|
277
|
+
function load() {
|
|
278
|
+
return request('GET', '/me/cross-project').then(function (res) {
|
|
279
|
+
if (res.status === 401) { showDoor(); return; }
|
|
280
|
+
if (!res.ok || !res.data) { showAlert('Couldn’t read your account right now — refresh in a moment.'); return; }
|
|
281
|
+
acct = res.data;
|
|
282
|
+
paint();
|
|
283
|
+
$('loading').hidden = true;
|
|
284
|
+
$('door').hidden = true;
|
|
285
|
+
$('acctBody').hidden = false;
|
|
286
|
+
$('whoName').textContent = acct.handle || acct.display_name || 'your account';
|
|
287
|
+
$('aWho').hidden = false;
|
|
288
|
+
}, function () { showAlert('Couldn’t reach the server — check your connection and refresh.'); });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---- paint: every control from the server's answer ----
|
|
292
|
+
function paint() { paintProfile(); paintHandle(); paintPrivacy(); paintRecord(); }
|
|
293
|
+
|
|
294
|
+
function paintProfile() {
|
|
295
|
+
$('fName').value = acct.display_name || '';
|
|
296
|
+
$('fBio').value = acct.bio || '';
|
|
297
|
+
var links = acct.links || {};
|
|
298
|
+
document.querySelectorAll('#profileForm [data-link]').forEach(function (input) {
|
|
299
|
+
input.value = links[input.getAttribute('data-link')] || '';
|
|
300
|
+
});
|
|
301
|
+
paintBioCount();
|
|
302
|
+
}
|
|
303
|
+
function paintBioCount() {
|
|
304
|
+
var n = codePoints($('fBio').value.trim());
|
|
305
|
+
var el = $('bioCount');
|
|
306
|
+
el.textContent = n + ' of ' + BIO_MAX + ' characters';
|
|
307
|
+
el.style.color = n > BIO_MAX ? 'var(--err)' : '';
|
|
308
|
+
}
|
|
309
|
+
function paintHandle() { $('fHandle').value = acct.handle || ''; }
|
|
310
|
+
|
|
311
|
+
function pick(name, value) {
|
|
312
|
+
document.querySelectorAll('input[name="' + name + '"]').forEach(function (r) { r.checked = r.value === value; });
|
|
313
|
+
}
|
|
314
|
+
function picked(name) {
|
|
315
|
+
var r = document.querySelector('input[name="' + name + '"]:checked');
|
|
316
|
+
return r ? r.value : null;
|
|
317
|
+
}
|
|
318
|
+
function paintPrivacy() {
|
|
319
|
+
pick('visibility', acct.account_visibility === 'private' ? 'private' : 'public');
|
|
320
|
+
pick('recruiters', acct.recruiter_discoverable_effective ? 'on' : 'off');
|
|
321
|
+
paintRecruitWhat();
|
|
322
|
+
}
|
|
323
|
+
// What turning recruiters on grants, for the choices as they stand on the
|
|
324
|
+
// page. The grant shrinks to name and picture when the record is hidden,
|
|
325
|
+
// because the rest of the sliver is derived from it (privacy spec D3).
|
|
326
|
+
function paintRecruitWhat() {
|
|
327
|
+
var isPublic = picked('visibility') !== 'private';
|
|
328
|
+
var hidden = picked('record') === 'hide';
|
|
329
|
+
var grant = hidden
|
|
330
|
+
? 'Because your cross-project record is hidden, they will see your name and your picture, and nothing else.'
|
|
331
|
+
: 'They will see your name, your picture, your roles, and whether you have been active in the last 30 days — never your credits, works shipped, karma, or the projects you build on.';
|
|
332
|
+
// The surprising half, and only where it applies: a private profile answers
|
|
333
|
+
// "not found" to every visitor, and this one switch is the exception.
|
|
334
|
+
var despitePrivate = isPublic ? ''
|
|
335
|
+
: ' This is the one thing a recruiter can see while your profile page is private.';
|
|
336
|
+
var el = $('recWhat');
|
|
337
|
+
el.textContent = grant + despitePrivate +
|
|
338
|
+
' Turning this off removes you from both recruiting surfaces — the recruiting directory, and a recruiter’s look-up of one builder — straight away.';
|
|
339
|
+
if (acct && acct.recruiter_discoverable == null) {
|
|
340
|
+
var tag = document.createElement('span');
|
|
341
|
+
tag.className = 'aTag';
|
|
342
|
+
tag.textContent = ' · For now this follows your profile page setting.';
|
|
343
|
+
el.appendChild(tag);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function paintRecord() {
|
|
347
|
+
pick('record', acct.hide_stats ? 'hide' : 'show');
|
|
348
|
+
var cp = acct.cross_project;
|
|
349
|
+
var t = cp && cp.totals ? cp.totals : null;
|
|
350
|
+
var line = t
|
|
351
|
+
? 'Your record: ' + t.projects + ' project' + (t.projects === 1 ? '' : 's') + ' · ' +
|
|
352
|
+
t.credits + ' credits · ' + t.works_shipped + ' works shipped · ' + t.karma + ' karma.'
|
|
353
|
+
: 'No cross-project activity reported yet.';
|
|
354
|
+
if (acct.profile_state !== 'active') line += ' Your profile is not published yet; it publishes after your next sign-in.';
|
|
355
|
+
$('recordLine').textContent = line;
|
|
356
|
+
paintRecruitWhat();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ---- save: your profile ----
|
|
360
|
+
function linkProblem(type, raw) {
|
|
361
|
+
var name = LINK_NAMES[type];
|
|
362
|
+
var u;
|
|
363
|
+
try { u = new URL(raw); } catch (e) { return 'Your ' + name + ' link isn’t a web address — paste the whole thing, starting with https://.'; }
|
|
364
|
+
if (u.protocol !== 'https:') return 'Your ' + name + ' link has to start with https://.';
|
|
365
|
+
var hosts = LINK_HOSTS[type];
|
|
366
|
+
var host = u.hostname.toLowerCase();
|
|
367
|
+
if (hosts && !hosts.some(function (h) { return host === h || host.slice(-(h.length + 1)) === '.' + h; })) {
|
|
368
|
+
return 'Your ' + name + ' link has to point at ' + hosts.join(' or ') + '.';
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
function sameLinks(a, b) {
|
|
373
|
+
a = a || {}; b = b || {};
|
|
374
|
+
return LINK_TYPES.every(function (k) { return (a[k] || '') === (b[k] || ''); });
|
|
375
|
+
}
|
|
376
|
+
$('fBio').addEventListener('input', paintBioCount);
|
|
377
|
+
$('profileForm').addEventListener('submit', function (ev) {
|
|
378
|
+
ev.preventDefault();
|
|
379
|
+
var msg = $('profileMsg');
|
|
380
|
+
var body = {};
|
|
381
|
+
var name = $('fName').value.trim();
|
|
382
|
+
if (name !== (acct.display_name || '')) {
|
|
383
|
+
if (!name) { say(msg, 'Your display name can’t be empty.', 'err'); $('fName').focus(); return; }
|
|
384
|
+
body.display_name = name;
|
|
385
|
+
}
|
|
386
|
+
var bio = $('fBio').value.trim();
|
|
387
|
+
if (bio !== (acct.bio || '')) {
|
|
388
|
+
if (codePoints(bio) > BIO_MAX) { say(msg, 'Your bio is ' + codePoints(bio) + ' characters — the most is ' + BIO_MAX + '.', 'err'); $('fBio').focus(); return; }
|
|
389
|
+
body.bio = bio || null;
|
|
390
|
+
}
|
|
391
|
+
var links = {};
|
|
392
|
+
var inputs = document.querySelectorAll('#profileForm [data-link]');
|
|
393
|
+
for (var i = 0; i < inputs.length; i++) {
|
|
394
|
+
var type = inputs[i].getAttribute('data-link');
|
|
395
|
+
var raw = inputs[i].value.trim();
|
|
396
|
+
if (!raw) continue;
|
|
397
|
+
var problem = linkProblem(type, raw);
|
|
398
|
+
if (problem) { say(msg, problem, 'err'); inputs[i].focus(); return; }
|
|
399
|
+
links[type] = raw;
|
|
400
|
+
}
|
|
401
|
+
if (!sameLinks(links, acct.links)) body.links = Object.keys(links).length ? links : null;
|
|
402
|
+
if (!Object.keys(body).length) { say(msg, 'Nothing has changed.'); return; }
|
|
403
|
+
var btn = $('profileSave');
|
|
404
|
+
btn.disabled = true;
|
|
405
|
+
say(msg, 'Saving…');
|
|
406
|
+
request('PATCH', '/me/profile', body).then(function (res) {
|
|
407
|
+
btn.disabled = false;
|
|
408
|
+
if (res.status === 401) { sessionGone(msg); return; }
|
|
409
|
+
if (!res.ok) { say(msg, serverWords(res, 'Couldn’t save your profile. Try again.'), 'err'); return; }
|
|
410
|
+
acct.display_name = res.data.display_name;
|
|
411
|
+
acct.bio = res.data.bio;
|
|
412
|
+
acct.links = res.data.links;
|
|
413
|
+
paintProfile();
|
|
414
|
+
say(msg, 'Saved — your profile shows this everywhere now.', 'ok');
|
|
415
|
+
}, function () { btn.disabled = false; say(msg, 'Couldn’t reach the server. Try again.', 'err'); });
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// ---- save: your handle ----
|
|
419
|
+
var HANDLE_REFUSALS = {
|
|
420
|
+
invalid_handle: 'A handle is 1 to 39 lowercase letters, numbers and hyphens, and can’t start or end with a hyphen.',
|
|
421
|
+
reserved_handle: 'That handle is reserved — pick another.',
|
|
422
|
+
handle_taken: 'Someone already has that handle — pick another.',
|
|
423
|
+
handle_rate_limited: 'You can change your handle once a week, and you changed it recently — try again in a few days.',
|
|
424
|
+
};
|
|
425
|
+
$('handleForm').addEventListener('submit', function (ev) {
|
|
426
|
+
ev.preventDefault();
|
|
427
|
+
var msg = $('handleMsg');
|
|
428
|
+
var handle = $('fHandle').value.trim().toLowerCase();
|
|
429
|
+
if (!handle) { say(msg, 'Type the handle you want.', 'err'); $('fHandle').focus(); return; }
|
|
430
|
+
if (handle === (acct.handle || '')) { say(msg, 'That is already your handle.'); return; }
|
|
431
|
+
var btn = $('handleSave');
|
|
432
|
+
btn.disabled = true;
|
|
433
|
+
say(msg, 'Saving…');
|
|
434
|
+
request('PATCH', '/me/handle', { handle: handle }).then(function (res) {
|
|
435
|
+
btn.disabled = false;
|
|
436
|
+
if (res.status === 401) { sessionGone(msg); return; }
|
|
437
|
+
if (!res.ok) {
|
|
438
|
+
var code = res.data && res.data.error && res.data.error.code;
|
|
439
|
+
say(msg, HANDLE_REFUSALS[code] || serverWords(res, 'Couldn’t change your handle. Try again.'), 'err');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
acct.handle = res.data.handle;
|
|
443
|
+
paintHandle();
|
|
444
|
+
say(msg, 'Saved — your handle is now ' + acct.handle + '.', 'ok');
|
|
445
|
+
}, function () { btn.disabled = false; say(msg, 'Couldn’t reach the server. Try again.', 'err'); });
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// ---- save: who can find you, and your record — both PATCH /me/cross-project,
|
|
449
|
+
// sending only what changed, then re-reading: the effective recruiter answer
|
|
450
|
+
// is derived server-side, so the server's own view is the only one to paint ----
|
|
451
|
+
function savePrivacy(body, msg, btn, done) {
|
|
452
|
+
btn.disabled = true;
|
|
453
|
+
say(msg, 'Saving…');
|
|
454
|
+
return request('PATCH', '/me/cross-project', body).then(function (res) {
|
|
455
|
+
if (res.status === 401) { btn.disabled = false; sessionGone(msg); return; }
|
|
456
|
+
if (!res.ok) { btn.disabled = false; say(msg, serverWords(res, 'Couldn’t save your privacy choice. Try again.'), 'err'); return; }
|
|
457
|
+
return request('GET', '/me/cross-project').then(function (again) {
|
|
458
|
+
btn.disabled = false;
|
|
459
|
+
if (again.ok && again.data) { acct = again.data; paint(); }
|
|
460
|
+
say(msg, done, 'ok');
|
|
461
|
+
});
|
|
462
|
+
}, function () { btn.disabled = false; say(msg, 'Couldn’t reach the server. Try again.', 'err'); });
|
|
463
|
+
}
|
|
464
|
+
document.querySelectorAll('input[name="visibility"], input[name="recruiters"], input[name="record"]').forEach(function (r) {
|
|
465
|
+
r.addEventListener('change', paintRecruitWhat);
|
|
466
|
+
});
|
|
467
|
+
$('privacyForm').addEventListener('submit', function (ev) {
|
|
468
|
+
ev.preventDefault();
|
|
469
|
+
var msg = $('privacyMsg');
|
|
470
|
+
var body = {};
|
|
471
|
+
var said = [];
|
|
472
|
+
var vis = picked('visibility');
|
|
473
|
+
if (vis && vis !== (acct.account_visibility === 'private' ? 'private' : 'public')) {
|
|
474
|
+
body.account_visibility = vis;
|
|
475
|
+
said.push(vis === 'private' ? 'your profile page is set to private' : 'your profile page is set to public');
|
|
476
|
+
}
|
|
477
|
+
var rec = picked('recruiters');
|
|
478
|
+
if (rec && (rec === 'on') !== !!acct.recruiter_discoverable_effective) {
|
|
479
|
+
body.recruiter_discoverable = rec === 'on';
|
|
480
|
+
said.push(rec === 'on' ? 'recruiters can find you' : 'you are out of the recruiting surfaces');
|
|
481
|
+
}
|
|
482
|
+
if (!Object.keys(body).length) { say(msg, 'Nothing has changed.'); return; }
|
|
483
|
+
savePrivacy(body, msg, $('privacySave'), 'Saved — ' + said.join(', and ') + '.');
|
|
484
|
+
});
|
|
485
|
+
$('recordForm').addEventListener('submit', function (ev) {
|
|
486
|
+
ev.preventDefault();
|
|
487
|
+
var msg = $('recordMsg');
|
|
488
|
+
var hide = picked('record') === 'hide';
|
|
489
|
+
if (hide === !!acct.hide_stats) { say(msg, 'Nothing has changed.'); return; }
|
|
490
|
+
savePrivacy({ hide_stats: hide }, msg, $('recordSave'), hide
|
|
491
|
+
? 'Cross-project record hidden — removed from your profile and directories immediately.'
|
|
492
|
+
: 'Cross-project record shown on your public profile.');
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// ---- sign out: the hub's own logout (projects.html doLogout) ----
|
|
496
|
+
$('signOut').addEventListener('click', function (ev) {
|
|
497
|
+
ev.preventDefault();
|
|
498
|
+
var finish = function () {
|
|
499
|
+
var apex = location.hostname.split('.').slice(-2).join('.');
|
|
500
|
+
['bongos_session', 'gds_session', 'pms_session'].forEach(function (name) {
|
|
501
|
+
document.cookie = name + '=; Max-Age=0; path=/';
|
|
502
|
+
document.cookie = name + '=; Max-Age=0; path=/; Domain=.' + apex;
|
|
503
|
+
});
|
|
504
|
+
window.location.assign(location.pathname);
|
|
505
|
+
};
|
|
506
|
+
fetch(API + '/auth/logout', { method: 'POST', credentials: 'same-origin' }).then(finish, finish);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
load();
|
|
510
|
+
})();
|
|
511
|
+
</script>
|
|
512
|
+
</body>
|
|
513
|
+
</html>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The account page's interaction contract for modules/ui-design/kit/probe.js (task 1004259): the door signed out; signed in, each section's save driven end to end against the stub's one account (serve.js holds it for the run and refuses what the platform-identity routes refuse), so a probe that saves sees the saved value come back. A probe with `continue` keeps the previous page — the contract is a sequence.",
|
|
3
|
+
"page": "/account",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"probes": [
|
|
6
|
+
{ "name": "signed out: the door, and sign-in returns to /account", "auth": false, "width": 1440,
|
|
7
|
+
"steps": [["wait", 300]],
|
|
8
|
+
"expect": { "visible": ["#door"], "hidden": ["#acctBody"], "href": [["#signin", "/api/bongos/auth/web/start?return=/account"]] } },
|
|
9
|
+
{ "name": "signed in: nothing changed saves nothing", "auth": true, "width": 1440,
|
|
10
|
+
"steps": [["wait", 400], ["click", "#profileSave"], ["wait", 150]],
|
|
11
|
+
"expect": { "text": [["#profileMsg", "Nothing has changed."]] } },
|
|
12
|
+
{ "name": "a LinkedIn link off linkedin.com is named at the field, before any request", "continue": true,
|
|
13
|
+
"steps": [["fill", "#fLinkLinkedin", "https://evil.example/in/builder"], ["click", "#profileSave"], ["wait", 150]],
|
|
14
|
+
"expect": { "text": [["#profileMsg", "Your LinkedIn link has to point at linkedin.com."]], "activeId": "fLinkLinkedin" } },
|
|
15
|
+
{ "name": "a real bio and link save, and the saved values repaint", "continue": true,
|
|
16
|
+
"steps": [["fill", "#fLinkLinkedin", "https://www.linkedin.com/in/builder"], ["fill", "#fBio", "Ships the quiet parts."], ["click", "#profileSave"], ["wait", 300]],
|
|
17
|
+
"expect": { "text": [["#profileMsg", "Saved — your profile shows this everywhere now."], ["#bioCount", "22 of 280 characters"]], "value": [["#fLinkLinkedin", "https://www.linkedin.com/in/builder"]] } },
|
|
18
|
+
{ "name": "a taken handle is refused in plain words", "continue": true,
|
|
19
|
+
"steps": [["fill", "#fHandle", "taken"], ["click", "#handleSave"], ["wait", 300]],
|
|
20
|
+
"expect": { "text": [["#handleMsg", "Someone already has that handle — pick another."]] } },
|
|
21
|
+
{ "name": "a free handle saves, lowercased", "continue": true,
|
|
22
|
+
"steps": [["fill", "#fHandle", "Quiet-Builder"], ["click", "#handleSave"], ["wait", 300]],
|
|
23
|
+
"expect": { "text": [["#handleMsg", "Saved — your handle is now quiet-builder."]], "value": [["#fHandle", "quiet-builder"]] } },
|
|
24
|
+
{ "name": "picking Private names the recruiter exception before anything is saved", "continue": true,
|
|
25
|
+
"steps": [["click", "#aPrivacy .aOpt:nth-child(2)"], ["wait", 150]],
|
|
26
|
+
"expect": { "checked": "input[name=visibility][value=private]", "text": [["#privacyMsg", ""], ["#recWhat", "They will see your name, your picture, your roles, and whether you have been active in the last 30 days — never your credits, works shipped, karma, or the projects you build on. This is the one thing a recruiter can see while your profile page is private. Turning this off removes you from both recruiting surfaces — the recruiting directory, and a recruiter’s look-up of one builder — straight away. · For now this follows your profile page setting."]] } },
|
|
27
|
+
{ "name": "saving Private re-reads the account: reach follows the page, so recruiters are off", "continue": true,
|
|
28
|
+
"steps": [["click", "#privacySave"], ["wait", 400]],
|
|
29
|
+
"expect": { "text": [["#privacyMsg", "Saved — your profile page is set to private."]], "checked": "input[name=recruiters][value=off]" } },
|
|
30
|
+
{ "name": "hiding the record saves hide_stats alone", "continue": true,
|
|
31
|
+
"steps": [["click", "#aRecord .aOpt:nth-child(2)"], ["click", "#recordSave"], ["wait", 400]],
|
|
32
|
+
"expect": { "text": [["#recordMsg", "Cross-project record hidden — removed from your profile and directories immediately."]], "checked": "input[name=record][value=hide]" } },
|
|
33
|
+
{ "name": "phone width: the bar stays one line and nothing scrolls sideways", "auth": true, "width": 320,
|
|
34
|
+
"steps": [["wait", 400]],
|
|
35
|
+
"expect": { "noOverflow": true, "visible": ["#acctBody"] } }
|
|
36
|
+
]
|
|
37
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The account page's states for modules/ui-design/kit/render.js (task 1004259, ADR 0337 D4.3): signed out it is the sign-in door and nothing else; signed in it is the four sections, painted from the stub's one account (fixtures/me__cross-project.json — public, recruiters following the profile setting, a bio and two links). Dark only, like every apex page (ADR 0204). The private, hidden, not-yet-published account renders from account-private.states.json.",
|
|
3
|
+
"page": "/account",
|
|
4
|
+
"surface": "public-landing",
|
|
5
|
+
"modes": ["dark"],
|
|
6
|
+
"modeQuery": false,
|
|
7
|
+
"ignoreRequests": ["/api/(gds|bongos)/me/cross-project$"],
|
|
8
|
+
"ignoreConsole": ["/api/(gds|bongos)/me/cross-project", "\\b401\\b", "Unauthorized"],
|
|
9
|
+
"ignoreControls": [".vh"],
|
|
10
|
+
"states": {
|
|
11
|
+
"out": { "auth": false, "actions": [["wait", 300]], "expect": { "visible": ["#door", "#signin"], "hidden": ["#acctBody", "#aWho", "#loading"] } },
|
|
12
|
+
"in": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#acctBody", "#fName", "#fBio", "#fHandle", "#recWhat", "#recordLine", "#signOut"], "text": [["#whoName", "builder"], ["#recordLine", "Your record: 3 projects · 1000 credits · 19 works shipped · 10 karma."]], "hidden": ["#door", "#loading"], "value": [["#fHandle", "builder"], ["#fName", "A Builder"], ["#fLinkX", "https://x.com/builder"]] } },
|
|
13
|
+
"in-privacy": { "auth": true, "actions": [["wait", 400], ["click", "#aPrivacy .aOpt:nth-child(2)"], ["wait", 150]], "expect": { "checked": "input[name=visibility][value=private]", "text": [["#recWhat", "They will see your name, your picture, your roles, and whether you have been active in the last 30 days — never your credits, works shipped, karma, or the projects you build on. This is the one thing a recruiter can see while your profile page is private. Turning this off removes you from both recruiting surfaces — the recruiting directory, and a recruiter’s look-up of one builder — straight away. · For now this follows your profile page setting."]] } }
|
|
14
|
+
},
|
|
15
|
+
"reducedMotion": { "auth": true, "actions": [["wait", 300]] }
|
|
16
|
+
}
|
|
@@ -700,8 +700,11 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
|
|
|
700
700
|
<p class="bandWho" id="bandWho"></p>
|
|
701
701
|
<ul class="bandRow">
|
|
702
702
|
<li class="bandCell"><a class="bandT" href="/projects?view=mine">My projects</a></li>
|
|
703
|
-
<!-- the
|
|
704
|
-
|
|
703
|
+
<!-- the slot that waited for a settings page (task 1004259): the hub's
|
|
704
|
+
account page, where the person's own settings live (ADR 0337 D4.3).
|
|
705
|
+
Apex-only because the page is — /account is served only where
|
|
706
|
+
platform-identity runs, so a child instance loses the item whole. -->
|
|
707
|
+
<!--apex-only:start--><li class="bandCell"><a class="bandT" href="/account">Account</a></li><!--apex-only:end-->
|
|
705
708
|
<!-- the honest hover target; the script upgrades the click to the POST -->
|
|
706
709
|
<li class="bandCell"><a class="bandT signout" href="/api/bongos/auth/logout">Sign out</a></li>
|
|
707
710
|
</ul>
|