@bongos/core 1.19.82 → 1.19.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +57 -22
- package/docs/adr/0055-server-mediated-branch-publish.md +4 -0
- package/docs/design/mocks/component-sheet.html +428 -0
- package/docs/design/mocks/projects-page-mock.html +167 -0
- package/docs/design/projects-surfaces-redesign-direction.md +3 -2
- package/modules/lifecycle/github-push.js +82 -0
- package/modules/provisioning/migrations/provisioning_006_liveness.sql +30 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/onboard-plan.js +20 -4
- package/modules/provisioning/pollers/liveness-sweep.js +266 -0
- package/modules/provisioning/provisioning.js +43 -0
- package/modules/provisioning/routes/provisioning.js +6 -2
- package/modules/provisioning/tests/liveness.mjs +420 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +311 -0
- package/public-landing/create.html +253 -125
- package/public-landing/gate/index.html +3 -0
- package/public-landing/gate/space.css +11 -1
- package/public-landing/projects.html +298 -213
- package/scripts/gds/publish-credential-check.js +63 -0
- package/src/module-api.js +5 -1
- package/tests/publish_branch_route.mjs +69 -0
|
@@ -10,184 +10,64 @@
|
|
|
10
10
|
correctly when viewed standalone. -->
|
|
11
11
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
12
12
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
13
|
-
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700&family=Quicksand:wght@400;500;600;700&display=swap">
|
|
13
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;600;700&family=Quicksand:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap">
|
|
14
|
+
<!-- The R07 redesign (task 1002696): shared token + component layer — the
|
|
15
|
+
space sky both modes ride on, light/dark governing the cards. The server
|
|
16
|
+
re-injects the branding :root AFTER stylesheets, so the pack stays
|
|
17
|
+
authoritative. Design contract: docs/design/projects-surfaces-redesign-direction.md -->
|
|
18
|
+
<link rel="stylesheet" href="/assets/cosmos.css">
|
|
19
|
+
<script>
|
|
20
|
+
/* apply the saved color mode BEFORE first paint so there's no flash;
|
|
21
|
+
no saved choice = follow the OS (prefers-color-scheme) */
|
|
22
|
+
try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')document.documentElement.setAttribute('data-mode',_m);}catch(e){}
|
|
23
|
+
</script>
|
|
14
24
|
<style>
|
|
15
|
-
/*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/create — every button here only ASKS (enqueues an intent); the control-plane
|
|
22
|
-
runner does the real infra work (ADR 0111 §2). ── */
|
|
23
|
-
:root{
|
|
24
|
-
--bg:#fff7ed; --bg-deep:#ffedd5;
|
|
25
|
-
--ink:#2a1f17; --ink-soft:#6b5d52; --ink-faint:#9c8d80;
|
|
26
|
-
--accent:#f97316; --accent-deep:#ea580c; --sea:#38bdf8; --terracotta:#fb7185;
|
|
27
|
-
--card:#fffdfa; --line:#f3e1c9; --ok:#16a34a; --err:#dc2626;
|
|
28
|
-
--font-display:"Baloo 2","Quicksand",system-ui,"Arial Rounded MT Bold",sans-serif;
|
|
29
|
-
--font-body:"Quicksand",system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
30
|
-
--font-mono:"Fira Code",ui-monospace,SFMono-Regular,Menlo,monospace;
|
|
31
|
-
}
|
|
32
|
-
*{box-sizing:border-box;margin:0;padding:0;}
|
|
33
|
-
[hidden]{display:none !important;}
|
|
34
|
-
html{scroll-behavior:smooth;}
|
|
35
|
-
body{
|
|
36
|
-
font-family:var(--font-body); color:var(--ink);
|
|
37
|
-
background:var(--bg);
|
|
38
|
-
min-height:100vh; -webkit-font-smoothing:antialiased; overflow-x:hidden;
|
|
39
|
-
}
|
|
40
|
-
/* the sky — same fixed dawn layer as /create (gradient here, not on body:
|
|
41
|
-
fixed-attachment backgrounds black out on scrolled composited pages) */
|
|
42
|
-
.sky{
|
|
43
|
-
position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden;
|
|
44
|
-
background:linear-gradient(180deg,#e9f5fd 0%,#fde8c8 34%,#fff4e6 62%,#fffdfa 100%);
|
|
45
|
-
}
|
|
46
|
-
.cloud{position:absolute;border-radius:50%;background:rgba(255,255,255,0.55);filter:blur(38px);}
|
|
47
|
-
.cloud.a{width:380px;height:380px;top:-120px;left:-80px;animation:drift 26s ease-in-out infinite alternate;}
|
|
48
|
-
.cloud.b{width:300px;height:300px;top:4%;right:-60px;background:rgba(255,224,178,0.5);animation:drift 32s ease-in-out infinite alternate-reverse;}
|
|
49
|
-
.cloud.c{width:460px;height:460px;bottom:-200px;left:24%;background:rgba(255,255,255,0.4);animation:drift 40s ease-in-out infinite alternate;}
|
|
50
|
-
@keyframes drift{from{transform:translateX(-18px)}to{transform:translateX(26px)}}
|
|
51
|
-
|
|
52
|
-
/* ── header ── */
|
|
53
|
-
.top{
|
|
54
|
-
position:relative;z-index:2;display:flex;align-items:center;justify-content:space-between;
|
|
55
|
-
padding:20px clamp(18px,4vw,40px);
|
|
56
|
-
}
|
|
57
|
-
.top .mark{
|
|
58
|
-
font-family:var(--font-display);font-weight:700;font-size:22px;letter-spacing:-0.5px;
|
|
59
|
-
text-transform:lowercase;color:var(--ink);text-decoration:none;
|
|
60
|
-
}
|
|
61
|
-
.top .mark span{color:var(--accent);}
|
|
62
|
-
.top-right{display:flex;align-items:center;gap:clamp(10px,2vw,18px);}
|
|
63
|
-
.who{display:flex;align-items:center;gap:10px;font-size:14px;font-weight:600;color:var(--ink-soft);min-height:34px;}
|
|
64
|
-
.who img{width:30px;height:30px;border-radius:50%;border:2px solid #fff;box-shadow:0 2px 8px rgba(42,31,23,0.12);}
|
|
65
|
-
|
|
66
|
-
/* ── layout ── */
|
|
67
|
-
.wrap{
|
|
68
|
-
position:relative;z-index:1;max-width:880px;margin:0 auto;
|
|
69
|
-
padding:clamp(8px,2vw,28px) clamp(16px,4vw,40px) 80px;
|
|
70
|
-
}
|
|
71
|
-
.card{
|
|
72
|
-
background:var(--card);border-radius:26px;
|
|
73
|
-
box-shadow:0 24px 60px -24px rgba(234,88,12,0.22), 0 6px 22px -8px rgba(42,31,23,0.10);
|
|
74
|
-
padding:clamp(22px,3.6vw,40px);
|
|
75
|
-
animation:rise .7s ease-out both;
|
|
76
|
-
}
|
|
77
|
-
@keyframes rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
|
|
78
|
-
h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32px);letter-spacing:-0.5px;line-height:1.15;margin-bottom:8px;}
|
|
79
|
-
.lede{font-size:15.5px;color:var(--ink-soft);font-weight:500;line-height:1.55;margin-bottom:24px;max-width:60ch;}
|
|
80
|
-
.lede a,.projMsg a,.pnote a{color:var(--accent-deep);font-weight:700;text-decoration:none;}
|
|
81
|
-
.lede a:hover,.projMsg a:hover,.pnote a:hover{text-decoration:underline;}
|
|
82
|
-
|
|
83
|
-
/* ── buttons ── */
|
|
84
|
-
.btn{
|
|
85
|
-
display:inline-flex;align-items:center;justify-content:center;gap:9px;cursor:pointer;
|
|
86
|
-
font-family:var(--font-display);font-weight:700;font-size:15px;color:#fff;
|
|
87
|
-
background:var(--accent);border:none;border-radius:999px;padding:11px 22px;text-decoration:none;
|
|
88
|
-
box-shadow:0 10px 22px -8px rgba(249,115,22,0.7);
|
|
89
|
-
transition:transform .15s ease, box-shadow .15s ease, background .15s ease;
|
|
90
|
-
}
|
|
91
|
-
.btn:hover{transform:translateY(-1px);box-shadow:0 14px 28px -8px rgba(249,115,22,0.8);}
|
|
92
|
-
.btn:disabled{opacity:0.6;cursor:default;transform:none;}
|
|
93
|
-
.btn.ghost{background:transparent;color:var(--ink-soft);box-shadow:none;border:2px solid var(--line);}
|
|
94
|
-
.btn.ghost:hover{border-color:var(--accent);color:var(--accent-deep);}
|
|
95
|
-
.btn.small{font-size:13.5px;padding:8px 16px;}
|
|
96
|
-
.btn.danger{background:transparent;color:var(--err);box-shadow:none;border:2px solid rgba(220,38,38,0.35);}
|
|
97
|
-
.btn.danger:hover{background:rgba(220,38,38,0.06);border-color:var(--err);}
|
|
98
|
-
.btn .gh{width:17px;height:17px;fill:currentColor;}
|
|
99
|
-
.linklike{background:none;border:none;cursor:pointer;font-family:var(--font-body);font-weight:700;font-size:13.5px;color:var(--accent-deep);text-decoration:underline;padding:6px 4px;}
|
|
100
|
-
|
|
101
|
-
/* ── project cards ── */
|
|
102
|
-
.projects{list-style:none;display:flex;flex-direction:column;gap:18px;}
|
|
103
|
-
.proj{
|
|
104
|
-
border:2px solid var(--line);border-radius:18px;padding:18px 20px;background:#fff;
|
|
105
|
-
}
|
|
106
|
-
.proj.gone{opacity:0.62;}
|
|
107
|
-
.projHead{display:flex;align-items:center;gap:12px;flex-wrap:wrap;}
|
|
108
|
-
.projName{font-family:var(--font-display);font-weight:700;font-size:19px;letter-spacing:-0.3px;}
|
|
109
|
-
.chip{
|
|
110
|
-
font-size:12px;font-weight:700;letter-spacing:0.3px;border-radius:999px;padding:3px 11px;
|
|
111
|
-
background:var(--bg-deep);color:var(--ink-soft);
|
|
112
|
-
}
|
|
113
|
-
.chip.live{background:rgba(22,163,74,0.13);color:var(--ok);}
|
|
114
|
-
.chip.busy{background:rgba(249,115,22,0.14);color:var(--accent-deep);}
|
|
115
|
-
.chip.bad{background:rgba(220,38,38,0.11);color:var(--err);}
|
|
116
|
-
.projMeta{font-size:13px;font-weight:600;color:var(--ink-faint);margin-top:6px;display:flex;gap:14px;flex-wrap:wrap;}
|
|
117
|
-
.projMeta code{font-family:var(--font-mono);font-size:12.5px;background:var(--bg-deep);border-radius:6px;padding:1px 6px;color:var(--accent-deep);}
|
|
118
|
-
.addr{margin-top:12px;font-size:14.5px;font-weight:600;color:var(--ink-soft);}
|
|
119
|
-
.addr a{color:var(--accent-deep);font-weight:700;text-decoration:none;}
|
|
120
|
-
.addr a:hover{text-decoration:underline;}
|
|
121
|
-
.addr .none{color:var(--ink-faint);font-style:italic;}
|
|
122
|
-
.attach{display:flex;gap:10px;margin-top:10px;flex-wrap:wrap;}
|
|
123
|
-
.attach input[type=text]{
|
|
124
|
-
flex:1;min-width:220px;font-family:var(--font-body);font-size:15px;font-weight:600;color:var(--ink);
|
|
125
|
-
background:#fff;border:2px solid var(--line);border-radius:12px;padding:9px 14px;outline:none;
|
|
126
|
-
transition:border-color .15s ease, box-shadow .15s ease;
|
|
127
|
-
}
|
|
128
|
-
.attach input[type=text]::placeholder{color:#c9b8a6;font-weight:500;}
|
|
129
|
-
.attach input[type=text]:focus{border-color:var(--accent);box-shadow:0 0 0 4px rgba(249,115,22,0.13);}
|
|
130
|
-
.projMsg{font-size:13.5px;font-weight:600;margin-top:10px;min-height:18px;color:var(--ink-faint);}
|
|
131
|
-
.projMsg.ok{color:var(--ok);}
|
|
132
|
-
.projMsg.err{color:var(--err);}
|
|
133
|
-
.actions{display:flex;gap:10px;margin-top:14px;flex-wrap:wrap;align-items:center;}
|
|
134
|
-
.confirmRow{display:flex;gap:10px;align-items:center;flex-wrap:wrap;font-size:13.5px;font-weight:700;color:var(--err);}
|
|
135
|
-
|
|
136
|
-
/* live standup watch — the /create stage line, per card */
|
|
137
|
-
.prog{margin-top:12px;border:2px solid var(--line);border-radius:14px;padding:12px 16px;background:var(--bg);}
|
|
138
|
-
.prog.isLive{border-color:rgba(22,163,74,0.4);}
|
|
139
|
-
.prog.isErr{border-color:rgba(220,38,38,0.35);}
|
|
140
|
-
.stages{list-style:none;display:flex;gap:0;margin-bottom:8px;}
|
|
141
|
-
.stages li{flex:1;display:flex;align-items:center;gap:7px;font-size:12.5px;font-weight:700;color:var(--ink-faint);}
|
|
142
|
-
.stages .sdot{width:11px;height:11px;border-radius:50%;background:var(--line);flex:0 0 auto;}
|
|
143
|
-
.stages li.done{color:var(--ink-soft);}
|
|
144
|
-
.stages li.done .sdot{background:var(--ok);}
|
|
145
|
-
.stages li.now{color:var(--accent-deep);}
|
|
146
|
-
.stages li.now .sdot{background:var(--accent);animation:pulseDot 1.4s ease-in-out infinite;}
|
|
147
|
-
@keyframes pulseDot{0%,100%{box-shadow:0 0 0 0 rgba(249,115,22,0.35)}50%{box-shadow:0 0 0 6px rgba(249,115,22,0.06)}}
|
|
148
|
-
@media (prefers-reduced-motion:reduce){.stages li.now .sdot{animation:none}}
|
|
149
|
-
.pnote{font-size:13px;font-weight:600;color:var(--ink-soft);line-height:1.5;}
|
|
150
|
-
|
|
151
|
-
/* ── page-level states ── */
|
|
152
|
-
.alert{
|
|
153
|
-
display:none;background:rgba(220,38,38,0.07);border:2px solid rgba(220,38,38,0.25);
|
|
154
|
-
color:var(--err);font-weight:600;font-size:14px;border-radius:14px;padding:12px 16px;margin-bottom:18px;
|
|
155
|
-
}
|
|
156
|
-
.alert.on{display:block;}
|
|
157
|
-
.loading{font-size:14.5px;font-weight:600;color:var(--ink-faint);padding:14px 0;}
|
|
158
|
-
.empty{padding:10px 0 4px;}
|
|
159
|
-
.empty p{font-size:15px;font-weight:600;color:var(--ink-soft);margin-bottom:16px;max-width:52ch;line-height:1.55;}
|
|
160
|
-
.finePrint{font-size:12.5px;color:var(--ink-faint);font-weight:600;margin-top:16px;line-height:1.5;}
|
|
161
|
-
|
|
162
|
-
.foot{
|
|
163
|
-
position:relative;z-index:1;text-align:center;font-size:13px;font-weight:600;color:var(--ink-faint);
|
|
164
|
-
padding:0 20px 34px;
|
|
165
|
-
}
|
|
166
|
-
@media (max-width:640px){
|
|
167
|
-
.projHead{gap:8px;}
|
|
168
|
-
.actions{flex-direction:column;align-items:stretch;}
|
|
169
|
-
}
|
|
25
|
+
/* page-specific layout. /projects is the owner's manage surface (goal
|
|
26
|
+
1000059): the durable place to find, watch, and manage your instances.
|
|
27
|
+
Same trust boundary as /create — every button only ASKS (enqueues an
|
|
28
|
+
intent); the control-plane runner does the real infra work (ADR 0111 §2). */
|
|
29
|
+
.wrap{position:relative;z-index:1;max-width:880px;margin:0 auto;padding:0 clamp(16px,4vw,40px) 80px;}
|
|
30
|
+
.card{margin-bottom:26px;}
|
|
170
31
|
</style>
|
|
171
32
|
</head>
|
|
172
33
|
<body>
|
|
173
34
|
<div class="sky" aria-hidden="true">
|
|
174
|
-
<
|
|
35
|
+
<span class="star" style="top:2%;left:12%;width:2px;height:2px;"></span>
|
|
36
|
+
<span class="star" style="top:5%;left:28%;width:1.5px;height:1.5px;animation-delay:.7s;"></span>
|
|
37
|
+
<span class="star" style="top:3%;left:47%;width:2px;height:2px;animation-delay:1.4s;"></span>
|
|
38
|
+
<span class="star" style="top:8%;left:63%;width:1.5px;height:1.5px;animation-delay:.3s;"></span>
|
|
39
|
+
<span class="star" style="top:4%;left:78%;width:2.5px;height:2.5px;animation-delay:2s;"></span>
|
|
40
|
+
<span class="star" style="top:10%;left:88%;width:1.5px;height:1.5px;animation-delay:1s;"></span>
|
|
41
|
+
<span class="star" style="top:14%;left:20%;width:1.5px;height:1.5px;animation-delay:1.8s;"></span>
|
|
42
|
+
<span class="star" style="top:22%;left:55%;width:2px;height:2px;animation-delay:.5s;"></span>
|
|
43
|
+
<span class="star" style="top:34%;left:36%;width:1.5px;height:1.5px;animation-delay:2.4s;"></span>
|
|
44
|
+
<span class="star" style="top:30%;left:78%;width:1.5px;height:1.5px;animation-delay:1.2s;"></span>
|
|
45
|
+
<span class="star" style="top:48%;left:8%;width:2px;height:2px;animation-delay:.9s;"></span>
|
|
46
|
+
<span class="star" style="top:55%;left:90%;width:1.5px;height:1.5px;animation-delay:1.6s;"></span>
|
|
47
|
+
<span class="star" style="top:68%;left:24%;width:1.5px;height:1.5px;animation-delay:2.1s;"></span>
|
|
48
|
+
<span class="star" style="top:76%;left:64%;width:2px;height:2px;animation-delay:.4s;"></span>
|
|
49
|
+
<span class="star" style="top:88%;left:42%;width:1.5px;height:1.5px;animation-delay:1.9s;"></span>
|
|
175
50
|
</div>
|
|
176
51
|
|
|
177
52
|
<header class="top">
|
|
178
53
|
<a class="mark" href="/" data-brand-product>cloud bongos</a>
|
|
179
54
|
<div class="top-right">
|
|
180
|
-
<a class="btn
|
|
55
|
+
<a class="btn-space" href="/create">+ New project</a>
|
|
56
|
+
<button type="button" class="mode-toggle" id="modeToggle" title="Switch light / dark" aria-label="Switch light or dark mode">☾</button>
|
|
181
57
|
<div class="who" id="who"></div>
|
|
182
58
|
</div>
|
|
183
59
|
</header>
|
|
184
60
|
|
|
61
|
+
<div class="hero">
|
|
62
|
+
<h1>Your projects</h1>
|
|
63
|
+
<p class="lede">The same sky as the front door — every project you own is an <b>orb</b> here. Each card says what the platform last verified, and when.</p>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
185
66
|
<div class="wrap">
|
|
186
67
|
<main class="card">
|
|
187
68
|
<!-- signed-out gate: identity-only sign-in (ADR 0143 least-privilege — unlike
|
|
188
69
|
/create, listing your own instances needs no repo scope) -->
|
|
189
70
|
<section id="gate" hidden>
|
|
190
|
-
<h1>Your projects</h1>
|
|
191
71
|
<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>
|
|
192
72
|
<a class="btn" id="signin" href="/api/bongos/auth/web/start?return=/projects">
|
|
193
73
|
<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,16 +78,14 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
198
78
|
|
|
199
79
|
<!-- signed-in: the list -->
|
|
200
80
|
<section id="mine" hidden>
|
|
201
|
-
<h1>Your projects</h1>
|
|
202
|
-
<p class="lede">Everything you own, watched live. Standups keep running even with this tab closed — come back any time.</p>
|
|
203
81
|
<div class="alert" id="pageAlert" role="alert"></div>
|
|
204
82
|
<div class="loading" id="loading">Reading your projects…</div>
|
|
205
83
|
<section class="empty" id="empty" hidden>
|
|
206
|
-
<p>
|
|
84
|
+
<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>
|
|
207
85
|
<a class="btn" href="/create">Create your first project</a>
|
|
208
86
|
</section>
|
|
209
87
|
<ul class="projects" id="projectsList"></ul>
|
|
210
|
-
<p class="finePrint">Buttons here
|
|
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>
|
|
211
89
|
</section>
|
|
212
90
|
</main>
|
|
213
91
|
</div>
|
|
@@ -221,14 +99,34 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
221
99
|
'use strict';
|
|
222
100
|
|
|
223
101
|
var API = '/api/bongos';
|
|
224
|
-
var POLL_MS = 5000;
|
|
225
|
-
var POLL_SOFT_MS = 10000;
|
|
102
|
+
var POLL_MS = 5000; /* the create.html watch cadence (in-flight cards) */
|
|
103
|
+
var POLL_SOFT_MS = 10000; /* transient failure back-off */
|
|
104
|
+
var REFRESH_MS = 60000; /* settled-card refresh: recency + heartbeat overlay
|
|
105
|
+
(direction §3 — an active card must be able to
|
|
106
|
+
self-correct, so the list re-reads once a minute) */
|
|
107
|
+
var FRESH_MS = 12 * 60 * 1000; /* "checked Xm ago" freshness ≈ 2 heartbeat sweeps */
|
|
226
108
|
|
|
227
109
|
var b = (typeof window !== 'undefined' && window.__BRANDING__) || {};
|
|
228
110
|
var productName = (b.identity && b.identity.productName) || 'Cloud Bongos';
|
|
229
111
|
document.querySelectorAll('[data-brand-product]').forEach(function (n) { n.textContent = productName.toLowerCase(); });
|
|
230
112
|
|
|
231
113
|
function $(id) { return document.getElementById(id); }
|
|
114
|
+
|
|
115
|
+
/* ── light/dark: the sky is constant, the mode governs the cards. No saved
|
|
116
|
+
choice = follow the OS; the toggle stores an explicit one. ── */
|
|
117
|
+
function resolvedMode() {
|
|
118
|
+
var m = document.documentElement.getAttribute('data-mode');
|
|
119
|
+
if (m === 'light' || m === 'dark') return m;
|
|
120
|
+
return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
|
|
121
|
+
}
|
|
122
|
+
function paintModeToggle() { $('modeToggle').textContent = resolvedMode() === 'dark' ? '☀' : '☾'; }
|
|
123
|
+
$('modeToggle').addEventListener('click', function () {
|
|
124
|
+
var next = resolvedMode() === 'dark' ? 'light' : 'dark';
|
|
125
|
+
document.documentElement.setAttribute('data-mode', next);
|
|
126
|
+
try { localStorage.setItem('cb-color-mode', next); } catch (e) {}
|
|
127
|
+
paintModeToggle();
|
|
128
|
+
});
|
|
129
|
+
paintModeToggle();
|
|
232
130
|
function esc(s) {
|
|
233
131
|
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
234
132
|
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
@@ -240,6 +138,27 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
240
138
|
var timers = {}; /* id → watch timer */
|
|
241
139
|
var lastMsg = {}; /* id → {text, cls} — survives a card re-render */
|
|
242
140
|
|
|
141
|
+
/* ── time helpers: every liveness claim carries recency (direction §3) ── */
|
|
142
|
+
|
|
143
|
+
function relTime(ts) {
|
|
144
|
+
var d = Date.now() - ts;
|
|
145
|
+
if (d < 90 * 1000) return 'just now';
|
|
146
|
+
if (d < 60 * 60 * 1000) return Math.round(d / 60000) + 'm ago';
|
|
147
|
+
if (d < 24 * 60 * 60 * 1000) return Math.round(d / 3600000) + 'h ago';
|
|
148
|
+
return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
149
|
+
}
|
|
150
|
+
function shortDate(iso) {
|
|
151
|
+
return iso ? new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '';
|
|
152
|
+
}
|
|
153
|
+
/* "since 14:02" today, "since Aug 7" earlier — for the unreachable chip */
|
|
154
|
+
function clockOrDate(ts) {
|
|
155
|
+
var d = new Date(ts);
|
|
156
|
+
if (Date.now() - ts < 24 * 60 * 60 * 1000 && d.getDate() === new Date().getDate()) {
|
|
157
|
+
return d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
|
158
|
+
}
|
|
159
|
+
return shortDate(d.toISOString());
|
|
160
|
+
}
|
|
161
|
+
|
|
243
162
|
/* ── API plumbing ─────────────────────────────────────────────────────── */
|
|
244
163
|
|
|
245
164
|
/* Parse the error envelope truthfully: { error: { code, message, details } }.
|
|
@@ -255,16 +174,30 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
255
174
|
return 'Your session expired — <a href="' + API + '/auth/web/start?return=/projects">sign in again</a>.';
|
|
256
175
|
}
|
|
257
176
|
|
|
258
|
-
/* ── status vocabulary
|
|
177
|
+
/* ── status vocabulary (direction §4): the chip only asserts what the
|
|
178
|
+
platform verified, and says when. `last_verified_*` is the heartbeat
|
|
179
|
+
overlay — absent (pre-migration or never probed) reads as unverified. ── */
|
|
180
|
+
|
|
181
|
+
function chipMarkup(cls, label, rec) {
|
|
182
|
+
return '<span class="chip ' + cls + '"><span class="cdot"></span>' + esc(label) +
|
|
183
|
+
(rec ? ' <span class="rec">· ' + esc(rec) + '</span>' : '') + '</span>';
|
|
184
|
+
}
|
|
185
|
+
function chipFor(inst) {
|
|
186
|
+
var s = inst.status;
|
|
187
|
+
if (s === 'active') {
|
|
188
|
+
var at = inst.last_verified_at ? new Date(inst.last_verified_at).getTime() : 0;
|
|
189
|
+
if (inst.last_verified_up === false) return chipMarkup('down', 'unreachable', at ? 'since ' + clockOrDate(at) : '');
|
|
190
|
+
if (inst.last_verified_up && at && (Date.now() - at) <= FRESH_MS) return chipMarkup('live', 'live', 'checked ' + relTime(at));
|
|
191
|
+
return chipMarkup('maybe', 'live?', at ? 'last seen ' + relTime(at) : 'not checked yet');
|
|
192
|
+
}
|
|
193
|
+
if (s === 'requested') return chipMarkup('busy', 'queued');
|
|
194
|
+
if (s === 'provisioning') return chipMarkup('busy', 'standing up');
|
|
195
|
+
if (s === 'error') return chipMarkup('bad', 'snagged', inst.updated_at ? 'since ' + shortDate(inst.updated_at) : '');
|
|
196
|
+
if (s === 'tearing_down') return chipMarkup('busy', 'tearing down');
|
|
197
|
+
if (s === 'torn_down') return chipMarkup('calm', 'torn down', shortDate(inst.torn_down_at));
|
|
198
|
+
return chipMarkup('', inst.status);
|
|
199
|
+
}
|
|
259
200
|
|
|
260
|
-
var CHIP = {
|
|
261
|
-
requested: { cls: 'busy', label: 'queued' },
|
|
262
|
-
provisioning: { cls: 'busy', label: 'standing up' },
|
|
263
|
-
active: { cls: 'live', label: 'live' },
|
|
264
|
-
error: { cls: 'bad', label: 'snagged' },
|
|
265
|
-
tearing_down: { cls: 'busy', label: 'tearing down' },
|
|
266
|
-
torn_down: { cls: '', label: 'torn down' }
|
|
267
|
-
};
|
|
268
201
|
/* client mirror of provisioning.TEARDOWN_FROM minus tearing_down (already
|
|
269
202
|
going); no button either while a teardown intent is already queued */
|
|
270
203
|
function canTeardown(inst) {
|
|
@@ -273,55 +206,123 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
273
206
|
}
|
|
274
207
|
/* client mirror of provisioning.DOMAIN_ATTACH_FROM */
|
|
275
208
|
function canAttachDomain(inst) { return !inst.domain && (inst.status === 'requested' || inst.status === 'error' || inst.status === 'active'); }
|
|
209
|
+
/* re-enqueue doors: Try again on snagged, Bring it back on torn down —
|
|
210
|
+
both re-POST the slug (the server's PROVISION_ENQUEUE_FROM gate) */
|
|
211
|
+
function canRetry(inst) { return inst.status === 'error' && !inst.open_intent; }
|
|
212
|
+
function canRevive(inst) { return inst.status === 'torn_down' && !inst.open_intent; }
|
|
276
213
|
/* keep watching while the platform is doing something to this instance */
|
|
277
214
|
function shouldWatch(inst) {
|
|
278
|
-
if (inst.status === 'requested' || inst.status === 'provisioning' || inst.status === 'tearing_down'
|
|
215
|
+
if (inst.status === 'requested' || inst.status === 'provisioning' || inst.status === 'tearing_down') return true;
|
|
279
216
|
return !!inst.open_intent; /* e.g. a queued teardown or domain-attach on an active instance */
|
|
280
217
|
}
|
|
281
218
|
|
|
282
|
-
/* ── the
|
|
219
|
+
/* ── the orb: the gate's project sphere, glow keyed to verification ────── */
|
|
220
|
+
|
|
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
|
+
function orbFor(inst) {
|
|
224
|
+
var h = 0, slug = String(inst.slug || '');
|
|
225
|
+
for (var i = 0; i < slug.length; i++) h = (h * 31 + slug.charCodeAt(i)) >>> 0;
|
|
226
|
+
var k = h % ORB_ACCENTS.length;
|
|
227
|
+
var cls = 'orb';
|
|
228
|
+
if (inst.status === 'torn_down' || inst.status === 'tearing_down') cls += ' ghost';
|
|
229
|
+
else if (inst.status === 'error' || (inst.status === 'active' && inst.last_verified_up === false)) cls += ' ashen';
|
|
230
|
+
else if (inst.status === 'active' && !(inst.last_verified_up && inst.last_verified_at && (Date.now() - new Date(inst.last_verified_at).getTime()) <= FRESH_MS)) cls += ' dim';
|
|
231
|
+
var style = '--acc:' + ORB_ACCENTS[k] + ';--glow:' + ORB_GLOWS[k];
|
|
232
|
+
if (inst.status === 'active' && inst.domain) {
|
|
233
|
+
return '<a class="' + cls + '" style="' + style + '" href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener" title="Open ' + esc(inst.slug) + '" aria-label="Open ' + esc(inst.slug) + '"></a>';
|
|
234
|
+
}
|
|
235
|
+
return '<span class="' + cls + '" style="' + style + '" aria-hidden="true"></span>';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ── progress strips: each flow speaks its own stages (direction §4).
|
|
239
|
+
The strip is chosen by the open intent + status, never one-size; a
|
|
240
|
+
torn-down card with a queued revive shows the REVIVE strip (F22). ── */
|
|
283
241
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
242
|
+
var FLOWS = {
|
|
243
|
+
standup: { names: ['Request filed', 'Standing it up', 'Live'], cls: '' },
|
|
244
|
+
revive: { names: ['Revive queued', 'Standing it up', 'Back live'], cls: '' },
|
|
245
|
+
teardown: { names: ['Teardown queued', 'Winding down', 'Torn down'], cls: 'flowDown' },
|
|
246
|
+
address: { names: ['Address queued', 'Wiring the address', 'Answering there'], cls: '' }
|
|
247
|
+
};
|
|
248
|
+
function stripMarkup(flow, stageNow, stagesDone, note) {
|
|
249
|
+
var f = FLOWS[flow];
|
|
250
|
+
return '<div class="strip ' + f.cls + '" role="status" aria-live="polite">' +
|
|
251
|
+
'<ul class="stages">' + f.names.map(function (n, i) {
|
|
288
252
|
var c = stagesDone > i ? 'done' : (stageNow === i ? 'now' : '');
|
|
289
253
|
return '<li class="' + c + '"><span class="sdot"></span><span class="slbl">' + n + '</span></li>';
|
|
290
254
|
}).join('') + '</ul>' +
|
|
291
255
|
'<p class="pnote">' + note + '</p></div>';
|
|
292
256
|
}
|
|
293
|
-
function
|
|
257
|
+
function stripFor(inst) {
|
|
294
258
|
var s = inst.status;
|
|
295
|
-
|
|
296
|
-
if (s === '
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
259
|
+
var revive = !!inst.torn_down_at; /* stood down before this standup → it's a revive */
|
|
260
|
+
if (s === 'requested') {
|
|
261
|
+
return stripMarkup(revive ? 'revive' : 'standup', 1, 1,
|
|
262
|
+
'Waiting for a runner to pick it up. This card updates the moment it starts.');
|
|
263
|
+
}
|
|
264
|
+
if (s === 'provisioning') {
|
|
265
|
+
return stripMarkup(revive ? 'revive' : 'standup', 1, 1,
|
|
266
|
+
revive ? 'Bringing it back on the newest platform version — nothing needed from you.'
|
|
267
|
+
: 'Raising the server, database, and address — nothing needed from you.');
|
|
268
|
+
}
|
|
269
|
+
if (s === 'tearing_down') {
|
|
270
|
+
return stripMarkup('teardown', 1, 1, 'Taking the server offline. The repository stays yours; the database is kept.');
|
|
271
|
+
}
|
|
272
|
+
if (inst.open_intent) {
|
|
300
273
|
var a = inst.open_intent.action;
|
|
301
|
-
|
|
302
|
-
return
|
|
274
|
+
if (a === 'teardown') return stripMarkup('teardown', 0, 0, 'A teardown is queued — this card updates when it starts.');
|
|
275
|
+
if (a === 'domain-attach') return stripMarkup('address', 1, 1, 'Setting up DNS and the secure connection' + (inst.domain ? ' for <strong>' + esc(inst.domain) + '</strong>' : '') + '.');
|
|
276
|
+
if (a === 'provision') return stripMarkup(s === 'torn_down' ? 'revive' : 'standup', 0, 0, (s === 'torn_down' ? 'A revive is queued' : 'A standup is queued') + ' — this card updates when it starts.');
|
|
277
|
+
return stripMarkup('standup', 2, 2, 'A ' + esc(a) + ' run is queued — this card updates when it lands.');
|
|
303
278
|
}
|
|
304
279
|
return '';
|
|
305
280
|
}
|
|
306
281
|
|
|
282
|
+
/* ── snagged cards: plain sentence first, raw detail folded away, and the
|
|
283
|
+
retry truth — nothing silently "keeps retrying" (direction §5, F19) ── */
|
|
284
|
+
|
|
285
|
+
function errCategory(note) {
|
|
286
|
+
var n = String(note || '');
|
|
287
|
+
if (/repository not found/i.test(n)) return 'We couldn’t push the starter code to the repository — it wasn’t found on GitHub.';
|
|
288
|
+
if (/mkdir|no such file or directory|read-only file system/i.test(n)) return 'The server couldn’t prepare a workspace for the project.';
|
|
289
|
+
if (/(push|clone|github).*permission|permission.*(push|clone|repo)/i.test(n)) return 'We couldn’t reach the repository — the account that set it up doesn’t have permission.';
|
|
290
|
+
if (/permission denied/i.test(n)) return 'The server hit a permission wall while setting up.';
|
|
291
|
+
if (/clone/i.test(n)) return 'We couldn’t fetch the repository from GitHub.';
|
|
292
|
+
if (/econnrefused|healthz|timed? ?out/i.test(n)) return 'The server didn’t come up.';
|
|
293
|
+
if (/dns|domain|caddy|tls|certificate/i.test(n)) return 'The address didn’t wire up.';
|
|
294
|
+
return 'The standup stopped partway.';
|
|
295
|
+
}
|
|
296
|
+
function errMarkup(inst) {
|
|
297
|
+
var when = inst.updated_at ? ' on ' + esc(shortDate(inst.updated_at)) : '';
|
|
298
|
+
var html = errCategory(inst.error_note) +
|
|
299
|
+
' We stopped trying' + when + ' — nothing more will happen until you try again.';
|
|
300
|
+
if (inst.error_note) {
|
|
301
|
+
html += '<details><summary>Technical details</summary><pre>' + esc(inst.error_note) + '</pre></details>';
|
|
302
|
+
}
|
|
303
|
+
return html;
|
|
304
|
+
}
|
|
305
|
+
|
|
307
306
|
/* ── rendering ───────────────────────────────────────────────────────── */
|
|
308
307
|
|
|
309
|
-
|
|
310
|
-
var c = CHIP[inst.status] || { cls: '', label: inst.status };
|
|
311
|
-
return '<span class="chip ' + c.cls + '">' + esc(c.label) + '</span>';
|
|
312
|
-
}
|
|
308
|
+
var SHAPE_LABEL = { standalone: 'its own server', 'co-tenant': 'shared server', dedicated: 'its own server' };
|
|
313
309
|
|
|
314
310
|
function addressRow(inst) {
|
|
315
311
|
if (inst.domain) {
|
|
316
312
|
if (inst.status === 'active') {
|
|
317
|
-
|
|
313
|
+
/* "answering" is a VERIFIED claim (direction §3 / F16): only the
|
|
314
|
+
heartbeat earns it — never the runner finishing its script */
|
|
315
|
+
var link = '<a href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">' + esc(inst.domain) + '</a>';
|
|
316
|
+
if (inst.last_verified_up) return 'Answering at ' + link;
|
|
317
|
+
if (inst.last_verified_up === false) return 'Not answering at ' + link + ' right now.';
|
|
318
|
+
return 'Its address is ' + link + ' — confirming it answers.';
|
|
318
319
|
}
|
|
319
320
|
return 'Address: <code>' + esc(inst.domain) + '</code> — it answers there once the instance is live.';
|
|
320
321
|
}
|
|
321
322
|
/* a torn-down instance has no address to speak of — the card's own line says so */
|
|
322
323
|
if (inst.status === 'torn_down' || inst.status === 'tearing_down') return '';
|
|
323
324
|
/* the HONEST no-address state: sign-in on the instance needs a real domain */
|
|
324
|
-
return '<span class="none">No address yet — sign-in on
|
|
325
|
+
return '<span class="none">No address yet</span> — sign-in on this project can’t be set up until it has one.';
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
function renderCard(inst) {
|
|
@@ -334,29 +335,37 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
334
335
|
li.className = 'proj' + (gone ? ' gone' : '');
|
|
335
336
|
var meta = [];
|
|
336
337
|
if (inst.target_ref) meta.push('repo <code>' + esc(inst.target_ref) + '</code>');
|
|
337
|
-
if (inst.hosting_shape) meta.push(esc(inst.hosting_shape));
|
|
338
|
+
if (inst.hosting_shape) meta.push(esc(SHAPE_LABEL[inst.hosting_shape] || inst.hosting_shape));
|
|
338
339
|
if (inst.onboard_mode === 'adopt') meta.push('adopted repo');
|
|
339
|
-
|
|
340
|
+
if (inst.status === 'active' && inst.torn_down_at) meta.push('back live since ' + esc(shortDate(inst.provisioned_at || inst.torn_down_at)));
|
|
341
|
+
var body =
|
|
340
342
|
'<div class="projHead"><span class="projName">' + esc(inst.slug) + '</span>' + chipFor(inst) + '</div>' +
|
|
341
343
|
(meta.length ? '<div class="projMeta">' + meta.join('<span aria-hidden="true">·</span>') + '</div>' : '');
|
|
342
344
|
var addr = addressRow(inst);
|
|
343
|
-
if (addr)
|
|
345
|
+
if (addr) body += '<div class="addr">' + addr + '</div>';
|
|
344
346
|
if (canAttachDomain(inst)) {
|
|
345
|
-
|
|
347
|
+
body += '<div class="attach">' +
|
|
346
348
|
'<input type="text" id="dom-' + iid + '" placeholder="myproject.example.com" autocomplete="off" spellcheck="false" maxlength="253">' +
|
|
347
|
-
'<button type="button" class="btn small" data-attach="' + iid + '">
|
|
349
|
+
'<button type="button" class="btn small" data-attach="' + iid + '">Give it an address</button></div>';
|
|
348
350
|
}
|
|
349
|
-
|
|
351
|
+
if (inst.status === 'error' && !inst.open_intent) {
|
|
352
|
+
/* once a retry is queued the strip tells the story — the stale error
|
|
353
|
+
paragraph would just contradict it (owner walk, 2026-08-12) */
|
|
354
|
+
body += '<div class="projMsg err">' + errMarkup(inst) + '</div>';
|
|
355
|
+
}
|
|
356
|
+
body += stripFor(inst);
|
|
350
357
|
if (gone) {
|
|
351
|
-
|
|
358
|
+
body += '<div class="projMsg">Torn down' + (inst.torn_down_at ? ' ' + esc(shortDate(inst.torn_down_at)) : '') +
|
|
359
|
+
'. The repository is still yours, and its database is kept — bringing it back takes one click. Or <a href="/create">start another</a>.</div>';
|
|
352
360
|
}
|
|
353
361
|
var lm = lastMsg[inst.id];
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
362
|
+
body += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '">' + (lm ? lm.text : '') + '</div>';
|
|
363
|
+
var acts = [];
|
|
364
|
+
if (canRetry(inst)) acts.push('<button type="button" class="btn small" data-retry="' + iid + '">Try again</button>');
|
|
365
|
+
if (canRevive(inst)) acts.push('<button type="button" class="btn small" data-revive="' + iid + '">Bring it back</button>');
|
|
366
|
+
if (canTeardown(inst)) acts.push('<button type="button" class="btn small danger" data-teardown="' + iid + '">Tear it down</button>');
|
|
367
|
+
if (acts.length) body += '<div class="actions" id="act-' + iid + '">' + acts.join('') + '</div>';
|
|
368
|
+
li.innerHTML = orbFor(inst) + '<div class="projBody">' + body + '</div>';
|
|
360
369
|
}
|
|
361
370
|
|
|
362
371
|
function render(instances) {
|
|
@@ -392,13 +401,25 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
392
401
|
function stopWatch(id) { if (timers[id]) { clearTimeout(timers[id]); delete timers[id]; } }
|
|
393
402
|
/* What the card RENDERS from — re-render only when this changes, so a quiet
|
|
394
403
|
poll tick never clobbers in-flight UI (an open teardown confirm, a
|
|
395
|
-
half-typed domain, a fresh action message).
|
|
404
|
+
half-typed domain, a fresh action message). The heartbeat overlay is part
|
|
405
|
+
of the signature: recency is bucketed per-minute so "checked 2m ago"
|
|
406
|
+
stays current without thrashing. */
|
|
396
407
|
function sig(inst) {
|
|
408
|
+
var at = inst.last_verified_at ? new Date(inst.last_verified_at).getTime() : 0;
|
|
397
409
|
return JSON.stringify([inst.status, inst.domain, inst.error_note,
|
|
398
|
-
inst.open_intent && inst.open_intent.action, inst.open_intent && inst.open_intent.state
|
|
410
|
+
inst.open_intent && inst.open_intent.action, inst.open_intent && inst.open_intent.state,
|
|
411
|
+
inst.last_verified_up, at ? Math.floor((Date.now() - at) / 60000) : -1]);
|
|
399
412
|
}
|
|
400
413
|
function takeUpdate(id, inst) {
|
|
401
|
-
var
|
|
414
|
+
var prev = byId[id];
|
|
415
|
+
var changed = !prev || sig(prev) !== sig(inst);
|
|
416
|
+
/* an action message ("Queued again…") describes the moment it was sent —
|
|
417
|
+
once the lifecycle moves past it (status flip, intent consumed) it
|
|
418
|
+
becomes a stale claim; clear it (owner walk, 2026-08-12) */
|
|
419
|
+
if (prev && (prev.status !== inst.status ||
|
|
420
|
+
((prev.open_intent && prev.open_intent.action) !== (inst.open_intent && inst.open_intent.action)))) {
|
|
421
|
+
lastMsg[id] = null;
|
|
422
|
+
}
|
|
402
423
|
byId[id] = inst;
|
|
403
424
|
if (changed) renderCard(inst);
|
|
404
425
|
}
|
|
@@ -440,14 +461,36 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
440
461
|
.catch(function () { if (byId[id] && shouldWatch(byId[id])) watch(id); });
|
|
441
462
|
}
|
|
442
463
|
|
|
443
|
-
/* ──
|
|
464
|
+
/* ── settled-card refresh: the heartbeat overlay must reach the page.
|
|
465
|
+
A card at 'active' has no fast poll; once a minute (and on tab return)
|
|
466
|
+
the whole list re-reads, so "live · checked 2m ago" stays true and an
|
|
467
|
+
instance that died shows up as unreachable without a reload. ── */
|
|
468
|
+
|
|
469
|
+
function refreshList() {
|
|
470
|
+
fetch(API + '/provisioning/instances', { credentials: 'same-origin' })
|
|
471
|
+
.then(function (res) { if (!res.ok) throw new Error('soft'); return res.json(); })
|
|
472
|
+
.then(function (data) {
|
|
473
|
+
var instances = data.instances || [];
|
|
474
|
+
var known = Object.keys(byId).length;
|
|
475
|
+
var sameSet = instances.length === known && instances.every(function (i) { return byId[i.id]; });
|
|
476
|
+
if (!sameSet) { render(instances); return; }
|
|
477
|
+
instances.forEach(function (inst) { takeUpdate(inst.id, inst); });
|
|
478
|
+
})
|
|
479
|
+
.catch(function () { /* transient — next tick will try again */ });
|
|
480
|
+
}
|
|
481
|
+
setInterval(function () { if (!document.hidden && me) refreshList(); }, REFRESH_MS);
|
|
482
|
+
document.addEventListener('visibilitychange', function () { if (!document.hidden && me) refreshList(); });
|
|
483
|
+
|
|
484
|
+
/* ── teardown: ask, confirm (the truth, itemized — direction §5.4), then
|
|
485
|
+
SAY exactly what the server said ── */
|
|
444
486
|
|
|
445
487
|
function askTeardown(id) {
|
|
446
488
|
var act = $('act-' + id);
|
|
447
489
|
if (!act) return;
|
|
448
|
-
act.innerHTML = '<div class="confirmRow"
|
|
490
|
+
act.innerHTML = '<div class="confirmRow">' +
|
|
491
|
+
'<span class="plain">Tear it down? The server goes away and the address stops answering. Your repository is untouched. The database is kept, so bringing it back later restores its memory.</span>' +
|
|
449
492
|
'<button type="button" class="btn small danger" data-teardown-yes="' + esc(id) + '">Yes, tear it down</button>' +
|
|
450
|
-
'<button type="button" class="linklike" data-teardown-no="' + esc(id) + '">Keep it</button></div>';
|
|
493
|
+
'<button type="button" class="linklike" data-teardown-no="' + esc(id) + '">Keep it running</button></div>';
|
|
451
494
|
}
|
|
452
495
|
function unAskTeardown(id) { var inst = byId[id]; if (inst) renderCard(inst); }
|
|
453
496
|
function doTeardown(id) {
|
|
@@ -486,6 +529,46 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
486
529
|
});
|
|
487
530
|
}
|
|
488
531
|
|
|
532
|
+
/* ── re-enqueue: Try again (snagged) and Bring it back (torn down) both
|
|
533
|
+
re-POST the slug — the server's PROVISION_ENQUEUE_FROM gate decides.
|
|
534
|
+
Same truth-telling as every other action: say what the server said. ── */
|
|
535
|
+
|
|
536
|
+
function reProvision(id, verb) {
|
|
537
|
+
var inst = byId[id];
|
|
538
|
+
if (!inst) return;
|
|
539
|
+
var act = $('act-' + id);
|
|
540
|
+
if (act) act.innerHTML = '<span class="projMsg">Asking…</span>';
|
|
541
|
+
var body = { slug: inst.slug, hosting_shape: inst.hosting_shape || 'standalone' };
|
|
542
|
+
if (inst.onboard_mode) body.onboard_mode = inst.onboard_mode;
|
|
543
|
+
if (inst.target_ref) body.target_ref = inst.target_ref;
|
|
544
|
+
if (inst.domain) body.domain = inst.domain;
|
|
545
|
+
fetch(API + '/provisioning/instances', {
|
|
546
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
547
|
+
body: JSON.stringify(body)
|
|
548
|
+
}).then(function (res) {
|
|
549
|
+
if (res.status === 401) { msg(id, signInAgain(), 'err'); return null; }
|
|
550
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
551
|
+
if (res.ok) {
|
|
552
|
+
msg(id, esc(data.message || (verb === 'revive' ? 'Revive queued — bringing it back.' : 'Queued again — a runner picks it up from here.')), 'ok');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
var e = apiError(data, res.status);
|
|
556
|
+
if (e.code === 'intent_conflict') {
|
|
557
|
+
msg(id, esc(e.message || 'A previous run is still open — retry once it settles.'), 'err');
|
|
558
|
+
} else if (e.code === 'rate_limited') {
|
|
559
|
+
msg(id, esc(e.message || 'Too many requests right now — give it a minute and try again.'), 'err');
|
|
560
|
+
} else {
|
|
561
|
+
msg(id, esc(e.message || 'The request could not be queued — try again in a moment.'), 'err');
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
}).catch(function () {
|
|
565
|
+
msg(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
566
|
+
}).then(function () {
|
|
567
|
+
if (byId[id]) renderCard(byId[id]);
|
|
568
|
+
watchOnce(id);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
489
572
|
/* ── domain attach: same truth-telling for every refusal (task 1002692) ── */
|
|
490
573
|
|
|
491
574
|
function doAttach(id) {
|
|
@@ -534,6 +617,8 @@ h1{font-family:var(--font-display);font-weight:700;font-size:clamp(24px,3.4vw,32
|
|
|
534
617
|
if (t.hasAttribute('data-teardown-yes')) return doTeardown(t.getAttribute('data-teardown-yes'));
|
|
535
618
|
if (t.hasAttribute('data-teardown-no')) return unAskTeardown(t.getAttribute('data-teardown-no'));
|
|
536
619
|
if (t.hasAttribute('data-attach')) return doAttach(t.getAttribute('data-attach'));
|
|
620
|
+
if (t.hasAttribute('data-retry')) return reProvision(t.getAttribute('data-retry'), 'retry');
|
|
621
|
+
if (t.hasAttribute('data-revive')) return reProvision(t.getAttribute('data-revive'), 'revive');
|
|
537
622
|
});
|
|
538
623
|
|
|
539
624
|
/* ── boot: who am I → my instances ────────────────────────────────────── */
|