@galda/cli 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/index.html +129 -23
- package/app/theme.css +4 -0
- package/engine/analytics-client.mjs +36 -0
- package/engine/server.mjs +70 -1
- package/package.json +1 -1
package/app/index.html
CHANGED
|
@@ -3,7 +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>
|
|
6
|
+
<title>Galda</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%233B6BFF'/%3E%3Cstop offset='1' stop-color='%230E2AA8'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='16' cy='16' r='11' fill='none' stroke='url(%23g)' stroke-width='5'/%3E%3C/svg%3E">
|
|
7
8
|
<!-- brand fonts FINAL (v45 §1): the 3 adopted faces self-host in /fonts.css — the old
|
|
8
9
|
Google Fonts Inter/JetBrains request was removed (offline-safe, no stray families) -->
|
|
9
10
|
<link rel="stylesheet" href="/theme.css">
|
|
@@ -1129,8 +1130,8 @@
|
|
|
1129
1130
|
#fsRoot .fsproj.on i{background:var(--green)}
|
|
1130
1131
|
/* running project = solid saturated green dot with a breathing glow — never dims to
|
|
1131
1132
|
grey, independent of selection (HANDOFF-v45 §11, flagship .proj.run port) */
|
|
1132
|
-
#fsRoot .fsproj.run i{background
|
|
1133
|
-
@keyframes fsrundot{0%,100%{box-shadow:0 0 0 0
|
|
1133
|
+
#fsRoot .fsproj.run i{background:var(--green);box-shadow:0 0 0 0 color-mix(in srgb,var(--green) 55%,transparent);animation:fsrundot 2s ease-in-out infinite}
|
|
1134
|
+
@keyframes fsrundot{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--green) 50%,transparent)}50%{box-shadow:0 0 5px 1.5px color-mix(in srgb,var(--green) 35%,transparent)}}
|
|
1134
1135
|
@media (prefers-reduced-motion:reduce){#fsRoot .fsproj.run i{animation:none}}
|
|
1135
1136
|
#fsRoot .fsproj .pname{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
1136
1137
|
#fsRoot .fsproj .num{margin-left:auto;font-size:10px;color:var(--ink3);flex:0 0 auto}
|
|
@@ -1168,7 +1169,7 @@
|
|
|
1168
1169
|
.fsctxmenu button svg{width:14px;height:14px;flex:0 0 14px;fill:none;stroke:currentColor;stroke-width:1.8}
|
|
1169
1170
|
.fsctxmenu .ctxsep{height:1px;margin:4px 6px;background:var(--hair)}
|
|
1170
1171
|
.fsctxmenu .swix{opacity:.5;margin-right:9px;font:600 10px/1 var(--mono);flex:0 0 auto}
|
|
1171
|
-
.fsctxmenu .swrun{margin-left:8px;width:6px;height:6px;border-radius:50%;background
|
|
1172
|
+
.fsctxmenu .swrun{margin-left:8px;width:6px;height:6px;border-radius:50%;background:var(--green);flex:0 0 auto}
|
|
1172
1173
|
#fsRoot .ghostbtn{height:26px;padding:0 8px;border:0;border-radius:7px;background:transparent;color:var(--ink3);font:inherit;font-size:11.5px;text-align:left;cursor:pointer;white-space:nowrap}
|
|
1173
1174
|
#fsRoot .ghostbtn:hover{background:var(--hover);color:var(--ink)}
|
|
1174
1175
|
#fsRoot .morebtn{display:grid;place-items:center;width:26px;padding:0}
|
|
@@ -1213,7 +1214,7 @@
|
|
|
1213
1214
|
color:var(--ink3);background:transparent;border:0;padding:0;margin-left:10px;cursor:pointer}
|
|
1214
1215
|
#fsRoot .chd .runsw[hidden]{display:none}
|
|
1215
1216
|
#fsRoot .chd .runsw:hover{color:var(--ink)}
|
|
1216
|
-
#fsRoot .chd .runsw i{width:5px;height:5px;border-radius:50%;background
|
|
1217
|
+
#fsRoot .chd .runsw i{width:5px;height:5px;border-radius:50%;background:var(--green);flex:0 0 5px;animation:fsrundot 2s ease-in-out infinite}
|
|
1217
1218
|
#fsRoot .chd .runsw svg{width:9px;height:9px;flex:0 0 9px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
1218
1219
|
#fsRoot .runmenu{display:none;flex-direction:column;gap:1px;background:var(--panel);border-radius:10px;
|
|
1219
1220
|
box-shadow:0 4px 14px rgba(20,23,31,.12),0 0 0 1px var(--hair);padding:4px;position:absolute;top:22px;right:0;min-width:210px;z-index:6}
|
|
@@ -1222,7 +1223,7 @@
|
|
|
1222
1223
|
color:var(--ink2);font:inherit;font-size:11.5px;text-align:left;cursor:pointer;white-space:nowrap;overflow:hidden}
|
|
1223
1224
|
#fsRoot .runmenu .rmrow:hover{background:var(--hover);color:var(--ink)}
|
|
1224
1225
|
#fsRoot .runmenu .rmrow.on{color:var(--ink);font-weight:600}
|
|
1225
|
-
#fsRoot .runmenu .rmrow i{width:6px;height:6px;border-radius:50%;background
|
|
1226
|
+
#fsRoot .runmenu .rmrow i{width:6px;height:6px;border-radius:50%;background:var(--green);flex:0 0 6px;animation:fsrundot 2s ease-in-out infinite}
|
|
1226
1227
|
#fsRoot .runmenu .rmrow .rmnm{overflow:hidden;text-overflow:ellipsis;flex:1 1 auto}
|
|
1227
1228
|
#fsRoot .runmenu .rmrow .rmpct{flex:0 0 auto;font:600 10px/1 var(--mono);color:var(--ink3)}
|
|
1228
1229
|
/* "Now doing" — Underline pattern (HANDOFF-v45 §10 confirmed default, flagship
|
|
@@ -1232,7 +1233,7 @@
|
|
|
1232
1233
|
max-width:var(--fscenterw,760px);width:100%;margin:-4px auto 14px;position:relative;z-index:1}
|
|
1233
1234
|
#fsRoot .nowdoing[hidden]{display:none}
|
|
1234
1235
|
#fsRoot .nowdoing .nd-lb{font:600 8.5px/1 var(--mono);letter-spacing:.12em;color:var(--ink3);text-transform:uppercase}
|
|
1235
|
-
#fsRoot .nowdoing .nd-run{width:9px;height:9px;flex:0 0 9px;border-radius:50%;background
|
|
1236
|
+
#fsRoot .nowdoing .nd-run{width:9px;height:9px;flex:0 0 9px;border-radius:50%;background:var(--green);animation:fsrundot 2s ease-in-out infinite}
|
|
1236
1237
|
#fsRoot .nowdoing .nd-no{font-size:12.5px;font-weight:600;color:var(--ink3);font-variant-numeric:tabular-nums}
|
|
1237
1238
|
#fsRoot .nowdoing .nd-nm{font-size:14px;font-weight:600;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:0 1 auto}
|
|
1238
1239
|
#fsRoot .nowdoing .nd-pct{margin-left:auto;font:600 11px/1 var(--mono);color:var(--ink2);font-variant-numeric:tabular-nums;flex:0 0 auto}
|
|
@@ -1673,6 +1674,14 @@
|
|
|
1673
1674
|
radial-gradient(80% 72% at 84% 7%,var(--lgA,rgba(216,228,255,.56)),transparent 62%),
|
|
1674
1675
|
radial-gradient(120% 108% at 95% -5%,var(--lgB,rgba(150,170,220,.42)),transparent 74%),#07080a;
|
|
1675
1676
|
background-attachment:fixed}
|
|
1677
|
+
/* light OFF (H17, flagship f9c75ad): kill the dominant top-right glow + every light layer →
|
|
1678
|
+
pure flat black (LP #111112). Overrides both the --glowc wash (any layout) and the flagship
|
|
1679
|
+
radial stack. Accent goes LP green #3DDC97 via var(--green) (set in theme.css). */
|
|
1680
|
+
body[data-theme="midnight"][data-light="off"],
|
|
1681
|
+
body[data-theme="midnight"][data-light="off"][data-layout="flagship"]{background:#111112 !important}
|
|
1682
|
+
/* send button on flat black: dark round + white arrow (was invisible white-on-white) */
|
|
1683
|
+
body[data-theme="midnight"][data-light="off"] .send{background:#17181c !important;box-shadow:inset 0 0 0 1px rgba(255,255,255,.16) !important}
|
|
1684
|
+
body[data-theme="midnight"][data-light="off"] .send svg{stroke:#fff !important}
|
|
1676
1685
|
body[data-theme="midnight"] #fsRoot .fscard{background:rgba(10,11,16,.66);-webkit-backdrop-filter:blur(22px) saturate(1.3);backdrop-filter:blur(22px) saturate(1.3);
|
|
1677
1686
|
box-shadow:0 1px 0 rgba(255,255,255,.05) inset,0 0 0 1px var(--hair),0 24px 60px -24px rgba(0,0,0,.72)}
|
|
1678
1687
|
|
|
@@ -1908,6 +1917,9 @@
|
|
|
1908
1917
|
#fsClawdHero{display:none;position:absolute;left:50%;top:38%;transform:translate(-50%,-50%);text-align:center;z-index:1;pointer-events:none}
|
|
1909
1918
|
#fsClawdHero b{display:block;margin-top:14px;font:600 16px/1.3 'Schibsted Grotesk',-apple-system,system-ui,sans-serif;letter-spacing:-.012em;color:var(--ink)} /* hero wordmark = Schibsted (v45 §1) */
|
|
1910
1919
|
#fsClawdHero span{display:block;margin-top:5px;font:500 10px/1.4 var(--mono);letter-spacing:.08em;color:var(--ink3)}
|
|
1920
|
+
/* Welcome P1 (H17 item 4, flagship f9c75ad): grounded hop + clean-cut eyes on settle + our CSS confetti.
|
|
1921
|
+
Two pixel frames (neutral black-eyes / laughing), no arm-raise, no official confetti. reduce-motion=static. */
|
|
1922
|
+
#fsClawdHero .cstage{position:relative;width:170px;height:132px;display:flex;align-items:flex-end;justify-content:center;margin:0 auto 2px}#fsClawdHero .cw{position:relative;width:150px;transform-origin:50% 100%;will-change:transform;animation:chHop 2.4s linear infinite}#fsClawdHero .cw img{position:absolute;left:0;bottom:0;width:100%;display:block;image-rendering:pixelated}#fsClawdHero .cw .chNeu{animation:chNeu 2.4s linear infinite}#fsClawdHero .cw .chLau{opacity:0;animation:chLau 2.4s linear infinite}#fsClawdHero .chConf{position:absolute;left:50%;top:34px;width:0;height:0;pointer-events:none;z-index:3}#fsClawdHero .chConf i{position:absolute;width:4px;height:7px;border-radius:1.5px;opacity:0;transform:translate(-50%,-50%);animation:chPop 2.4s cubic-bezier(.2,.7,.3,1) infinite}@keyframes chHop{0%,52%{transform:translateY(0) scale(1,1)}55%{transform:translateY(0) scale(1.08,.85)}58%{transform:translateY(-16px) scale(.95,1.14)}61%{transform:translateY(-27px) scale(.98,1.05)}64%{transform:translateY(-31px) scale(1,1)}67%{transform:translateY(-26px) scale(.98,1.05)}70%{transform:translateY(-14px) scale(.95,1.12)}73%{transform:translateY(0) scale(1.1,.82)}77%{transform:translateY(-4px) scale(.99,1.02)}80%,100%{transform:translateY(0) scale(1,1)}}@keyframes chLau{0%,53.8%{opacity:0}54%,85.8%{opacity:1}86%,100%{opacity:0}}@keyframes chNeu{0%,53.8%{opacity:1}54%,85.8%{opacity:0}86%,100%{opacity:1}}@keyframes chPop{0%,54%{opacity:0;transform:translate(0,6px) rotate(0) scale(.5)}60%{opacity:1;transform:translate(calc(var(--dx)*.5),calc(var(--dy)*.5)) rotate(calc(var(--rot)*.5)) scale(1)}72%{opacity:1;transform:translate(var(--dx),var(--dy)) rotate(var(--rot))}100%{opacity:0;transform:translate(calc(var(--dx)*1.1),calc(var(--dy) + 44px)) rotate(calc(var(--rot)*1.4)) scale(.9)}}@media (prefers-reduced-motion:reduce){#fsClawdHero .cw,#fsClawdHero .cw .chNeu,#fsClawdHero .cw .chLau{animation:none}#fsClawdHero .cw .chLau{opacity:0}#fsClawdHero .chConf{display:none}}
|
|
1911
1923
|
#fsRoot.clawdhero #fsClawdHero{display:block}
|
|
1912
1924
|
#fsRoot .cc-who{display:flex;align-items:center;gap:9px;padding:2px 0 6px}
|
|
1913
1925
|
#fsRoot .cc-who .cs-n{font-size:12px;font-weight:600;color:var(--ink)}
|
|
@@ -2037,7 +2049,7 @@
|
|
|
2037
2049
|
<canvas id="fx"></canvas>
|
|
2038
2050
|
<div class="top">
|
|
2039
2051
|
<div class="mark"></div>
|
|
2040
|
-
<div class="brand">
|
|
2052
|
+
<div class="brand">Galda</div>
|
|
2041
2053
|
<div class="spacer"></div>
|
|
2042
2054
|
<!-- Feedback 7: layout switcher — flip the arrangement live. Increment 1:
|
|
2043
2055
|
現行 / タスク中央 / 3列カンバン (CSS reflow). 中央フロー・1列 = increment 2. -->
|
|
@@ -5187,7 +5199,7 @@ async function deleteProject(p){
|
|
|
5187
5199
|
if (!p) return;
|
|
5188
5200
|
if (!(await confirmDelete({
|
|
5189
5201
|
title: 'Delete this project?',
|
|
5190
|
-
body: 'This removes the project from
|
|
5202
|
+
body: 'This removes the project from Galda. This cannot be undone.',
|
|
5191
5203
|
preview: p.name,
|
|
5192
5204
|
}))) return;
|
|
5193
5205
|
try {
|
|
@@ -5310,7 +5322,8 @@ function applyFsBand(){
|
|
|
5310
5322
|
// Clawd HERO (§4.5 初期状態): big Clawd + "Claude Code / CONNECTED" in the
|
|
5311
5323
|
// implementing column. Visibility is state-driven (renderFsFeed toggles
|
|
5312
5324
|
// #fsRoot.clawdhero when the column has zero activity — no timers).
|
|
5313
|
-
$('fsClawdHero').innerHTML = CLAWD(88) + '<b>Claude Code</b><span>CONNECTED · WAITING FOR GOALS</span>';
|
|
5325
|
+
$('fsClawdHero').innerHTML = '<div class="cstage"><div class="chConf"></div><div class="cw"><img class="chNeu" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARgAAAETCAYAAADtSRlPAAAMdElEQVR4nO3dPYwk6VkH8Keqq2f2E4uzzydzCToCH8JCIiAxgY1EDMIBSByS5cQkthCCHCFBgGTJSBA5cGQnRDYJws6xxElA6C9ZBEjGt+e1rZvdnZmurkJvdfds9+581HTvTL9d/fsF+0xrd7tm3u5+3v+89RUBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMBDFtr8BLvaPn3+nfWvypPu6Lo1UzqpmVn80vh9f/MrXfa7mqsUX5Oe1hw/izTpiFNOYxiiqqoy6btQMx2E6OYnxqIr32/vbfttkRYPJWD1to4g2Ttv0aBr1ZNpFTjW/cRhHxGRaR0iaKwxHxqpREdHWcZBKRHQP1SzHISkLH6cXGZHME0x60zZt082U03a2aKbmNw5Jep1YpcFknmDSmzY1mW3P0KoEsw4NJmMSzO4lmLKcP6CjwWRMgtmd5FRHOft1trGHepkGkzlrMLtRq2gkmHNoMJmzBpNHQpFg1qPBZMwazO4lGFZpMBmzBrP9ZOI4mM04kndnEkzZayYdF2WU0UQ1X2vsU7uZt+j3/LtYmzbi8IWft5qP63J98e/r6L+d9L9n43jLb5LMaTC5J5jJ/DiYdjajTi+pqbk8Oj6Jnx09jdNmGlVZRd3Ul9eTp1Ed3otfPjyI1x/eufT5d7U+OT6N/338uPs5rxyPpXF5642PxsGo7LWdFHUcyfsyDWYgCWYcTYzLMr79kw/iq9/4VjwqUr6/+piMURQxjTb+5pNvx+996nfjwZOfdh+WpmdiyrWm8Ug/xy8evhHf/M634svvfi+ix5gUZRlt08TrEfH5P/nDeOeNu905RleNxyLBlCPHwSyzBpOxdNBW371I3XEYo6WXM32Q0gcquaSm5nL2ODW1+VtiCHt1LnTJeKTmsnhcFqNoy9nX9iKtR4PJWDpoq+9xMN1ejOlsL8az9MfybN2zpmvOpOfp/vsA9uq8pM84LI1b006jSAs4PcbDcTDn02AGmGDupj/atov7nR5JZi8STJ9xWBq3lGAWJJj1aDADSzDVvfurcT/pm2S6RTkJ5mzc0tDMf0Xqm2BYpcEMLMHUT2eX2JRgNk8wy64a/+71cj2Yl2gwA1yD6UgwryTBnA3nFePfvV4SzEs0mIEcyXveXiRrMLeXYBZnU7PKiGROgtmNBGMv0vk0mMxJMNvbi7TMXqT1aDADOZvaGkweCYZVGswOrMEkN3Uk73JNNw9zHMxqglkcaNc3wbQ+Uis0mIwtz4jXPpI3ueaRvMneHweThqNpujFMR/IumrYEsx4nO2bqL3774+3Dg4Oo67L70C9myovO5q3bMqqyiDfvjOOPfu0j8d6DD/fazsHxpKtvvvZaV9NM3Gd7udduPIoy7p4+id9448PxmY+/1XvsT++M46NHP41PlHWcTNK5SVW0bXPl9tL9q8ajtnvtvvzu954fHr3HDMI1ffev/rSdtm2M0omCN1CXI/bJZPbhb+cvVJ+a3uzjahxF1yTKK+vCk3Ico5OnvbezKzWpilHcGRdRt0VURdvV9PMnadyXHy+ksUm3ha3b6dkHpc/20rbS3TirxQV3lvR5Pa7zuqWf5e0vfS3rz7AEc03pzTitm3jWTm9oBt78eaOezB9Pe9aIUTvJInncRG1jGkens8fH13wdkuslp9n/ezY57++nr6x2S0PjdMPavFmDuabZDOhOi7d9drQaK+MwP0UqexrMNc1itjstbvts6X2vzfN1+axpMNckwUgUOSSqUoIZJgkmjxl832sjwQyXNZjtz+D7XksJZni+8Ae/37b1xBpMBjP4vtdGghmmohpLMBnM4PteSwlmeP7pm//Svaz2Im1/Bt/32kgww2QvUh4z+L7XUoIZJnuR8pjB9702EswwSTB5zOD7XksJZpgkmDxm8H2vjQQzTBJMHjP4vtdSghkmCSaPGXzfayPBDJMEk8cMvu+1lGCGSYLJYwbf99pIMMMkweQxg+97LSWYYZJg8pjB9702EsxwOZt6+zP4vtdSghkeZ1Nvf+ZWQ4IZ8smOzqbOYwbf91pKMMPlbGpJYttJqrEGM0z2IuUxg+97LSWYYUo3R0t38EsOb7hac9jdpHTT748m3S64yD/GuKvANU3qiKN6dmvVm6on3c3C8pgp1euNQ8zri6/r6bR55e+T40n+DWZHgtZ++vpf/ln79uQX3ZrP5Ip7I6vbHYeUKFKy/c/il+Kz//AVn6s5CSZjk2nR3Yg+2lqSyDxRpdepaZsoy/xTxW3SYDI2Hj1/s257TUG9es2lLMpoGuFlmQaTsXrxS33kM1OrF6+9pATDKg0mY9ViapRgsk9QdZRdgmGVEcnY8owoOeSdoKzBnE+D2RHbnqHVfgnGGswqDWZHbHuGVvslmOVfa9Fgsrb8O70EsSMJxkLvimr1ITmxBrM7yWncJZhtv2Py41ekjEkwO5hgHAezQoPJmONgtp9M7EXajAaTseXDzrc9Q6sSzDo0mIwtx+1tz9Cq42DWocFkzJG8u5OcrMGcT4PJmL1Iu5OcHMl7Pg0mY/YibT+ZSDCb0WAyJsFsP5lIMJvRYDImwWw/mUgwm9FgMuY4mO0nEwlmMxpMxhwHs/1kIsFsRoPJmONgtp9MJJjNaDAZcxzM9pOJBLMZDSZj9iJtP5lIMJvRYDJmL9L2k4kEsxkNJmP2Im0/mUgwm3HBqdzXYObX/V7MpOlNf1lNFz26Mx5HW0+iqGa1rEbR1NNza123MapGcTKZRFWMom6nvbaz+PCNilEclG1MmyZGZdnVy7a3Tl08b6pPp0233b7j0R3GX4yiiNnPedV4LGobZdR1041Hn+3Vbdnd2dH1YFZpMANZg0lv8uTHx5P4nx/8KJrxg5hEE+NLQuri71P99dcexK986EO9Z+wklf/68ftx9OxZtMXNvpWKto4Hd+/Gb33sI2cf+j7f5/TwXrz36L34wfvz5ymfRtvcu7Au3I/H8atvfixev3MYddv0vqJduXSzPDSYnVuDuSy53B2V8Z2jJv7uX/9j3m76+/tP/2Z773c+FQ8/eNR9WCZR9koGn/vnf7v2tjbx33/+Tlv0HI/0cxwd3I93v//D+Ot//+61v8+//ewft5+5F1E2Eafp+S65R7gEcz5rMANJMN09rDdQlaNZnT9PnwSTksQ2tLcwHsu6e053TeTiJONs6vNpMAPZi5SuR7KJukm/dtRnz3PV9pKUYG7b9IU1kV7jscH3OZnW8+2m18P1YK5LgxlYginX/DDtSoJJi8q3mWAWTb7P9rqks3SZUzSYwSWYZs0PfUowXR1ignkFTd4V7dYjwQzkOBgJRoLJkQYzkLOpN00wCxLMc9VIgtmUBrMjbjrBHB6Ou2oN5rl6ag1mUxpMxm5zDebkZNJVCeY5CWZzGkzGbnMv0mKRV4LZLMEsX2IDDSZrt5lgFiSYDRPM0qSABpO120wwCxLMhmswS5MCGkzWJJjtHgcjwWxOu82YBLP9BFONZ3vXJJj1aDAZk2AySDCpy1iDWZsGkzEJZvsJJl18KpFg1qPBZEyCeZkEs1s0mIxJMC+TYHaLBpMxCWb3EszyCapoMFmTYHYwwbgezAoJJmO3mWAWF5xyJO/SmIwi6snsHC3Xg1mPBpOx20wwzqY+nyvabUaDydhtJpgnz467KsGsckW7zWgwGbMGk8EajGvybkSDyVi3VyK9REXVK8G0ZXHjZ1Mn6f5Axbxuay/S6fwOj4vv58W6GLeNtxejs+dZXIP4ojoeVe7s+AINJnPpdqTR1mcfnsWH/LCIKOc3ATv7dxveVaBqZs+TGtuLz794vPh+un/3Cu891NfheNylmHtV1d1GpJqPy+LnX9R0E7r0/Y2b2W1H1pVui5t+3rSddPvZi2pqLonrwaxy69iMPf7gKB7Haff1SX0ah1UZJ3XT1XQTsLYcRdGcdm/y9PjewUH87Gj276/r6OQ0nj75II6fHXfPt9hOquV4FM1kurLd43Yad7ZwbaX/O3ra1bRzJ52HeFFN32/6vn9SNfHz0/WazLPJaTx6UpyN/2IczqtFM+3G7fFZrgIAAAAAAAAAAAAAAAAgevp/2dsONs8JxOUAAAAASUVORK5CYII=" alt=""><img class="chLau" src="data:image/png;base64,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" alt=""></div></div>' + '<b>Claude Code</b><span>CONNECTED · WAITING FOR GOALS</span>';
|
|
5326
|
+
(function(){var conf=document.querySelector('#fsClawdHero .chConf');if(!conf)return;if(window.matchMedia&&matchMedia('(prefers-reduced-motion: reduce)').matches)return;var C=["#FF6B6B","#F86A5C","#FFD166","#3DDC97","#8FB0FF","#C77DFF","#ffffff"],N=16;for(var k=0;k<N;k++){var i=document.createElement('i');var ang=(-172+Math.random()*164)*Math.PI/180;var dist=50+Math.random()*94;var dx=Math.cos(ang)*dist,dy=Math.sin(ang)*dist-12;i.style.setProperty('--dx',dx.toFixed(1)+'px');i.style.setProperty('--dy',dy.toFixed(1)+'px');i.style.setProperty('--rot',(Math.random()*360-180|0)+'deg');i.style.animationDelay=(-(Math.random()*0.04)).toFixed(3)+'s';i.style.background=C[k%C.length];conf.appendChild(i);}})(); // Welcome P1 confetti (H17 item 4)
|
|
5314
5327
|
// fold toggles = パネルアイコン Fill v2 (HANDOFF-v45 §4) — faithful port of flagship
|
|
5315
5328
|
// tgSvg/tgUpdate at the adopted value (__tgv v=1). OPEN = pane side filled 14% +
|
|
5316
5329
|
// divider; CLOSED = outward arrow pointing in the opening direction. No rotation.
|
|
@@ -5671,7 +5684,90 @@ function connectSSE(){
|
|
|
5671
5684
|
};
|
|
5672
5685
|
}
|
|
5673
5686
|
|
|
5687
|
+
// Signed in, but this device's engine isn't running yet. We're the REAL app
|
|
5688
|
+
// (served by the relay), just flagged disconnected: render the board, then gate
|
|
5689
|
+
// the composer so the first attempt to type explains the one-time device setup
|
|
5690
|
+
// (npx @galda/cli). Poll `/`; the moment the engine connects the relay proxies
|
|
5691
|
+
// the live app (flag gone) → reload into it. (Masa 2026-07-15: one product, not a
|
|
5692
|
+
// separate look-alike page.)
|
|
5693
|
+
function bootDisconnected(info){
|
|
5694
|
+
const email = (info && info.email) || '';
|
|
5695
|
+
document.body.classList.add('disconnected');
|
|
5696
|
+
document.body.setAttribute('data-layout', localStorage.getItem('layout') || 'flagship');
|
|
5697
|
+
applyTheme(); applyCust();
|
|
5698
|
+
// seed a minimal board so it renders like the real empty state
|
|
5699
|
+
state.projects = [{ id: '__onboard', name: 'Your project' }];
|
|
5700
|
+
state.active = '__onboard';
|
|
5701
|
+
state.billing = { licensed: false, email };
|
|
5702
|
+
try { render(); } catch (e) { console.warn('disconnected render', e); }
|
|
5703
|
+
const heroSpan = document.querySelector('#fsClawdHero span');
|
|
5704
|
+
if (heroSpan) heroSpan.textContent = 'CONNECT YOUR MACHINE TO BEGIN';
|
|
5705
|
+
|
|
5706
|
+
const style = document.createElement('style');
|
|
5707
|
+
style.textContent = `
|
|
5708
|
+
#connectGate{position:fixed;inset:0;z-index:1000;display:none;align-items:center;justify-content:center;padding:22px;
|
|
5709
|
+
background:rgba(4,4,6,.72);-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px)}
|
|
5710
|
+
#connectGate.on{display:flex}
|
|
5711
|
+
#connectGate .cg-card{position:relative;width:100%;max-width:440px;background:var(--panel,#0d0d0f);border:1px solid var(--hair2,rgba(255,255,255,.12));
|
|
5712
|
+
border-radius:18px;padding:30px 28px;text-align:center;box-shadow:0 30px 80px -30px #000}
|
|
5713
|
+
#connectGate h2{font:700 20px/1.3 var(--ui,system-ui);letter-spacing:-.02em;margin:0 0 8px;color:var(--ink,#e9e7e2)}
|
|
5714
|
+
#connectGate p{font-size:13.5px;line-height:1.7;color:var(--ink2,#a3a097);margin:0 0 18px}
|
|
5715
|
+
#connectGate .cg-cmd{display:flex;align-items:center;justify-content:space-between;gap:12px;background:#0a0a0b;
|
|
5716
|
+
border:1px solid rgba(255,255,255,.14);border-radius:12px;padding:12px 14px;margin:0 0 16px}
|
|
5717
|
+
#connectGate .cg-cmd code{font:500 14px/1 var(--mono,monospace);color:#e9e7e2}
|
|
5718
|
+
#connectGate .cg-cmd button{font:500 11px/1 var(--mono,monospace);color:#a3a097;background:transparent;
|
|
5719
|
+
border:1px solid rgba(255,255,255,.18);border-radius:7px;padding:6px 9px;cursor:pointer}
|
|
5720
|
+
#connectGate .cg-cmd button:hover{color:#fff}
|
|
5721
|
+
#connectGate .cg-who{font-size:12px;color:var(--ink3,#6b6862);margin:0 0 4px}
|
|
5722
|
+
#connectGate .cg-who b{color:var(--ink2,#a3a097)}
|
|
5723
|
+
#connectGate .cg-switch{font-size:12px;color:var(--ink3,#6b6862);margin:14px 0 0}
|
|
5724
|
+
#connectGate .cg-switch a{color:#8fb0ff;text-decoration:none}
|
|
5725
|
+
#connectGate .cg-x{position:absolute;top:14px;right:16px;width:28px;height:28px;border:0;background:transparent;color:var(--ink3,#6b6862);font-size:20px;line-height:1;cursor:pointer}
|
|
5726
|
+
#connectGate .cg-wait{display:inline-flex;align-items:center;gap:7px;font:500 10.5px/1 var(--mono,monospace);letter-spacing:.08em;color:var(--ink3,#6b6862);margin-top:16px}
|
|
5727
|
+
#connectGate .cg-wait i{width:6px;height:6px;border-radius:50%;background:var(--green,#3DDC97);animation:cgpulse 1.8s ease-in-out infinite}
|
|
5728
|
+
@keyframes cgpulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--green,#3DDC97) 50%,transparent)}50%{box-shadow:0 0 6px 2px color-mix(in srgb,var(--green,#3DDC97) 35%,transparent)}}
|
|
5729
|
+
body.disconnected .composer{cursor:pointer}`;
|
|
5730
|
+
document.head.appendChild(style);
|
|
5731
|
+
|
|
5732
|
+
const ov = document.createElement('div');
|
|
5733
|
+
ov.id = 'connectGate';
|
|
5734
|
+
ov.innerHTML = '<div class="cg-card"><button class="cg-x" aria-label="Close">×</button>'
|
|
5735
|
+
+ '<h2>Connect this device to start</h2>'
|
|
5736
|
+
+ '<p>Galda runs on your own machine. Run this where you use Claude Code or Codex — this board comes alive the moment it connects, no refresh needed:</p>'
|
|
5737
|
+
+ '<div class="cg-cmd"><code>npx @galda/cli</code><button class="cg-copy">Copy</button></div>'
|
|
5738
|
+
+ '<p class="cg-who">Signed in as <b></b></p>'
|
|
5739
|
+
+ '<p class="cg-switch">Wrong account? <a href="/logout">Switch account →</a></p>'
|
|
5740
|
+
+ '<div class="cg-wait"><i></i>WAITING FOR THIS DEVICE</div></div>';
|
|
5741
|
+
document.body.appendChild(ov);
|
|
5742
|
+
ov.querySelector('.cg-who b').textContent = email || 'your account';
|
|
5743
|
+
const open = () => ov.classList.add('on');
|
|
5744
|
+
const close = () => ov.classList.remove('on');
|
|
5745
|
+
ov.querySelector('.cg-x').onclick = close;
|
|
5746
|
+
ov.addEventListener('mousedown', (e) => { if (e.target === ov) close(); });
|
|
5747
|
+
ov.querySelector('.cg-copy').onclick = function(){ try { navigator.clipboard.writeText('npx @galda/cli'); } catch {} this.textContent = 'Copied'; setTimeout(() => { this.textContent = 'Copy'; }, 1400); };
|
|
5748
|
+
|
|
5749
|
+
// gate the composer: any attempt to type a goal explains the setup first
|
|
5750
|
+
const input = document.getElementById('input');
|
|
5751
|
+
const send = document.getElementById('send');
|
|
5752
|
+
const gate = (e) => { if (e) e.preventDefault(); open(); };
|
|
5753
|
+
if (input) { input.readOnly = true; input.addEventListener('focus', gate); input.addEventListener('mousedown', gate); input.addEventListener('keydown', gate); }
|
|
5754
|
+
if (send) send.addEventListener('click', gate);
|
|
5755
|
+
const cw = document.getElementById('composerWrap'); if (cw) cw.addEventListener('mousedown', gate);
|
|
5756
|
+
|
|
5757
|
+
// come alive automatically: when the relay stops flagging disconnected (the
|
|
5758
|
+
// engine connected → its live app is proxied), reload into the real board.
|
|
5759
|
+
setInterval(() => {
|
|
5760
|
+
fetch('/', { cache: 'no-store', credentials: 'same-origin' }).then((r) => r.text()).then((t) => {
|
|
5761
|
+
if (t.indexOf('__GALDA_DISCONNECTED') === -1) location.reload();
|
|
5762
|
+
}).catch(() => {});
|
|
5763
|
+
}, 4000);
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5674
5766
|
async function boot(){
|
|
5767
|
+
// Served by the relay when you're signed in but this device's engine isn't
|
|
5768
|
+
// connected yet (window.__GALDA_DISCONNECTED). Show the real board, but gate the
|
|
5769
|
+
// composer with a "connect this device" explainer on the first attempt to type.
|
|
5770
|
+
if (window.__GALDA_DISCONNECTED) return bootDisconnected(window.__GALDA_DISCONNECTED);
|
|
5675
5771
|
$('input').value = localStorage.getItem('draft') ?? '';
|
|
5676
5772
|
await refresh();
|
|
5677
5773
|
flushOutbox();
|
|
@@ -6934,7 +7030,7 @@ const cust = {
|
|
|
6934
7030
|
ring: Number(localStorage.getItem('ringPal') ?? 1), // prism send-ring palette (0-16) — default = 2nd 全分光 (§6.5 確定)
|
|
6935
7031
|
tban: Number(localStorage.getItem('tbAnim') ?? 5), // TB縁アニメ (0-9 + 10 none) — default = 5 パルス (§6.5 確定)
|
|
6936
7032
|
tbc: Number(localStorage.getItem('tbColor') ?? 1), // TB縁アニメの色 (RINGS 0-16) — default = 2nd 全分光 (§6.5 確定)
|
|
6937
|
-
glow: Number(localStorage.getItem('glowLight') ??
|
|
7033
|
+
glow: Number(localStorage.getItem('glowLight') ?? 6), // Midnight LIGHT preset (0-5, or 6="Off" flat black — Midnight default)
|
|
6938
7034
|
hue: localStorage.getItem('lightHue') != null ? Number(localStorage.getItem('lightHue')) : null, // Midnight free hue (0-359, null = preset)
|
|
6939
7035
|
bg3: Number(localStorage.getItem('bg3') ?? 0), // Banff BACKDROP (0 Lake / 1 Peak / 2-6 dark scenes / -1 upload)
|
|
6940
7036
|
cine: Number(localStorage.getItem('cineText') ?? 3), // Cinema TEXT (0-4) — default 3 Below (v3 §6.5)
|
|
@@ -7194,14 +7290,21 @@ function applyCust(){
|
|
|
7194
7290
|
// Midnight light: preset (Moon default) or the free hue slider (__light/__lighthue port)
|
|
7195
7291
|
const setLg = (la, lb, lc, w) => { const s = document.body.style;
|
|
7196
7292
|
s.setProperty('--lgA', la); s.setProperty('--lgB', lb); s.setProperty('--lgC', lc); s.setProperty('--lgW', w); };
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
document.body.
|
|
7293
|
+
// light OFF (glow index 6) = pure flat black (LP #111112) via [data-light="off"] — a 3rd state
|
|
7294
|
+
// beyond preset/hue (flagship __light('off')). CSS handles the flat black + green accent.
|
|
7295
|
+
if (cust.hue == null && cust.glow === 6) {
|
|
7296
|
+
document.body.setAttribute('data-light', 'off');
|
|
7201
7297
|
} else {
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
7298
|
+
document.body.removeAttribute('data-light');
|
|
7299
|
+
if (cust.hue != null) {
|
|
7300
|
+
const h = ((cust.hue | 0) % 360 + 360) % 360;
|
|
7301
|
+
setLg(`hsla(${h},90%,79%,.60)`, `hsla(${(h + 18) % 360},78%,56%,.48)`, `hsla(${h},95%,87%,.40)`, '.44');
|
|
7302
|
+
document.body.style.setProperty('--glowc', `hsl(${h},90%,79%)`); // legacy old-layout wash
|
|
7303
|
+
} else {
|
|
7304
|
+
const L = LIGHTS[((cust.glow % 6) + 6) % 6];
|
|
7305
|
+
setLg(L[1], L[2], L[3], L[4]);
|
|
7306
|
+
document.body.style.setProperty('--glowc', LIGHT_SW[((cust.glow % 6) + 6) % 6]);
|
|
7307
|
+
}
|
|
7205
7308
|
}
|
|
7206
7309
|
// Banff backdrop (v3 §6.5): photo / dark scene / uploaded image (bg3 = -1 → dataURL)
|
|
7207
7310
|
const tbg = document.getElementById('themebg');
|
|
@@ -7244,11 +7347,13 @@ function gearHistHTML(){
|
|
|
7244
7347
|
for (const tk of state.tasks) if (tk.goalId === g.id && tk.finishedAt) t = Math.max(t, Date.parse(tk.finishedAt) || 0);
|
|
7245
7348
|
return t;
|
|
7246
7349
|
};
|
|
7247
|
-
|
|
7350
|
+
// History = everything you've sent, newest first (not only settled goals —
|
|
7351
|
+
// that felt empty). Each row gets a status glyph; unknown states fall back.
|
|
7352
|
+
const MARK = { done: ['ok', '✓'], reverted: ['no', '↩'], skipped: ['ar', '—'], failed: ['no', '✕'], interrupted: ['ar', '⏸'], review: ['rv', '●'], running: ['run', '…'], planning: ['run', '…'], stacked: ['ar', '·'], needsInput: ['rv', '?'] };
|
|
7248
7353
|
const evs = (state.goals || [])
|
|
7249
|
-
.filter((g) => g.projectId === state.active
|
|
7354
|
+
.filter((g) => g.projectId === state.active)
|
|
7250
7355
|
.map((g) => ({ g, ts: goalTs(g) })).sort((a, b) => b.ts - a.ts);
|
|
7251
|
-
if (!evs.length) return '<div class="mcpnote">No
|
|
7356
|
+
if (!evs.length) return '<div class="mcpnote">No history yet — everything you send shows up here.</div>';
|
|
7252
7357
|
const todayK = new Date().toDateString(), yestK = new Date(Date.now() - 864e5).toDateString();
|
|
7253
7358
|
const cap = (ts) => { const k = new Date(ts).toDateString();
|
|
7254
7359
|
return k === todayK ? 'TODAY' : k === yestK ? 'YESTERDAY'
|
|
@@ -7257,7 +7362,7 @@ function gearHistHTML(){
|
|
|
7257
7362
|
for (const { g, ts } of evs) {
|
|
7258
7363
|
const c = cap(ts);
|
|
7259
7364
|
if (c !== last) { out += `<h4>${c}</h4>`; last = c; }
|
|
7260
|
-
const [cls, m] = MARK[g.status];
|
|
7365
|
+
const [cls, m] = MARK[g.status] || ['ar', '·'];
|
|
7261
7366
|
const hhmm = new Date(ts).toTimeString().slice(0, 5);
|
|
7262
7367
|
// 1-line ellipsis + a dedicated More/Less button (HANDOFF-v45 §9 — replaces row-tap;
|
|
7263
7368
|
// the button itself is hidden post-render for rows that don't overflow, see wiring
|
|
@@ -7386,7 +7491,8 @@ function buildGearPop(){
|
|
|
7386
7491
|
`<button class="sw${cust.bg3 === i ? ' on' : ''}" data-gbg3="${i}" title="${n}" style="background:${i < 2 ? bg3Image(i) + ' center/cover' : bg3Image(i)}"></button>`).join('')
|
|
7387
7492
|
+ `<button class="optchip${cust.bg3 === -1 ? ' on' : ''}" data-gbg3up="1" title="Upload an image">+</button>`;
|
|
7388
7493
|
// Midnight LIGHT: 6 presets (Moon default) + free HUE slider
|
|
7389
|
-
const
|
|
7494
|
+
const offSw = `<button class="sw${cust.hue == null && cust.glow === 6 ? ' on' : ''}" data-gglow="6" title="Off" style="background:#111112"></button>`; // flat-black "Off" — Midnight default, leftmost
|
|
7495
|
+
const lightSw = offSw + LIGHTS.map((L, i) => `<button class="sw${cust.hue == null && cust.glow === i ? ' on' : ''}" data-gglow="${i}" title="${L[0]}" style="background:${LIGHT_SW[i]}"></button>`).join('');
|
|
7390
7496
|
// Cinema SCENE (reuses the band presets + upload) + TEXT modes
|
|
7391
7497
|
const sceneNames = ['Lake', 'Peak', 'Blue', 'Violet', 'Dusk', 'Jade', 'Midnight', 'Gold'];
|
|
7392
7498
|
const sceneSw = FS_BANDS.map((v, i) =>
|
package/app/theme.css
CHANGED
|
@@ -71,6 +71,10 @@ body[data-theme="dark"],body[data-theme="midnight"],body[data-theme="cinema"]{
|
|
|
71
71
|
/* flagship surface tokens — flagship internal T5/T6 (dark) values */
|
|
72
72
|
--hover:rgba(255,255,255,.06); --hover-s:rgba(255,255,255,.05); --surf:rgba(255,255,255,.04);
|
|
73
73
|
}
|
|
74
|
+
/* Midnight · light OFF = pure flat black (LP #111112). data-light="off" (H17 port, flagship f9c75ad 2026-07-14).
|
|
75
|
+
Black-screen accent = LP green #3DDC97 — unifies PR pill / ✓ / token / LIVE / DOING / progress. Both --green
|
|
76
|
+
(var(--green) users) and --green-rgb (rgba(var(--green-rgb),.x) / color-mix users) are re-pointed. */
|
|
77
|
+
body[data-theme="midnight"][data-light="off"]{--green:#3DDC97;--green-rgb:61,220,151}
|
|
74
78
|
/* Theme 4 — Glass: ARCHIVED (v3 §6.5 row 4 / §7 decision log 2026-07-07).
|
|
75
79
|
Not selectable from any UI; the token block + WebGL pipeline stay dormant/intact
|
|
76
80
|
so the archive can be revived from the e574 loop without re-porting.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// analytics-client.mjs — fire-and-forget client for Phase 1 usage analytics
|
|
2
|
+
// (docs/DATA-RETENTION-PLAN.md). Posts a single Tier-A event to the billing-api
|
|
3
|
+
// /events endpoint.
|
|
4
|
+
//
|
|
5
|
+
// PRIVACY: NEVER pass prompt / task text / file paths. Only an event type, a
|
|
6
|
+
// user identifier (the endpoint hashes it into a pseudonymous id), and
|
|
7
|
+
// allow-listed non-sensitive meta (project_hash / plan / status / model /
|
|
8
|
+
// source — see workers/billing-api/src/analytics.mjs sanitizeMeta). Failures
|
|
9
|
+
// are swallowed so analytics can never break a task run or block the caller.
|
|
10
|
+
//
|
|
11
|
+
// Hook points (wiring lives in engine/relay, kept out of this module):
|
|
12
|
+
// signup → relay Google callback, right after the session is minted
|
|
13
|
+
// task_created → engine, when a task/goal is accepted
|
|
14
|
+
// task_completed → engine, task.status = 'done'
|
|
15
|
+
// task_failed → engine, task.status = 'failed'
|
|
16
|
+
// task_interrupted → engine, task.status = 'interrupted'
|
|
17
|
+
// review_approved / review_dismissed → on the review action
|
|
18
|
+
|
|
19
|
+
const BILLING_API_URL = (process.env.MANAGER_BILLING_API_URL ?? '').replace(/\/$/, '');
|
|
20
|
+
|
|
21
|
+
export function analyticsEnabled() {
|
|
22
|
+
return Boolean(BILLING_API_URL);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// type: one of the EVENT_TYPES; uid: account email/id (hashed server-side);
|
|
26
|
+
// meta: optional allow-listed non-sensitive fields only.
|
|
27
|
+
export function emitEvent(type, uid, meta) {
|
|
28
|
+
if (!BILLING_API_URL) return; // analytics off when unconfigured — no-op
|
|
29
|
+
try {
|
|
30
|
+
fetch(`${BILLING_API_URL}/events`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'content-type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({ type, uid, meta }),
|
|
34
|
+
}).catch(() => {}); // fire-and-forget: never surface network errors
|
|
35
|
+
} catch { /* never throw into the caller */ }
|
|
36
|
+
}
|
package/engine/server.mjs
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
import { createServer } from 'node:http';
|
|
15
15
|
import { spawn, execFile, spawnSync } from 'node:child_process';
|
|
16
16
|
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
17
|
-
import {
|
|
17
|
+
import { emitEvent } from './analytics-client.mjs';
|
|
18
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
18
19
|
import { resolve, dirname, join, basename } from 'node:path';
|
|
19
20
|
import { homedir } from 'node:os';
|
|
20
21
|
import { fileURLToPath } from 'node:url';
|
|
@@ -36,6 +37,10 @@ const PORT = Number(process.env.MANAGER_PORT ?? 4400);
|
|
|
36
37
|
const keyFile = join(DATA_DIR, 'secret.key');
|
|
37
38
|
if (!existsSync(keyFile)) writeFileSync(keyFile, randomBytes(18).toString('base64url'));
|
|
38
39
|
const ACCESS_KEY = readFileSync(keyFile, 'utf8').trim();
|
|
40
|
+
// A stable, anonymous per-install id for usage analytics — a one-way hash of the
|
|
41
|
+
// access key so the raw credential NEVER leaves the machine (the billing Worker
|
|
42
|
+
// hashes this again into its pseudonymous id). Not the access key itself.
|
|
43
|
+
const ANALYTICS_UID = createHash('sha256').update('galda-analytics:' + ACCESS_KEY).digest('hex').slice(0, 24);
|
|
39
44
|
// Per-worker run cap. Env-configurable so it can be tuned operationally and the
|
|
40
45
|
// timeout→'interrupted' path is testable (a test sets a short value + a slow
|
|
41
46
|
// fake worker instead of waiting 10 real minutes).
|
|
@@ -541,6 +546,56 @@ function saveTask(t) {
|
|
|
541
546
|
appendFileSync(logFile, JSON.stringify({ ...persist, kind: 'task' }) + '\n');
|
|
542
547
|
const goal = goals.find((g) => g.id === t.goalId);
|
|
543
548
|
if (goal) sendGoalEvent({ ev: 'task', task: persist }, goal); else send({ ev: 'task', task: persist });
|
|
549
|
+
emitTaskAnalytics(t);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Phase 1 usage analytics (docs/DATA-RETENTION-PLAN.md). Fire-and-forget, no
|
|
553
|
+
// content: a task's creation and its terminal outcome are counted once each,
|
|
554
|
+
// keyed by ANALYTICS_UID (the hashed, non-credential install id). saveTask runs
|
|
555
|
+
// on every status change, so a per-(id,event) Set dedups repeat saves.
|
|
556
|
+
const _analyticsEmitted = new Set();
|
|
557
|
+
const _TERMINAL_EVENT = { done: 'task_completed', failed: 'task_failed', interrupted: 'task_interrupted' };
|
|
558
|
+
function emitTaskAnalytics(t) {
|
|
559
|
+
if (!t || t.id == null) return;
|
|
560
|
+
if (!_analyticsEmitted.has(`created:${t.id}`)) {
|
|
561
|
+
_analyticsEmitted.add(`created:${t.id}`);
|
|
562
|
+
emitEvent('task_created', ANALYTICS_UID, { source: 'engine' });
|
|
563
|
+
}
|
|
564
|
+
const ev = _TERMINAL_EVENT[t.status];
|
|
565
|
+
if (ev && !_analyticsEmitted.has(`${t.status}:${t.id}`)) {
|
|
566
|
+
_analyticsEmitted.add(`${t.status}:${t.id}`);
|
|
567
|
+
emitEvent(ev, ANALYTICS_UID, { status: t.status, source: 'engine' });
|
|
568
|
+
postHistory(t);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Phase 2 cloud history (paid). On a finished task, post an encrypted history
|
|
573
|
+
// record (the Worker gates on the license token and encrypts the content, so
|
|
574
|
+
// free / no-token installs store nothing — their work stays fully local).
|
|
575
|
+
// Fire-and-forget: never blocks or breaks a run.
|
|
576
|
+
function readLicenseToken() {
|
|
577
|
+
try { return existsSync(licenseFile) ? readFileSync(licenseFile, 'utf8').trim() : null; } catch { return null; }
|
|
578
|
+
}
|
|
579
|
+
function postHistory(t) {
|
|
580
|
+
if (!BILLING_API_URL) return;
|
|
581
|
+
const token = readLicenseToken();
|
|
582
|
+
if (!token) return; // no license → nothing stored server-side (stays local)
|
|
583
|
+
const goal = goals.find((g) => g.id === t.goalId);
|
|
584
|
+
const project = projects.find((p) => p.id === t.projectId);
|
|
585
|
+
try {
|
|
586
|
+
fetch(`${BILLING_API_URL}/history`, {
|
|
587
|
+
method: 'POST',
|
|
588
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
589
|
+
body: JSON.stringify({
|
|
590
|
+
id: `t${t.id}`,
|
|
591
|
+
project: project?.name ?? null,
|
|
592
|
+
content: goal?.text ?? null, // the user's request — encrypted server-side
|
|
593
|
+
summary: t.result ?? null, // the task's result — encrypted server-side
|
|
594
|
+
status: t.status,
|
|
595
|
+
createdAt: t.startedAt ? Date.parse(t.startedAt) : Date.now(),
|
|
596
|
+
}),
|
|
597
|
+
}).catch(() => {});
|
|
598
|
+
} catch { /* never throw into the caller */ }
|
|
544
599
|
}
|
|
545
600
|
const ACTIVITY_LIMIT = 1000;
|
|
546
601
|
function activityLine(line) {
|
|
@@ -3265,6 +3320,20 @@ const server6 = createServer(server.listeners('request')[0]);
|
|
|
3265
3320
|
server6.on('error', (e) => console.log(`[manager] ::1 listen skipped: ${e.code}`));
|
|
3266
3321
|
try { server6.listen(PORT, '::1'); } catch { /* IPv6 loopback unavailable */ }
|
|
3267
3322
|
|
|
3323
|
+
// Guard the primary IPv4 listen the same way the ::1 twin is guarded: without
|
|
3324
|
+
// this, a busy port emits an unhandled 'error' and Node prints a raw stack
|
|
3325
|
+
// trace + crashes. The common cause is a second manager (or `npx @galda/cli`)
|
|
3326
|
+
// started while one is already running on this port — so say that, and how to
|
|
3327
|
+
// pick another port, instead of dumping a stack trace.
|
|
3328
|
+
server.on('error', (e) => {
|
|
3329
|
+
if (e.code === 'EADDRINUSE') {
|
|
3330
|
+
console.error(`[manager] port ${PORT} is already in use — another manager is likely running on it.`);
|
|
3331
|
+
console.error(`[manager] Use a different port: MANAGER_PORT=${PORT + 10} npx @galda/cli`);
|
|
3332
|
+
process.exit(1);
|
|
3333
|
+
}
|
|
3334
|
+
throw e;
|
|
3335
|
+
});
|
|
3336
|
+
|
|
3268
3337
|
server.listen(PORT, '127.0.0.1', () => {
|
|
3269
3338
|
console.log(`[manager] Manager for AI → http://localhost:${PORT}/?key=${ACCESS_KEY}`);
|
|
3270
3339
|
if (process.env.MANAGER_OPEN_BROWSER === '1' && process.platform === 'darwin') {
|
package/package.json
CHANGED