@bongos/core 1.19.92 → 1.19.94
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 +38 -88
- package/docs/design/mocks/projects-hub-mock.html +991 -0
- package/docs/design/mocks/projects-hub-variants.html +146 -0
- package/docs/design/projects-hub-direction.md +347 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +240 -0
- package/public-landing/index.html +10 -4
- package/public-landing/projects.html +663 -44
- package/scripts/gds/run-unit-tests.js +0 -1
- package/src/bongos/module-scope-map.js +1 -1
- package/src/module-api.js +5 -1
- package/src/platform-server.js +24 -42
- package/tests/known_failing_ratchet.mjs +7 -8
- package/tests/landing_page.mjs +15 -2
- package/tests/projects_hub.mjs +205 -0
- package/public-landing/gate/README.md +0 -137
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +0 -634
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +0 -809
- package/public-landing/gate/space.js +0 -1848
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/tests/gate_launch_metadata.mjs +0 -67
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title>
|
|
7
|
-
<meta name="description" content="
|
|
6
|
+
<title>{{productName}} — projects</title>
|
|
7
|
+
<meta name="description" content="{{tagline}} Browse the map of what is being built, manage your own projects, and start a new one.">
|
|
8
8
|
<!-- Fonts: the production instance also injects these from the branding pack
|
|
9
9
|
(config/branding.json theme.ui.fonts.links); kept here so the page reads
|
|
10
10
|
correctly when viewed standalone. -->
|
|
@@ -22,12 +22,35 @@
|
|
|
22
22
|
try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')document.documentElement.setAttribute('data-mode',_m);}catch(e){}
|
|
23
23
|
</script>
|
|
24
24
|
<style>
|
|
25
|
-
/*
|
|
26
|
-
|
|
25
|
+
/* Page-local layout only. /projects is the project HUB (task 1002743): one
|
|
26
|
+
surface holding the map, your projects, and creating a new one. Every
|
|
27
|
+
component it draws lives in cosmos.css (direction §6) — what stays here is
|
|
28
|
+
the per-view container rhythm and nothing else.
|
|
29
|
+
|
|
30
|
+
NO page-local :root, ever (direction C1): injectBrandingGlobal() splices the
|
|
31
|
+
pack's :root at the END of <head>, at equal specificity but later in order,
|
|
32
|
+
so a :root here would silently outrank it and defeat re-branding.
|
|
33
|
+
|
|
34
|
+
The old `.wrap{max-width:880px}` override is gone: it shadowed cosmos.css's
|
|
35
|
+
1060px purely by source order and would have clamped the map and the table.
|
|
36
|
+
|
|
27
37
|
Same trust boundary as /create — every button only ASKS (enqueues an
|
|
28
38
|
intent); the control-plane runner does the real infra work (ADR 0111 §2). */
|
|
29
|
-
.
|
|
39
|
+
.viewWrap{padding-bottom:40px;}
|
|
40
|
+
.viewWrap.narrow{max-width:820px;}
|
|
30
41
|
.card{margin-bottom:26px;}
|
|
42
|
+
|
|
43
|
+
/* The bongo pair on the New-project view. Component-scoped custom properties,
|
|
44
|
+
never :root (C1). The drum head is SKIN — using --bg-card would turn it black
|
|
45
|
+
in dark mode — and --drum-shadow is ink in light but PLAIN BLACK in dark,
|
|
46
|
+
because --ink is near-white there and an ink-filled ellipse paints a glowing
|
|
47
|
+
puddle under the drums instead of a shadow. Same move cosmos.css already
|
|
48
|
+
makes for --shadow-card. Written twice so an explicit choice and the OS
|
|
49
|
+
preference both land. */
|
|
50
|
+
.bongos{display:block;width:100%;height:auto;overflow:visible;
|
|
51
|
+
--skin-hi:#fffdf7;--skin-mid:#f4e6cd;--skin-lo:#cdb896;--drum-shadow:var(--ink);}
|
|
52
|
+
:root[data-mode="dark"] .bongos{--skin-hi:#fdf7ea;--skin-mid:#efe1c8;--skin-lo:#c3ae90;--drum-shadow:#000;}
|
|
53
|
+
@media (prefers-color-scheme:dark){:root:not([data-mode="light"]) .bongos{--skin-hi:#fdf7ea;--skin-mid:#efe1c8;--skin-lo:#c3ae90;--drum-shadow:#000;}}
|
|
31
54
|
</style>
|
|
32
55
|
</head>
|
|
33
56
|
<body>
|
|
@@ -49,48 +72,272 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
49
72
|
<span class="star" style="top:88%;left:42%;width:1.5px;height:1.5px;animation-delay:1.9s;"></span>
|
|
50
73
|
</div>
|
|
51
74
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
<!-- ══ THE PERSISTENT SHELL ═══════════════════════════════════════════════════
|
|
76
|
+
A header of the PAGE, not of the orb field, so it never drifts, scales or
|
|
77
|
+
pans with the sky — and it renders in EVERY view including the table. The
|
|
78
|
+
gate's corner pill was hidden by `body:not(.view-space)`, so the one view
|
|
79
|
+
where a person scans for their own project was the view with no way out.
|
|
80
|
+
|
|
81
|
+
Home is a PAGE (the landing page at /); the other three are VIEWS of this
|
|
82
|
+
page. They sit in one control because they are all "where can I go from
|
|
83
|
+
here", but the divider keeps the distinction honest — Home navigates and
|
|
84
|
+
never takes the current-view pill. ── -->
|
|
85
|
+
<header class="shell">
|
|
86
|
+
<!-- The mark types the PREFIX and holds `bongos` still, because `bongos` is
|
|
87
|
+
the word you type and the assistant is what varies — which is also what
|
|
88
|
+
the vanity doors do (claudebongos.com, geminibongos.com swap the first
|
|
89
|
+
word, not the second). The typing only runs on the default two-word name;
|
|
90
|
+
a re-branded instance gets its own wordmark, static. -->
|
|
91
|
+
<a class="mark" href="/"><span id="markPrefix">cloud</span><span class="caret" aria-hidden="true">▌</span> <span id="markWord">bongos</span></a>
|
|
92
|
+
|
|
93
|
+
<nav class="navrow" aria-label="Project hub">
|
|
94
|
+
<a href="/" class="navHome" data-nav="home">Home</a>
|
|
95
|
+
<span class="navSep" aria-hidden="true"></span>
|
|
96
|
+
<a href="?view=map" data-nav="map" aria-current="page">Map</a>
|
|
97
|
+
<a href="?view=mine" data-nav="mine">Your projects</a>
|
|
98
|
+
<a href="?view=new" data-nav="new">New project</a>
|
|
99
|
+
</nav>
|
|
100
|
+
|
|
101
|
+
<div class="shell-right">
|
|
102
|
+
<!-- the hall lives on a different HOST, so its href must be stamped
|
|
103
|
+
server-side: clientBranding() deliberately withholds domains, so a
|
|
104
|
+
client fill can never re-point it. apex-only — a child instance has no
|
|
105
|
+
hall of its own. -->
|
|
106
|
+
<!--apex-only:start--><a class="btn-space" data-link-hall href="{{buildersOrigin}}">🏛 Builders' hall</a><!--apex-only:end-->
|
|
56
107
|
<button type="button" class="mode-toggle" id="modeToggle" title="Switch light / dark" aria-label="Switch light or dark mode">☾</button>
|
|
57
108
|
<div class="who" id="who"></div>
|
|
109
|
+
<a class="btn-space" id="signinTop" hidden href="/api/bongos/auth/web/start?return=/projects">Sign in</a>
|
|
58
110
|
</div>
|
|
59
111
|
</header>
|
|
60
112
|
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
</
|
|
113
|
+
<noscript>
|
|
114
|
+
<div class="viewWrap wrap"><div class="fieldNote">The project map needs JavaScript. You can still
|
|
115
|
+
<a href="/create">start a project</a> or read what this is on the <a href="/">home page</a>.</div></div>
|
|
116
|
+
</noscript>
|
|
117
|
+
|
|
118
|
+
<!-- ══════════════════ VIEW · MAP ══════════════════════════════════════════
|
|
119
|
+
What you land on, signed in or out (owner, 2026-08-13). The orbs are
|
|
120
|
+
placed statically on purpose: where things sit is this pass; how the sky
|
|
121
|
+
moves is not. -->
|
|
122
|
+
<section id="view-map">
|
|
123
|
+
<div class="toolbar">
|
|
124
|
+
<div class="seg" role="group" aria-label="Map display">
|
|
125
|
+
<button type="button" id="sub-sky" aria-pressed="true">Sky</button>
|
|
126
|
+
<button type="button" id="sub-table" aria-pressed="false">Table</button>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<label class="switch" title="Show only projects you own or build on">
|
|
130
|
+
<input type="checkbox" id="onlyMine"><span class="track"></span> Only mine
|
|
131
|
+
</label>
|
|
132
|
+
|
|
133
|
+
<div class="search">
|
|
134
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
|
|
135
|
+
<input type="text" id="mapQ" placeholder="Search projects by name or description…" aria-label="Search projects">
|
|
136
|
+
<span class="count" id="mapCount"></span>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
65
139
|
|
|
66
|
-
<div class="
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
140
|
+
<div class="fieldWrap" id="sub-sky-body">
|
|
141
|
+
<div class="field" id="orbField">
|
|
142
|
+
<div class="ground"></div>
|
|
143
|
+
<div class="stars" id="mapStars" aria-hidden="true"></div>
|
|
144
|
+
<!-- The platform's own orb. Static markup, not JS-built, precisely so the
|
|
145
|
+
apex-only sentinels can remove it: a child instance has no "this
|
|
146
|
+
platform" to point at, and stripApexOnly() is a string transform that
|
|
147
|
+
cannot reach anything a script renders later. -->
|
|
148
|
+
<!--apex-only:start-->
|
|
149
|
+
<a class="worldOrb" id="selfOrb" href="/" style="--acc:var(--accent);--d:122px;left:50%;top:44%">
|
|
150
|
+
<span class="disc"></span>
|
|
151
|
+
<span class="nm" data-brand-product-inline>Cloud Bongos</span>
|
|
152
|
+
<span class="sub">this platform</span>
|
|
75
153
|
</a>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<div class="
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
154
|
+
<!--apex-only:end-->
|
|
155
|
+
<div class="fieldEmpty">
|
|
156
|
+
<p id="emptyMsg">You don’t own or build on anything here yet.</p>
|
|
157
|
+
<span class="sub">Turn the filter off to see what everyone else is building — or start your own.</span>
|
|
158
|
+
<a class="btn small" href="?view=new" data-nav-to="new">Start a project</a>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="hintbar"><span><kbd>click a project</kbd> open it</span></div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div class="wrap viewWrap" id="sub-table-body" hidden>
|
|
165
|
+
<table class="tbl">
|
|
166
|
+
<thead>
|
|
167
|
+
<tr>
|
|
168
|
+
<th class="colName" data-sort="name" aria-sort="ascending">Project</th>
|
|
169
|
+
<th class="right" data-sort="builders">Builders</th>
|
|
170
|
+
<th class="colType">Type</th>
|
|
171
|
+
<th class="colTags">Tags</th>
|
|
172
|
+
<th class="right colStatus">Status</th>
|
|
173
|
+
<th class="colGo"></th>
|
|
174
|
+
</tr>
|
|
175
|
+
</thead>
|
|
176
|
+
<tbody id="mapRows"></tbody>
|
|
177
|
+
</table>
|
|
178
|
+
<p class="finePrint" style="text-align:center">Click a column to sort.</p>
|
|
179
|
+
</div>
|
|
180
|
+
</section>
|
|
181
|
+
|
|
182
|
+
<!-- ══════════════════ VIEW · YOUR PROJECTS ════════════════════════════════ -->
|
|
183
|
+
<section id="view-mine" hidden>
|
|
184
|
+
<div class="wrap viewWrap">
|
|
185
|
+
<div class="hero" style="padding-left:0;padding-right:0">
|
|
186
|
+
<h1>Your projects</h1>
|
|
187
|
+
<p class="lede">Every project you own — what it is, whether it is answering, and the controls that matter.
|
|
188
|
+
Each card says what the platform <b>last verified</b>, and when.</p>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<main class="card">
|
|
192
|
+
<!-- signed-out gate: identity-only sign-in (ADR 0143 least-privilege — unlike
|
|
193
|
+
/create, listing your own instances needs no repo scope) -->
|
|
194
|
+
<section id="gate" hidden>
|
|
195
|
+
<p class="lede">Every project you own, in one place — its live status, its address, and the controls that matter. Sign in with GitHub to see yours.</p>
|
|
196
|
+
<a class="btn" id="signin" href="/api/bongos/auth/web/start?return=/projects%3Fview%3Dmine">
|
|
197
|
+
<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>
|
|
198
|
+
Sign in with GitHub
|
|
199
|
+
</a>
|
|
200
|
+
<div class="alert" id="gateAlert" role="alert"></div>
|
|
86
201
|
</section>
|
|
87
|
-
<ul class="projects" id="projectsList"></ul>
|
|
88
|
-
<p class="finePrint">Buttons here ask the platform to act — a runner does the real work, and each card reports only what was verified, with a timestamp. Standups keep running even with this tab closed.</p>
|
|
89
|
-
</section>
|
|
90
|
-
</main>
|
|
91
|
-
</div>
|
|
92
202
|
|
|
93
|
-
|
|
203
|
+
<!-- signed-in: the list -->
|
|
204
|
+
<section id="mine" hidden>
|
|
205
|
+
<div class="alert" id="pageAlert" role="alert"></div>
|
|
206
|
+
<div class="loading" id="loading">Reading your projects…</div>
|
|
207
|
+
|
|
208
|
+
<!-- search + state filter. The filter GROUPS the chip outcomes; it never
|
|
209
|
+
relabels them: "Live" holds both `live` and `live?` because neither is
|
|
210
|
+
known-down, and every card still says which it is. -->
|
|
211
|
+
<div class="mineBar" id="mineBar" hidden>
|
|
212
|
+
<div class="search">
|
|
213
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg>
|
|
214
|
+
<input type="text" id="mineQ" placeholder="Search your projects by name, repo or address…" aria-label="Search your projects">
|
|
215
|
+
<span class="count" id="mineCount"></span>
|
|
216
|
+
</div>
|
|
217
|
+
<div class="filters" role="group" aria-label="Filter by state">
|
|
218
|
+
<button type="button" data-f="all" aria-pressed="true">All <span class="n">0</span></button>
|
|
219
|
+
<button type="button" data-f="live" aria-pressed="false">Live <span class="n">0</span></button>
|
|
220
|
+
<button type="button" data-f="attention" aria-pressed="false">Needs you <span class="n">0</span></button>
|
|
221
|
+
<button type="button" data-f="working" aria-pressed="false">Working <span class="n">0</span></button>
|
|
222
|
+
<button type="button" data-f="gone" aria-pressed="false">Torn down <span class="n">0</span></button>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
<p class="mineEmpty" id="mineEmpty" hidden>Nothing matches that. <button type="button" class="linklike" id="mineClear">Clear the filters</button></p>
|
|
226
|
+
|
|
227
|
+
<section class="empty" id="empty" hidden>
|
|
228
|
+
<p>You don't own any projects yet. Creating one takes about five minutes — name it, pick a home for its code, and it gets an address of its own in this sky.</p>
|
|
229
|
+
<a class="btn" href="/create">Create your first project</a>
|
|
230
|
+
</section>
|
|
231
|
+
<ul class="projects" id="projectsList"></ul>
|
|
232
|
+
<p class="finePrint">Buttons here ask the platform to act — a runner does the real work, and each card reports only what was verified, with a timestamp. Standups keep running even with this tab closed.</p>
|
|
233
|
+
</section>
|
|
234
|
+
</main>
|
|
235
|
+
</div>
|
|
236
|
+
</section>
|
|
237
|
+
|
|
238
|
+
<!-- ══════════════════ VIEW · NEW PROJECT ══════════════════════════════════
|
|
239
|
+
Step 1 of the four-beat rail. Naming it here hands straight into the
|
|
240
|
+
wizard with the name already seeded (/create?name=…), which is the entry
|
|
241
|
+
point the wizard already reads. -->
|
|
242
|
+
<section id="view-new" hidden>
|
|
243
|
+
<div class="wrap viewWrap narrow">
|
|
244
|
+
<div class="card">
|
|
245
|
+
<ol class="steps">
|
|
246
|
+
<li class="now"><span class="snum">1</span><span class="slbl">Name</span></li>
|
|
247
|
+
<li><span class="snum">2</span><span class="slbl">Home</span></li>
|
|
248
|
+
<li><span class="snum">3</span><span class="slbl">Address</span></li>
|
|
249
|
+
<li><span class="snum">4</span><span class="slbl">Create</span></li>
|
|
250
|
+
</ol>
|
|
251
|
+
|
|
252
|
+
<div class="createSplit">
|
|
253
|
+
<div>
|
|
254
|
+
<h1>Start a project</h1>
|
|
255
|
+
<p class="lede">Your own project: its own repository, its own home on the web, and a hall of AI builders
|
|
256
|
+
who do the building. You set the direction; agents lay down the code.</p>
|
|
257
|
+
<form class="attach" id="newForm">
|
|
258
|
+
<input type="text" id="newName" placeholder="What should it be called?" maxlength="80" autocomplete="off" aria-label="Project name">
|
|
259
|
+
<button class="btn" type="submit">Continue →</button>
|
|
260
|
+
</form>
|
|
261
|
+
<p class="finePrint">Free while we find our feet. Your project’s repo and server are yours —
|
|
262
|
+
<span data-brand-product-inline>Cloud Bongos</span> is <a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">open source (AGPL-3.0)</a>
|
|
263
|
+
and capture-resistant by design.</p>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<!-- the bongo pair — one mark on every surface (owner, 2026-08-13).
|
|
267
|
+
The drum head is SKIN, never --bg-card, or it turns black in dark
|
|
268
|
+
mode; --drum-shadow is ink in light and plain black in dark, the
|
|
269
|
+
same move cosmos.css already makes for --shadow-card. -->
|
|
270
|
+
<div class="bongoCell">
|
|
271
|
+
<svg class="bongos" viewBox="0 0 440 320" role="img" aria-label="A pair of bongo drums">
|
|
272
|
+
<defs>
|
|
273
|
+
<linearGradient id="shellBig" x1="0" y1="0" x2="1" y2="0.35">
|
|
274
|
+
<stop offset="0%" stop-color="var(--accent-deep)"/><stop offset="16%" stop-color="var(--accent)"/>
|
|
275
|
+
<stop offset="48%" stop-color="var(--accent)"/><stop offset="86%" stop-color="var(--accent-deep)"/>
|
|
276
|
+
<stop offset="100%" stop-color="var(--accent-deep)"/>
|
|
277
|
+
</linearGradient>
|
|
278
|
+
<linearGradient id="shellSml" x1="0" y1="0" x2="1" y2="0.35">
|
|
279
|
+
<stop offset="0%" stop-color="var(--accent-deep)"/><stop offset="18%" stop-color="var(--terracotta)"/>
|
|
280
|
+
<stop offset="55%" stop-color="var(--terracotta)"/><stop offset="100%" stop-color="var(--accent-deep)"/>
|
|
281
|
+
</linearGradient>
|
|
282
|
+
<radialGradient id="skin" cx="0.36" cy="0.26" r="0.9">
|
|
283
|
+
<stop offset="0%" stop-color="var(--skin-hi)"/><stop offset="58%" stop-color="var(--skin-mid)"/>
|
|
284
|
+
<stop offset="100%" stop-color="var(--skin-lo)"/>
|
|
285
|
+
</radialGradient>
|
|
286
|
+
<radialGradient id="halo" cx="0.5" cy="0.5" r="0.5">
|
|
287
|
+
<stop offset="0%" stop-color="var(--accent)" stop-opacity="0.30"/>
|
|
288
|
+
<stop offset="48%" stop-color="var(--accent)" stop-opacity="0.13"/>
|
|
289
|
+
<stop offset="100%" stop-color="var(--accent)" stop-opacity="0"/>
|
|
290
|
+
</radialGradient>
|
|
291
|
+
<filter id="soft" x="-60%" y="-160%" width="220%" height="420%"><feGaussianBlur stdDeviation="9"/></filter>
|
|
292
|
+
</defs>
|
|
293
|
+
<ellipse class="glow" cx="222" cy="170" rx="215" ry="140" fill="url(#halo)"/>
|
|
294
|
+
<ellipse class="bshadow" cx="220" cy="286" rx="142" ry="14" fill="var(--drum-shadow)" opacity=".22" filter="url(#soft)"/>
|
|
295
|
+
<path d="M 214,196 C 232,188 250,188 264,196 L 258,232 C 244,225 230,225 219,232 Z" fill="var(--accent-deep)"/>
|
|
296
|
+
<path d="M 214,196 C 232,188 250,188 264,196 L 262,205 C 247,197 231,197 216,205 Z" fill="var(--ink)" opacity=".22"/>
|
|
297
|
+
<g class="drumBig">
|
|
298
|
+
<path d="M 74,118 C 76,166 90,228 98,266 A 52,17 0 0 0 202,266 C 210,228 224,166 226,118 Z" fill="url(#shellBig)"/>
|
|
299
|
+
<path d="M 172,124 C 174,176 186,232 192,268 A 52,17 0 0 0 202,266 C 210,228 224,166 226,118 Z" fill="var(--ink)" opacity=".17"/>
|
|
300
|
+
<path d="M 92,126 C 94,174 104,230 110,266 A 52,17 0 0 1 100,267 C 92,230 78,172 76,124 Z" fill="#fff" opacity=".20"/>
|
|
301
|
+
<ellipse cx="150" cy="121" rx="80" ry="29" fill="var(--accent-deep)"/>
|
|
302
|
+
<ellipse cx="150" cy="117" rx="80" ry="29" fill="var(--accent)"/>
|
|
303
|
+
<ellipse cx="150" cy="117" rx="80" ry="29" fill="#fff" opacity=".13"/>
|
|
304
|
+
<ellipse cx="150" cy="118" rx="70" ry="24" fill="url(#skin)"/>
|
|
305
|
+
<ellipse cx="150" cy="118" rx="70" ry="24" fill="none" stroke="var(--ink)" stroke-opacity=".09" stroke-width="1.4"/>
|
|
306
|
+
<ellipse cx="150" cy="118" rx="55" ry="18" fill="none" stroke="var(--ink)" stroke-opacity=".045" stroke-width="1"/>
|
|
307
|
+
<g fill="var(--accent-deep)">
|
|
308
|
+
<path d="M 78,138 l 9,0 -1.5,20 -6,0 z"/><path d="M 120,148 l 9,0 -1,21 -7,0 z"/>
|
|
309
|
+
<path d="M 172,148 l 9,0 -1,21 -7,0 z"/><path d="M 214,138 l 9,0 -1.5,20 -6,0 z"/>
|
|
310
|
+
</g>
|
|
311
|
+
</g>
|
|
312
|
+
<g class="drumSml">
|
|
313
|
+
<path d="M 240,134 C 242,178 252,230 258,266 A 40,13 0 0 0 338,266 C 344,230 354,178 356,134 Z" fill="url(#shellSml)"/>
|
|
314
|
+
<path d="M 314,140 C 316,186 324,234 328,268 A 40,13 0 0 0 338,266 C 344,230 354,178 356,134 Z" fill="var(--ink)" opacity=".19"/>
|
|
315
|
+
<path d="M 254,140 C 256,182 264,232 268,266 A 40,13 0 0 1 260,267 C 254,232 244,182 242,138 Z" fill="#fff" opacity=".17"/>
|
|
316
|
+
<ellipse cx="296" cy="137" rx="60" ry="22" fill="var(--accent-deep)"/>
|
|
317
|
+
<ellipse cx="296" cy="133" rx="60" ry="22" fill="var(--terracotta)"/>
|
|
318
|
+
<ellipse cx="296" cy="133" rx="60" ry="22" fill="#fff" opacity=".13"/>
|
|
319
|
+
<ellipse cx="296" cy="134" rx="51" ry="18" fill="url(#skin)"/>
|
|
320
|
+
<ellipse cx="296" cy="134" rx="51" ry="18" fill="none" stroke="var(--ink)" stroke-opacity=".09" stroke-width="1.4"/>
|
|
321
|
+
<ellipse cx="296" cy="134" rx="39" ry="13" fill="none" stroke="var(--ink)" stroke-opacity=".045" stroke-width="1"/>
|
|
322
|
+
<g fill="var(--accent-deep)">
|
|
323
|
+
<path d="M 243,152 l 8,0 -1,17 -6,0 z"/><path d="M 276,160 l 8,0 -1,18 -6,0 z"/>
|
|
324
|
+
<path d="M 312,160 l 8,0 -1,18 -6,0 z"/><path d="M 345,152 l 8,0 -1,17 -6,0 z"/>
|
|
325
|
+
</g>
|
|
326
|
+
</g>
|
|
327
|
+
</svg>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<!-- a child instance does not provision: say so plainly rather than fall
|
|
333
|
+
into the generic "couldn't read your projects" alert -->
|
|
334
|
+
<div class="fieldNote" id="childNote" style="margin-top:22px" hidden>
|
|
335
|
+
Projects aren’t created from this instance — this is a project, not the platform that hosts them.
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</section>
|
|
339
|
+
|
|
340
|
+
<div class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on <span data-brand-product-inline>Cloud Bongos</span>.</div>
|
|
94
341
|
|
|
95
342
|
<script>
|
|
96
343
|
/* Brand-pack fill (R57): the server injects window.__BRANDING__ ahead of this
|
|
@@ -109,6 +356,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
109
356
|
var b = (typeof window !== 'undefined' && window.__BRANDING__) || {};
|
|
110
357
|
var productName = (b.identity && b.identity.productName) || 'Cloud Bongos';
|
|
111
358
|
document.querySelectorAll('[data-brand-product]').forEach(function (n) { n.textContent = productName.toLowerCase(); });
|
|
359
|
+
document.querySelectorAll('[data-brand-product-inline]').forEach(function (n) { n.textContent = productName; });
|
|
112
360
|
|
|
113
361
|
function $(id) { return document.getElementById(id); }
|
|
114
362
|
|
|
@@ -133,6 +381,344 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
133
381
|
});
|
|
134
382
|
}
|
|
135
383
|
|
|
384
|
+
var REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
385
|
+
|
|
386
|
+
/* ══════════════════════════════════════════════════════════════════════════
|
|
387
|
+
THE HUB SHELL — one surface, three views, one real URL.
|
|
388
|
+
|
|
389
|
+
`?view=map|mine|new` is the deep-link contract. It is written with
|
|
390
|
+
history.replaceState so a view is linkable and shareable, and it is what
|
|
391
|
+
makes the landing page's two "Start a project" CTAs (which have always
|
|
392
|
+
pointed at /projects?view=new) actually arrive somewhere.
|
|
393
|
+
══════════════════════════════════════════════════════════════════════════ */
|
|
394
|
+
|
|
395
|
+
var VIEWS = { map: 1, mine: 1, new: 1 };
|
|
396
|
+
var currentView = 'map';
|
|
397
|
+
|
|
398
|
+
function viewFromUrl() {
|
|
399
|
+
var v;
|
|
400
|
+
try { v = new URLSearchParams(location.search).get('view'); } catch (e) { v = null; }
|
|
401
|
+
return VIEWS[v] ? v : 'map';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function setView(v, focus) {
|
|
405
|
+
if (!VIEWS[v]) v = 'map';
|
|
406
|
+
currentView = v;
|
|
407
|
+
Object.keys(VIEWS).forEach(function (k) { $('view-' + k).hidden = (k !== v); });
|
|
408
|
+
document.querySelectorAll('[data-nav]').forEach(function (a) {
|
|
409
|
+
/* Home is a PAGE, not a view — it never takes the current-view pill */
|
|
410
|
+
if (a.dataset.nav !== 'home' && a.dataset.nav === v) a.setAttribute('aria-current', 'page');
|
|
411
|
+
else a.removeAttribute('aria-current');
|
|
412
|
+
});
|
|
413
|
+
try { history.replaceState(null, '', '?view=' + v); } catch (e) {}
|
|
414
|
+
/* the mine view is the only one that polls; it re-reads on entry so a
|
|
415
|
+
card that changed while you were on the map is not stale */
|
|
416
|
+
if (v === 'mine' && me) refreshList();
|
|
417
|
+
/* focus the name field only on a deliberate move into the view — never on
|
|
418
|
+
boot, where it would steal focus from whatever the visitor arrived at */
|
|
419
|
+
if (v === 'new' && focus) $('newName').focus();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
document.querySelectorAll('[data-nav]').forEach(function (a) {
|
|
423
|
+
if (a.dataset.nav === 'home') return; /* a real navigation — leave it alone */
|
|
424
|
+
a.addEventListener('click', function (ev) { ev.preventDefault(); setView(a.dataset.nav, true); });
|
|
425
|
+
});
|
|
426
|
+
document.querySelectorAll('[data-nav-to]').forEach(function (a) {
|
|
427
|
+
a.addEventListener('click', function (ev) { ev.preventDefault(); setView(a.dataset.navTo, true); });
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
/* ── the self-typing mark. `bongos` is the CONSTANT — it is the word you
|
|
431
|
+
type — and the prefix is what varies, which is exactly what the vanity
|
|
432
|
+
doors do (claudebongos.com swaps the first word, not the second). Only
|
|
433
|
+
runs on the default two-word name so a re-branded instance keeps its own
|
|
434
|
+
wordmark, and never when the visitor asked for reduced motion. ── */
|
|
435
|
+
(function typeMark() {
|
|
436
|
+
var parts = productName.toLowerCase().split(/\s+/);
|
|
437
|
+
var word = parts.pop();
|
|
438
|
+
var prefix = parts.join(' ');
|
|
439
|
+
$('markWord').textContent = word;
|
|
440
|
+
$('markPrefix').textContent = prefix;
|
|
441
|
+
if (REDUCED || prefix !== 'cloud' || word !== 'bongos') return;
|
|
442
|
+
var PREFIXES = ['cloud', 'claude', 'chatgpt', 'gemini'];
|
|
443
|
+
var wi = 0, ci = PREFIXES[0].length, dir = -1;
|
|
444
|
+
setInterval(function () {
|
|
445
|
+
ci += dir;
|
|
446
|
+
if (ci <= 0) { dir = 1; wi = (wi + 1) % PREFIXES.length; ci = 0; }
|
|
447
|
+
else if (ci >= PREFIXES[wi].length) { dir = -1; ci = PREFIXES[wi].length; }
|
|
448
|
+
$('markPrefix').textContent = PREFIXES[wi].slice(0, ci);
|
|
449
|
+
}, 190);
|
|
450
|
+
})();
|
|
451
|
+
|
|
452
|
+
/* ══ THE MAP ═══════════════════════════════════════════════════════════════
|
|
453
|
+
The public catalog: GET /projects/featured is the PUBLIC projection
|
|
454
|
+
(featured + active only, no auth), which is what lets a signed-out visitor
|
|
455
|
+
land on the map at all. The richer GET /projects needs project.curate and
|
|
456
|
+
would 401 them. It carries origin/name/tagline/art_url and nothing else —
|
|
457
|
+
so builders, type and tags render as "—" rather than as invented values.
|
|
458
|
+
The orbs are placed on fixed slots: where things sit is this pass; how the
|
|
459
|
+
sky moves is goal 1000031. ══ */
|
|
460
|
+
|
|
461
|
+
var ORB_SLOTS = [
|
|
462
|
+
[22, 30, 74], [78, 66, 60], [33, 74, 56], [70, 26, 64],
|
|
463
|
+
[13, 58, 52], [88, 42, 50], [50, 80, 54], [62, 48, 58]
|
|
464
|
+
];
|
|
465
|
+
/* ONE accent palette for every orb on the page — the map's spheres and the
|
|
466
|
+
manage cards' — so a project reads the same colour in both views.
|
|
467
|
+
Token references, never literals: the five hardcoded hexes this replaced
|
|
468
|
+
followed no re-theme (two happened to equal --sun-lit/--gold-soft, three
|
|
469
|
+
were orphans). Keeping the per-project variety is what makes a sky legible;
|
|
470
|
+
deriving it from contract tokens is what makes it survive a re-brand. */
|
|
471
|
+
var ORB_ACCENTS = ['var(--sea)', 'var(--terracotta)', 'var(--gold-soft)', 'var(--sun-lit)', 'var(--accent)'];
|
|
472
|
+
var ORB_GLOWS = ORB_ACCENTS.map(function (t) { return 'color-mix(in srgb,' + t + ' 45%,transparent)'; });
|
|
473
|
+
var catalog = [];
|
|
474
|
+
var mineOrigins = {}; /* normalized host → 'owner' | 'builder' */
|
|
475
|
+
var mapSort = { key: 'name', dir: 1 };
|
|
476
|
+
|
|
477
|
+
/* Catalog origins become hrefs, so they are scheme-checked before they are
|
|
478
|
+
rendered — esc() neutralizes HTML metacharacters but not a `javascript:`
|
|
479
|
+
URI, and this page also drives teardown / revive / domain-attach for the
|
|
480
|
+
signed-in owner, so script running in its origin is not a small thing.
|
|
481
|
+
Same predicate the landing page's catalog band already uses. */
|
|
482
|
+
function safeHttpUrl(raw) {
|
|
483
|
+
try {
|
|
484
|
+
var u = new URL(String(raw));
|
|
485
|
+
return (u.protocol === 'https:' || u.protocol === 'http:') ? u.href : '';
|
|
486
|
+
} catch (e) { return ''; }
|
|
487
|
+
}
|
|
488
|
+
function hostOf(origin) {
|
|
489
|
+
return String(origin || '').replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/.*$/, '').toLowerCase();
|
|
490
|
+
}
|
|
491
|
+
/* Accent by POSITION in a canonical (origin-sorted) order, not by hashing the
|
|
492
|
+
name: a hash collides — with five tokens and four projects it put three of
|
|
493
|
+
them on the same ember and the sky stopped being readable. Position over
|
|
494
|
+
the FULL catalog, so filtering or sorting the view never re-colours an orb
|
|
495
|
+
the visitor has already learned. */
|
|
496
|
+
function assignAccents() {
|
|
497
|
+
catalog.slice()
|
|
498
|
+
.sort(function (a, b) { return String(a.origin).localeCompare(String(b.origin)); })
|
|
499
|
+
.forEach(function (p, i) { p._acc = ORB_ACCENTS[i % ORB_ACCENTS.length]; });
|
|
500
|
+
}
|
|
501
|
+
function accentOf(p) { return p._acc || ORB_ACCENTS[0]; }
|
|
502
|
+
function relOf(p) { return mineOrigins[hostOf(p.origin)] || 'none'; }
|
|
503
|
+
function matchesMapQuery(p, q) {
|
|
504
|
+
if (!q) return true;
|
|
505
|
+
return (String(p.name || '') + ' ' + String(p.tagline || '')).toLowerCase().indexOf(q) !== -1;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function renderOrbs(list) {
|
|
509
|
+
var field = $('orbField');
|
|
510
|
+
/* replace only the generated orbs — the platform's own orb is static
|
|
511
|
+
apex-only markup and must survive every re-render */
|
|
512
|
+
field.querySelectorAll('.worldOrb[data-generated]').forEach(function (n) { n.remove(); });
|
|
513
|
+
var hint = field.querySelector('.hintbar');
|
|
514
|
+
list.forEach(function (p, i) {
|
|
515
|
+
var slot = ORB_SLOTS[i % ORB_SLOTS.length];
|
|
516
|
+
var a = document.createElement('a');
|
|
517
|
+
a.className = 'worldOrb';
|
|
518
|
+
a.setAttribute('data-generated', '');
|
|
519
|
+
a.setAttribute('data-rel', relOf(p));
|
|
520
|
+
var href = safeHttpUrl(p.origin);
|
|
521
|
+
a.href = href || '#';
|
|
522
|
+
if (href) { a.target = '_blank'; a.rel = 'noopener'; }
|
|
523
|
+
a.title = p.tagline || p.name || '';
|
|
524
|
+
a.style.cssText = '--acc:' + accentOf(p) +
|
|
525
|
+
';--d:' + slot[2] + 'px;left:' + slot[0] + '%;top:' + slot[1] + '%';
|
|
526
|
+
var rel = relOf(p);
|
|
527
|
+
a.innerHTML =
|
|
528
|
+
(rel === 'owner' ? '<span class="badge">yours</span>' :
|
|
529
|
+
rel === 'builder' ? '<span class="badge">you build here</span>' : '') +
|
|
530
|
+
'<span class="disc"></span>' +
|
|
531
|
+
'<span class="nm">' + esc(p.name) + '</span>';
|
|
532
|
+
field.insertBefore(a, hint);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function renderRows(list) {
|
|
537
|
+
$('mapRows').innerHTML = list.map(function (p) {
|
|
538
|
+
var rel = relOf(p);
|
|
539
|
+
var pill = rel === 'owner' ? ' <span class="pill">yours</span>'
|
|
540
|
+
: rel === 'builder' ? ' <span class="pill">you build here</span>' : '';
|
|
541
|
+
return '<tr>' +
|
|
542
|
+
'<td class="colName"><span class="pname"><span class="pdot" style="--acc:' +
|
|
543
|
+
accentOf(p) + '"></span>' +
|
|
544
|
+
'<span><span class="ptitle">' + esc(p.name) + '</span>' + pill + '<br>' +
|
|
545
|
+
'<span class="ptag">' + esc(p.tagline || '') + '</span></span></span></td>' +
|
|
546
|
+
/* the public projection carries no builder count, type or tags — an
|
|
547
|
+
em dash is the truthful cell, an invented number is not */
|
|
548
|
+
'<td class="right num">—</td>' +
|
|
549
|
+
'<td class="colType">—</td>' +
|
|
550
|
+
'<td class="colTags">—</td>' +
|
|
551
|
+
/* liveness on a catalog row is NOT probed by this surface. A chip that
|
|
552
|
+
said "live" here would claim more than anything verified (C6); the
|
|
553
|
+
verification overlay on owned orbs is its own task. */
|
|
554
|
+
'<td class="right colStatus"><span class="chip calm"><span class="cdot"></span>—</span></td>' +
|
|
555
|
+
'<td class="colGo">' + (safeHttpUrl(p.origin)
|
|
556
|
+
? '<a class="btn small ghost" href="' + esc(safeHttpUrl(p.origin)) + '" target="_blank" rel="noopener">Open →</a>'
|
|
557
|
+
: '') + '</td>' +
|
|
558
|
+
'</tr>';
|
|
559
|
+
}).join('');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function applyMapFilter() {
|
|
563
|
+
var onlyMine = $('onlyMine').checked;
|
|
564
|
+
var q = $('mapQ').value.trim().toLowerCase();
|
|
565
|
+
var list = catalog.filter(function (p) {
|
|
566
|
+
return matchesMapQuery(p, q) && (!onlyMine || relOf(p) !== 'none');
|
|
567
|
+
});
|
|
568
|
+
list.sort(function (x, y) {
|
|
569
|
+
if (mapSort.key === 'name') return String(x.name || '').localeCompare(String(y.name || '')) * mapSort.dir;
|
|
570
|
+
return 0;
|
|
571
|
+
});
|
|
572
|
+
renderOrbs(list);
|
|
573
|
+
renderRows(list);
|
|
574
|
+
var self = $('selfOrb');
|
|
575
|
+
if (self) self.hidden = onlyMine; /* the platform is nobody's project */
|
|
576
|
+
$('orbField').classList.toggle('isEmpty', list.length === 0 && (onlyMine || !!q));
|
|
577
|
+
$('emptyMsg').textContent = onlyMine
|
|
578
|
+
? 'You don’t own or build on anything here yet.'
|
|
579
|
+
: 'Nothing here matches that.';
|
|
580
|
+
$('mapCount').textContent = list.length + (list.length === 1 ? ' project' : ' projects');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function loadCatalog() {
|
|
584
|
+
fetch(API + '/projects/featured', { credentials: 'same-origin' })
|
|
585
|
+
.then(function (res) { if (!res.ok) throw new Error('catalog'); return res.json(); })
|
|
586
|
+
.then(function (data) { catalog = (data && data.projects) || []; assignAccents(); applyMapFilter(); })
|
|
587
|
+
.catch(function () {
|
|
588
|
+
/* a child instance has no catalog module at all — say so plainly
|
|
589
|
+
rather than leave an empty sky with no explanation */
|
|
590
|
+
catalog = [];
|
|
591
|
+
applyMapFilter();
|
|
592
|
+
$('mapCount').textContent = '';
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/* "Mine" is a MEMBERSHIP, not the catalog's advisory owner column: the
|
|
597
|
+
authority is platform_identity_project_memberships, whose role is
|
|
598
|
+
owner|builder|visitor, and GET /my-projects already returns the
|
|
599
|
+
{ owned, building } split. 401 (signed out) is not an error here. */
|
|
600
|
+
function loadMemberships() {
|
|
601
|
+
return fetch(API + '/my-projects', { credentials: 'same-origin' })
|
|
602
|
+
.then(function (res) { return res.ok ? res.json() : { owned: [], building: [] }; })
|
|
603
|
+
.then(function (d) {
|
|
604
|
+
mineOrigins = {};
|
|
605
|
+
(d.owned || []).forEach(function (m) { if (m.origin) mineOrigins[hostOf(m.origin)] = 'owner'; });
|
|
606
|
+
(d.building || []).forEach(function (m) { if (m.origin) mineOrigins[hostOf(m.origin)] = 'builder'; });
|
|
607
|
+
})
|
|
608
|
+
.catch(function () { mineOrigins = {}; });
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
$('onlyMine').addEventListener('change', applyMapFilter);
|
|
612
|
+
$('mapQ').addEventListener('input', applyMapFilter);
|
|
613
|
+
document.querySelectorAll('.tbl th[data-sort]').forEach(function (th) {
|
|
614
|
+
th.style.cursor = 'pointer';
|
|
615
|
+
th.addEventListener('click', function () {
|
|
616
|
+
var k = th.getAttribute('data-sort');
|
|
617
|
+
mapSort.dir = (mapSort.key === k) ? -mapSort.dir : 1;
|
|
618
|
+
mapSort.key = k;
|
|
619
|
+
document.querySelectorAll('.tbl th[data-sort]').forEach(function (o) { o.removeAttribute('aria-sort'); });
|
|
620
|
+
th.setAttribute('aria-sort', mapSort.dir === 1 ? 'ascending' : 'descending');
|
|
621
|
+
applyMapFilter();
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
function setSub(which) {
|
|
626
|
+
$('sub-sky').setAttribute('aria-pressed', String(which === 'sky'));
|
|
627
|
+
$('sub-table').setAttribute('aria-pressed', String(which === 'table'));
|
|
628
|
+
$('sub-sky-body').hidden = which !== 'sky';
|
|
629
|
+
$('sub-table-body').hidden = which !== 'table';
|
|
630
|
+
}
|
|
631
|
+
$('sub-sky').addEventListener('click', function () { setSub('sky'); });
|
|
632
|
+
$('sub-table').addEventListener('click', function () { setSub('table'); });
|
|
633
|
+
|
|
634
|
+
/* the starfield only paints where --star-display is block (dark mode) */
|
|
635
|
+
(function stars() {
|
|
636
|
+
var frag = document.createDocumentFragment();
|
|
637
|
+
for (var i = 0; i < 90; i++) {
|
|
638
|
+
var s = document.createElement('i');
|
|
639
|
+
s.style.left = ((i * 37 + 11) % 100) + '%';
|
|
640
|
+
s.style.top = ((i * 61 + 7) % 100) + '%';
|
|
641
|
+
s.style.opacity = (0.25 + ((i * 13) % 50) / 100).toFixed(2);
|
|
642
|
+
frag.appendChild(s);
|
|
643
|
+
}
|
|
644
|
+
$('mapStars').appendChild(frag);
|
|
645
|
+
})();
|
|
646
|
+
|
|
647
|
+
/* ══ NEW PROJECT — step 1 of the four-beat rail. Naming it here hands into
|
|
648
|
+
the wizard with the name already seeded; /create?name= is the entry point
|
|
649
|
+
the wizard already reads, so nothing about the flow changes. ══ */
|
|
650
|
+
$('newForm').addEventListener('submit', function (ev) {
|
|
651
|
+
ev.preventDefault();
|
|
652
|
+
var name = String($('newName').value || '').trim();
|
|
653
|
+
location.assign('/create' + (name ? '?name=' + encodeURIComponent(name) : ''));
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
/* ══ YOUR PROJECTS — search + the five state pills. The filter GROUPS the
|
|
657
|
+
chip outcomes; it never relabels them, so "Live" holding both `live` and
|
|
658
|
+
`live?` is a question about what is known-down, not a claim that they are
|
|
659
|
+
the same thing. Counts recompute against the search so a "Needs you 0" is
|
|
660
|
+
a real answer rather than a stale one. ══ */
|
|
661
|
+
|
|
662
|
+
var mineFilter = 'all';
|
|
663
|
+
var STATE_GROUP = {
|
|
664
|
+
active: 'live', requested: 'working', provisioning: 'working',
|
|
665
|
+
tearing_down: 'working', error: 'attention', torn_down: 'gone'
|
|
666
|
+
};
|
|
667
|
+
function groupFor(inst) {
|
|
668
|
+
if (inst.status === 'active' && inst.last_verified_up === false) return 'attention';
|
|
669
|
+
return STATE_GROUP[inst.status] || 'all';
|
|
670
|
+
}
|
|
671
|
+
function findText(inst) {
|
|
672
|
+
return [inst.slug, inst.target_ref, inst.domain].join(' ').toLowerCase();
|
|
673
|
+
}
|
|
674
|
+
function applyMine() {
|
|
675
|
+
var q = $('mineQ').value.trim().toLowerCase();
|
|
676
|
+
var shown = 0;
|
|
677
|
+
Object.keys(byId).forEach(function (id) {
|
|
678
|
+
var inst = byId[id];
|
|
679
|
+
var li = document.querySelector('[data-inst="' + esc(String(id)) + '"]');
|
|
680
|
+
if (!li) return;
|
|
681
|
+
var keep = (mineFilter === 'all' || groupFor(inst) === mineFilter) &&
|
|
682
|
+
(!q || findText(inst).indexOf(q) !== -1);
|
|
683
|
+
li.hidden = !keep;
|
|
684
|
+
if (keep) shown++;
|
|
685
|
+
});
|
|
686
|
+
$('mineCount').textContent = shown + (shown === 1 ? ' project' : ' projects');
|
|
687
|
+
$('mineEmpty').hidden = shown !== 0 || !Object.keys(byId).length;
|
|
688
|
+
}
|
|
689
|
+
function recountMine() {
|
|
690
|
+
var q = $('mineQ').value.trim().toLowerCase();
|
|
691
|
+
var tally = { all: 0 };
|
|
692
|
+
Object.keys(byId).forEach(function (id) {
|
|
693
|
+
var inst = byId[id];
|
|
694
|
+
if (q && findText(inst).indexOf(q) === -1) return;
|
|
695
|
+
tally.all++;
|
|
696
|
+
var g = groupFor(inst);
|
|
697
|
+
tally[g] = (tally[g] || 0) + 1;
|
|
698
|
+
});
|
|
699
|
+
document.querySelectorAll('.filters button').forEach(function (btn) {
|
|
700
|
+
btn.querySelector('.n').textContent = tally[btn.dataset.f] || 0;
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
document.querySelectorAll('.filters button').forEach(function (btn) {
|
|
704
|
+
btn.addEventListener('click', function () {
|
|
705
|
+
mineFilter = btn.dataset.f;
|
|
706
|
+
document.querySelectorAll('.filters button').forEach(function (o) {
|
|
707
|
+
o.setAttribute('aria-pressed', String(o === btn));
|
|
708
|
+
});
|
|
709
|
+
applyMine();
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
$('mineQ').addEventListener('input', function () { recountMine(); applyMine(); });
|
|
713
|
+
$('mineClear').addEventListener('click', function () {
|
|
714
|
+
$('mineQ').value = '';
|
|
715
|
+
mineFilter = 'all';
|
|
716
|
+
document.querySelectorAll('.filters button').forEach(function (o) {
|
|
717
|
+
o.setAttribute('aria-pressed', String(o.dataset.f === 'all'));
|
|
718
|
+
});
|
|
719
|
+
recountMine(); applyMine();
|
|
720
|
+
});
|
|
721
|
+
|
|
136
722
|
var me = null;
|
|
137
723
|
var byId = {}; /* id → last-seen instance (+open_intent) */
|
|
138
724
|
var timers = {}; /* id → watch timer */
|
|
@@ -218,8 +804,6 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
218
804
|
|
|
219
805
|
/* ── the orb: the gate's project sphere, glow keyed to verification ────── */
|
|
220
806
|
|
|
221
|
-
var ORB_ACCENTS = ['#fb923c', '#38bdf8', '#fb7185', '#f6c976', '#8b7ec8'];
|
|
222
|
-
var ORB_GLOWS = ['rgba(251,146,60,.5)', 'rgba(56,189,248,.5)', 'rgba(251,113,133,.45)', 'rgba(246,201,118,.45)', 'rgba(139,126,200,.45)'];
|
|
223
807
|
function orbFor(inst) {
|
|
224
808
|
var h = 0, slug = String(inst.slug || '');
|
|
225
809
|
for (var i = 0; i < slug.length; i++) h = (h * 31 + slug.charCodeAt(i)) >>> 0;
|
|
@@ -374,10 +958,15 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
374
958
|
byId = {};
|
|
375
959
|
if (!instances.length) {
|
|
376
960
|
$('empty').hidden = false;
|
|
961
|
+
$('mineBar').hidden = true;
|
|
962
|
+
$('mineEmpty').hidden = true;
|
|
377
963
|
list.innerHTML = '';
|
|
378
964
|
return;
|
|
379
965
|
}
|
|
380
966
|
$('empty').hidden = true;
|
|
967
|
+
/* the search + state filter only earn their space once there is a list to
|
|
968
|
+
narrow — one project does not need a filter bar above it */
|
|
969
|
+
$('mineBar').hidden = instances.length < 2;
|
|
381
970
|
list.innerHTML = instances.map(function (inst) {
|
|
382
971
|
return '<li class="proj" data-inst="' + esc(String(inst.id)) + '"></li>';
|
|
383
972
|
}).join('');
|
|
@@ -386,6 +975,8 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
386
975
|
renderCard(inst);
|
|
387
976
|
if (shouldWatch(inst)) watch(inst.id);
|
|
388
977
|
});
|
|
978
|
+
recountMine();
|
|
979
|
+
applyMine();
|
|
389
980
|
}
|
|
390
981
|
|
|
391
982
|
function msg(id, text, cls) {
|
|
@@ -421,7 +1012,7 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
421
1012
|
lastMsg[id] = null;
|
|
422
1013
|
}
|
|
423
1014
|
byId[id] = inst;
|
|
424
|
-
if (changed) renderCard(inst);
|
|
1015
|
+
if (changed) { renderCard(inst); recountMine(); applyMine(); }
|
|
425
1016
|
}
|
|
426
1017
|
function watch(id) {
|
|
427
1018
|
stopWatch(id);
|
|
@@ -478,8 +1069,12 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
478
1069
|
})
|
|
479
1070
|
.catch(function () { /* transient — next tick will try again */ });
|
|
480
1071
|
}
|
|
481
|
-
|
|
482
|
-
|
|
1072
|
+
/* view-guarded: the map and the create view have no cards to refresh, so the
|
|
1073
|
+
poll must not run there — a hub that kept re-reading the owner's instances
|
|
1074
|
+
while you browsed the sky would be spending requests on nothing */
|
|
1075
|
+
function shouldPoll() { return !document.hidden && me && currentView === 'mine'; }
|
|
1076
|
+
setInterval(function () { if (shouldPoll()) refreshList(); }, REFRESH_MS);
|
|
1077
|
+
document.addEventListener('visibilitychange', function () { if (shouldPoll()) refreshList(); });
|
|
483
1078
|
|
|
484
1079
|
/* ── teardown: ask, confirm (the truth, itemized — direction §5.4), then
|
|
485
1080
|
SAY exactly what the server said ── */
|
|
@@ -650,17 +1245,38 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
650
1245
|
});
|
|
651
1246
|
}
|
|
652
1247
|
|
|
1248
|
+
/* the shell never renders a door it has not earned: signed out, there is no
|
|
1249
|
+
identity chip, no "your projects" tab and no only-mine filter — but the
|
|
1250
|
+
view itself stays reachable by URL and answers with the sign-in gate */
|
|
1251
|
+
function paintShellIdentity() {
|
|
1252
|
+
var signedIn = !!me;
|
|
1253
|
+
$('signinTop').hidden = signedIn;
|
|
1254
|
+
$('who').hidden = !signedIn;
|
|
1255
|
+
document.querySelector('[data-nav="mine"]').hidden = !signedIn;
|
|
1256
|
+
$('onlyMine').closest('.switch').hidden = !signedIn;
|
|
1257
|
+
if (!signedIn && $('onlyMine').checked) { $('onlyMine').checked = false; applyMapFilter(); }
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/* Boot. The view is set FIRST, from the URL, so the page is navigable before
|
|
1261
|
+
any request resolves — /projects?view=new must land on the create view even
|
|
1262
|
+
if the identity call is slow or fails. */
|
|
1263
|
+
setView(viewFromUrl());
|
|
1264
|
+
|
|
653
1265
|
fetch(API + '/me', { credentials: 'same-origin' })
|
|
654
1266
|
.then(function (res) { if (!res.ok) throw new Error(String(res.status)); return res.json(); })
|
|
655
1267
|
.then(function (data) {
|
|
656
1268
|
me = data.builder || data.me || data;
|
|
657
1269
|
renderWho();
|
|
1270
|
+
paintShellIdentity();
|
|
658
1271
|
$('gate').hidden = true;
|
|
659
1272
|
$('mine').hidden = false;
|
|
660
1273
|
load();
|
|
1274
|
+
return loadMemberships();
|
|
661
1275
|
})
|
|
662
1276
|
.catch(function (err) {
|
|
1277
|
+
me = null;
|
|
663
1278
|
renderWho();
|
|
1279
|
+
paintShellIdentity();
|
|
664
1280
|
$('gate').hidden = false;
|
|
665
1281
|
if (String(err && err.message) !== '401') {
|
|
666
1282
|
var g = $('gateAlert');
|
|
@@ -669,7 +1285,10 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
669
1285
|
g.classList.add('on');
|
|
670
1286
|
}
|
|
671
1287
|
}
|
|
672
|
-
})
|
|
1288
|
+
})
|
|
1289
|
+
/* the catalog is PUBLIC, so it loads either way — the map is the view a
|
|
1290
|
+
signed-out stranger lands on and it must not depend on identity */
|
|
1291
|
+
.then(loadCatalog, loadCatalog);
|
|
673
1292
|
})();
|
|
674
1293
|
</script>
|
|
675
1294
|
</body>
|