@rubytech/create-sitedesk-code 0.1.525 → 0.1.527
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/dist/__tests__/samba-provision.test.js +46 -1
- package/dist/__tests__/websockify-bind-wiring.test.js +47 -0
- package/dist/index.js +39 -1
- package/dist/samba-provision.js +37 -0
- package/package.json +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +80 -13
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +13 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +70 -4
- package/payload/platform/plugins/docs/references/internals.md +1 -1
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/linkedin-extension/extension/README.md +10 -2
- package/payload/platform/plugins/linkedin-extension/extension/background/sw.js +37 -4
- package/payload/platform/plugins/linkedin-extension/extension/content/profile.js +17 -2
- package/payload/platform/plugins/linkedin-extension/extension/content/thread.js +17 -2
- package/payload/platform/plugins/linkedin-extension/extension/manifest.json +1 -0
- package/payload/platform/plugins/linkedin-extension/extension/options/options.html +1 -0
- package/payload/platform/plugins/linkedin-extension/extension/options/options.js +78 -3
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js +63 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/routine-roster-audit.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +35 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +78 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +13 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/scripts/vnc.sh +45 -1
- package/payload/server/{chunk-VUXKXOJU.js → chunk-7MGR5M6G.js} +4 -2
- package/payload/server/{chunk-WT2PFGY4.js → chunk-KY3BBT67.js} +216 -1
- package/payload/server/{manager-4TJK3YOY.js → manager-6XOWHREK.js} +1 -1
- package/payload/server/maxy-edge.js +226 -6
- package/payload/server/public/activity.html +5 -5
- package/payload/server/public/agents.html +4 -4
- package/payload/server/public/assets/{AdminLoginScreens-BgDjueVe.js → AdminLoginScreens-DSt9DYZZ.js} +1 -1
- package/payload/server/public/assets/AdminLoginScreens-DSt9DYZZ.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-DSt9DYZZ.js.gz +0 -0
- package/payload/server/public/assets/{AdminShell-YctTrR-D.js → AdminShell-MM3_yp3N.js} +1 -1
- package/payload/server/public/assets/AdminShell-MM3_yp3N.js.br +0 -0
- package/payload/server/public/assets/AdminShell-MM3_yp3N.js.gz +0 -0
- package/payload/server/public/assets/{activity-DGQt5ys_.js → activity-aVvbaVd4.js} +1 -1
- package/payload/server/public/assets/activity-aVvbaVd4.js.br +0 -0
- package/payload/server/public/assets/activity-aVvbaVd4.js.gz +0 -0
- package/payload/server/public/assets/{admin-DnUxbB3P.js → admin-B0lQnkH-.js} +1 -1
- package/payload/server/public/assets/admin-B0lQnkH-.js.br +0 -0
- package/payload/server/public/assets/admin-B0lQnkH-.js.gz +0 -0
- package/payload/server/public/assets/{agents-Bc2LZjwC.js → agents-CrpVTPRG.js} +1 -1
- package/payload/server/public/assets/agents-CrpVTPRG.js.br +0 -0
- package/payload/server/public/assets/agents-CrpVTPRG.js.gz +0 -0
- package/payload/server/public/assets/browser-CYHUzTop.js +1 -0
- package/payload/server/public/assets/browser-CYHUzTop.js.br +0 -0
- package/payload/server/public/assets/browser-CYHUzTop.js.gz +0 -0
- package/payload/server/public/assets/{calendar-D25EhXbD.js → calendar-4IWQpwYv.js} +1 -1
- package/payload/server/public/assets/calendar-4IWQpwYv.js.br +0 -0
- package/payload/server/public/assets/calendar-4IWQpwYv.js.gz +0 -0
- package/payload/server/public/assets/chat-DmjZTqnV.js +1 -0
- package/payload/server/public/assets/chat-DmjZTqnV.js.br +0 -0
- package/payload/server/public/assets/chat-DmjZTqnV.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-CIl-s3LZ.js +1 -0
- package/payload/server/public/assets/chevron-left-CIl-s3LZ.js.br +0 -0
- package/payload/server/public/assets/chevron-right-DpAi2BsF.js +1 -0
- package/payload/server/public/assets/chevron-right-DpAi2BsF.js.br +0 -0
- package/payload/server/public/assets/clock-CGuM9VRx.js +1 -0
- package/payload/server/public/assets/clock-CGuM9VRx.js.br +0 -0
- package/payload/server/public/assets/data-BpdFiZKH.js +1 -0
- package/payload/server/public/assets/data-BpdFiZKH.js.br +2 -0
- package/payload/server/public/assets/data-BpdFiZKH.js.gz +0 -0
- package/payload/server/public/assets/{file-download-CiGklOfA.js → file-download-DayBHhKS.js} +1 -1
- package/payload/server/public/assets/file-download-DayBHhKS.js.br +0 -0
- package/payload/server/public/assets/file-download-DayBHhKS.js.gz +0 -0
- package/payload/server/public/assets/{file-text-XjJPULAZ.js → file-text-BSyhVH0m.js} +1 -1
- package/payload/server/public/assets/file-text-BSyhVH0m.js.br +0 -0
- package/payload/server/public/assets/file-text-BSyhVH0m.js.gz +0 -0
- package/payload/server/public/assets/{graph-BjHJ0z_6.js → graph-Bjv3Tc3B.js} +1 -1
- package/payload/server/public/assets/graph-Bjv3Tc3B.js.br +0 -0
- package/payload/server/public/assets/graph-Bjv3Tc3B.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-D-0u37KA.js → graph-labels-DegLKGdx.js} +1 -1
- package/payload/server/public/assets/graph-labels-DegLKGdx.js.br +0 -0
- package/payload/server/public/assets/graph-labels-DegLKGdx.js.gz +0 -0
- package/payload/server/public/assets/{maximize-2-BJ9S-nfS.js → maximize-2-DJFG2EXJ.js} +1 -1
- package/payload/server/public/assets/maximize-2-DJFG2EXJ.js.br +0 -0
- package/payload/server/public/assets/maximize-2-DJFG2EXJ.js.gz +0 -0
- package/payload/server/public/assets/{operator-Cay4rOOK.js → operator-Du9Ve_PR.js} +1 -1
- package/payload/server/public/assets/operator-Du9Ve_PR.js.br +0 -0
- package/payload/server/public/assets/operator-Du9Ve_PR.js.gz +0 -0
- package/payload/server/public/assets/{page-shuh4ES9.js → page-DgHYxG3M.js} +1 -1
- package/payload/server/public/assets/page-DgHYxG3M.js.br +0 -0
- package/payload/server/public/assets/page-DgHYxG3M.js.gz +0 -0
- package/payload/server/public/assets/{page-Cci_j_y7.js → page-qV1ypb6i.js} +5 -5
- package/payload/server/public/assets/page-qV1ypb6i.js.br +0 -0
- package/payload/server/public/assets/page-qV1ypb6i.js.gz +0 -0
- package/payload/server/public/assets/{public-BbJcQGsO.js → public-CNVEM-Bx.js} +1 -1
- package/payload/server/public/assets/public-CNVEM-Bx.js.br +0 -0
- package/payload/server/public/assets/public-CNVEM-Bx.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-jeSVwUAv.js → rotate-ccw-DQCaky8_.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-DQCaky8_.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-DQCaky8_.js.gz +0 -0
- package/payload/server/public/assets/{routines-D3M1Wh3a.js → routines-DwK5FeLm.js} +1 -1
- package/payload/server/public/assets/routines-DwK5FeLm.js.br +0 -0
- package/payload/server/public/assets/routines-DwK5FeLm.js.gz +0 -0
- package/payload/server/public/assets/{skills-Bhej2ub1.js → skills-CosN3uA4.js} +1 -1
- package/payload/server/public/assets/skills-CosN3uA4.js.br +0 -0
- package/payload/server/public/assets/skills-CosN3uA4.js.gz +0 -0
- package/payload/server/public/assets/tasks-f0QikHmw.js +3 -0
- package/payload/server/public/assets/tasks-f0QikHmw.js.br +0 -0
- package/payload/server/public/assets/tasks-f0QikHmw.js.gz +0 -0
- package/payload/server/public/assets/{time-entry-format-r5UY9rjG.js → time-entry-format-DLTdS35_.js} +1 -1
- package/payload/server/public/assets/time-entry-format-DLTdS35_.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-DLTdS35_.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert--PCKaewD.js → triangle-alert-CpwrbdYq.js} +1 -1
- package/payload/server/public/assets/triangle-alert-CpwrbdYq.js.br +3 -0
- package/payload/server/public/assets/triangle-alert-CpwrbdYq.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-Dg5lNgqH.js → useCopyFeedback-D01gSp36.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-D01gSp36.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-D01gSp36.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CJzJ9xV3.css +1 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CJzJ9xV3.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-CJzJ9xV3.css.gz +0 -0
- package/payload/server/public/assets/{useVoiceRecorder-BY6ZsGMH.js → useVoiceRecorder-DiPgyDUy.js} +1 -1
- package/payload/server/public/assets/useVoiceRecorder-DiPgyDUy.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-DiPgyDUy.js.gz +0 -0
- package/payload/server/public/assets/{wrench-Bw1FMolk.js → wrench-C5SJreEu.js} +1 -1
- package/payload/server/public/assets/wrench-C5SJreEu.js.br +1 -0
- package/payload/server/public/assets/wrench-C5SJreEu.js.gz +0 -0
- package/payload/server/public/brand-defaults.css +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +7 -7
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +12 -12
- package/payload/server/public/graph.html +9 -9
- package/payload/server/public/index.html +15 -15
- package/payload/server/public/operator.html +14 -14
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/routines.html +7 -7
- package/payload/server/public/skills.html +5 -5
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +53 -9
- package/payload/server/public/assets/AdminLoginScreens-BgDjueVe.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-BgDjueVe.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-YctTrR-D.js.br +0 -0
- package/payload/server/public/assets/AdminShell-YctTrR-D.js.gz +0 -0
- package/payload/server/public/assets/activity-DGQt5ys_.js.br +0 -0
- package/payload/server/public/assets/activity-DGQt5ys_.js.gz +0 -0
- package/payload/server/public/assets/admin-DnUxbB3P.js.br +0 -0
- package/payload/server/public/assets/admin-DnUxbB3P.js.gz +0 -0
- package/payload/server/public/assets/agents-Bc2LZjwC.js.br +0 -0
- package/payload/server/public/assets/agents-Bc2LZjwC.js.gz +0 -0
- package/payload/server/public/assets/browser-BtF7SzvG.js +0 -1
- package/payload/server/public/assets/browser-BtF7SzvG.js.br +0 -0
- package/payload/server/public/assets/browser-BtF7SzvG.js.gz +0 -0
- package/payload/server/public/assets/calendar-D25EhXbD.js.br +0 -0
- package/payload/server/public/assets/calendar-D25EhXbD.js.gz +0 -0
- package/payload/server/public/assets/chat-z4jsHFXg.js +0 -1
- package/payload/server/public/assets/chat-z4jsHFXg.js.br +0 -0
- package/payload/server/public/assets/chat-z4jsHFXg.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-BD09SN6R.js +0 -1
- package/payload/server/public/assets/chevron-right-ChmWeo7T.js +0 -1
- package/payload/server/public/assets/chevron-right-ChmWeo7T.js.br +0 -0
- package/payload/server/public/assets/clock-Yqf7Ke7I.js +0 -1
- package/payload/server/public/assets/clock-Yqf7Ke7I.js.br +0 -0
- package/payload/server/public/assets/clock-Yqf7Ke7I.js.gz +0 -0
- package/payload/server/public/assets/data-RmnKj5nC.js +0 -1
- package/payload/server/public/assets/data-RmnKj5nC.js.br +0 -2
- package/payload/server/public/assets/data-RmnKj5nC.js.gz +0 -0
- package/payload/server/public/assets/file-download-CiGklOfA.js.br +0 -0
- package/payload/server/public/assets/file-download-CiGklOfA.js.gz +0 -0
- package/payload/server/public/assets/file-text-XjJPULAZ.js.br +0 -0
- package/payload/server/public/assets/file-text-XjJPULAZ.js.gz +0 -0
- package/payload/server/public/assets/graph-BjHJ0z_6.js.br +0 -0
- package/payload/server/public/assets/graph-BjHJ0z_6.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-D-0u37KA.js.br +0 -0
- package/payload/server/public/assets/graph-labels-D-0u37KA.js.gz +0 -0
- package/payload/server/public/assets/maximize-2-BJ9S-nfS.js.br +0 -0
- package/payload/server/public/assets/maximize-2-BJ9S-nfS.js.gz +0 -0
- package/payload/server/public/assets/operator-Cay4rOOK.js.br +0 -0
- package/payload/server/public/assets/operator-Cay4rOOK.js.gz +0 -0
- package/payload/server/public/assets/page-Cci_j_y7.js.br +0 -0
- package/payload/server/public/assets/page-Cci_j_y7.js.gz +0 -0
- package/payload/server/public/assets/page-shuh4ES9.js.br +0 -0
- package/payload/server/public/assets/page-shuh4ES9.js.gz +0 -0
- package/payload/server/public/assets/public-BbJcQGsO.js.br +0 -0
- package/payload/server/public/assets/public-BbJcQGsO.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-jeSVwUAv.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-jeSVwUAv.js.gz +0 -0
- package/payload/server/public/assets/routines-D3M1Wh3a.js.br +0 -0
- package/payload/server/public/assets/routines-D3M1Wh3a.js.gz +0 -0
- package/payload/server/public/assets/skills-Bhej2ub1.js.br +0 -0
- package/payload/server/public/assets/skills-Bhej2ub1.js.gz +0 -0
- package/payload/server/public/assets/tasks-qY-dtOI2.js +0 -1
- package/payload/server/public/assets/tasks-qY-dtOI2.js.br +0 -0
- package/payload/server/public/assets/tasks-qY-dtOI2.js.gz +0 -0
- package/payload/server/public/assets/time-entry-format-r5UY9rjG.js.br +0 -0
- package/payload/server/public/assets/time-entry-format-r5UY9rjG.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert--PCKaewD.js.br +0 -2
- package/payload/server/public/assets/triangle-alert--PCKaewD.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-Dg5lNgqH.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-Dg5lNgqH.js.gz +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BYfe9vj8.css +0 -1
- package/payload/server/public/assets/useSubAccountSwitcher-BYfe9vj8.css.br +0 -0
- package/payload/server/public/assets/useSubAccountSwitcher-BYfe9vj8.css.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-BY6ZsGMH.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-BY6ZsGMH.js.gz +0 -0
- package/payload/server/public/assets/wrench-Bw1FMolk.js.br +0 -0
- package/payload/server/public/assets/wrench-Bw1FMolk.js.gz +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-CnpeWIYq.js → useSubAccountSwitcher-BRHYXwGO.js} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-CnpeWIYq.js.br → useSubAccountSwitcher-BRHYXwGO.js.br} +0 -0
- /package/payload/server/public/assets/{useSubAccountSwitcher-CnpeWIYq.js.gz → useSubAccountSwitcher-BRHYXwGO.js.gz} +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// style and runs under `node --test dist/__tests__/*.test.js` after build.
|
|
7
7
|
import test from "node:test";
|
|
8
8
|
import assert from "node:assert/strict";
|
|
9
|
-
import { renderBrandStanza, renderGlobalSection, renderFullSmbConf, isPrivateIPv4, pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, mergeSmbConf, removeBrandStanza, hasAnyBrandStanza, formatSambaMarker, SAMBA_STEPS, SAMBA_ENABLE_UNITS, } from "../samba-provision.js";
|
|
9
|
+
import { renderBrandStanza, renderGlobalSection, renderFullSmbConf, isPrivateIPv4, pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, pickWebsockifyBind, formatWebsockifyBindFile, mergeSmbConf, removeBrandStanza, hasAnyBrandStanza, formatSambaMarker, SAMBA_STEPS, SAMBA_ENABLE_UNITS, } from "../samba-provision.js";
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
// Fixtures
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
@@ -384,6 +384,51 @@ test("formatSambaMarker emits the `[install-invariant] samba-provision-<step> <s
|
|
|
384
384
|
assert.equal(formatSambaMarker("units", "fail: Job for smbd.service failed"), "[install-invariant] samba-provision-units fail: Job for smbd.service failed");
|
|
385
385
|
});
|
|
386
386
|
// ---------------------------------------------------------------------------
|
|
387
|
+
// pickWebsockifyBind — the noVNC transport's host bind posture (Task 2089)
|
|
388
|
+
//
|
|
389
|
+
// The mapper reads the edge decision and nothing else, so a host can never end
|
|
390
|
+
// up with a public noVNC page behind a loopback edge. The `kind` passthrough is
|
|
391
|
+
// what lets the runtime log line distinguish `loopback-only` from `none`, which
|
|
392
|
+
// map to the same address.
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
test("pickWebsockifyBind: LAN host keeps the wildcard bind", () => {
|
|
395
|
+
// Byte-identical to the pre-2089 behaviour. The Pi's `.local` noVNC page is
|
|
396
|
+
// reached over whichever family avahi resolved, so the wildcard stays `[::]`
|
|
397
|
+
// rather than narrowing to `0.0.0.0`.
|
|
398
|
+
assert.deepEqual(pickWebsockifyBind(pickEdgeBindDecision(PI_WLAN0_ONLY)), {
|
|
399
|
+
bind: "[::]",
|
|
400
|
+
kind: "lan",
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
test("pickWebsockifyBind: public-only host binds loopback", () => {
|
|
404
|
+
assert.deepEqual(pickWebsockifyBind(pickEdgeBindDecision(HETZNER_PUBLIC_ETH0_ONLY)), {
|
|
405
|
+
bind: "127.0.0.1",
|
|
406
|
+
kind: "loopback-only",
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
test("pickWebsockifyBind: dual-homed cloud host binds loopback despite a private iface", () => {
|
|
410
|
+
// The fail-open Task 2082 closed for the edge, closed here too: a provider
|
|
411
|
+
// private network alongside a public address must not keep a public bind.
|
|
412
|
+
// `kind` stays `lan` because pickBindDecision keys on the private interface;
|
|
413
|
+
// the bind address is what the edge decision, not `kind`, dictates.
|
|
414
|
+
assert.deepEqual(pickWebsockifyBind(pickEdgeBindDecision(HETZNER_CLOUD_PUBLIC_PLUS_PRIVNET)), {
|
|
415
|
+
bind: "127.0.0.1",
|
|
416
|
+
kind: "lan",
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
test("pickWebsockifyBind: no non-loopback IPv4 binds loopback", () => {
|
|
420
|
+
assert.deepEqual(pickWebsockifyBind(pickEdgeBindDecision(PI_LO_ONLY)), {
|
|
421
|
+
bind: "127.0.0.1",
|
|
422
|
+
kind: "none",
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
test("formatWebsockifyBindFile: one parseable line, trailing newline", () => {
|
|
426
|
+
// vnc.sh parses this with `head -n1` + two sed captures, so the shape is
|
|
427
|
+
// load-bearing: two `key=value` pairs, space-separated, one line.
|
|
428
|
+
assert.equal(formatWebsockifyBindFile({ bind: "127.0.0.1", kind: "loopback-only" }), "bind=127.0.0.1 kind=loopback-only\n");
|
|
429
|
+
assert.equal(formatWebsockifyBindFile({ bind: "[::]", kind: "lan" }), "bind=[::] kind=lan\n");
|
|
430
|
+
});
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
387
432
|
// SAMBA_ENABLE_UNITS — the units step must never enable nmbd (Task 755)
|
|
388
433
|
// ---------------------------------------------------------------------------
|
|
389
434
|
test("SAMBA_ENABLE_UNITS enables smbd only — nmbd is never started", () => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Nothing else in the repo ties vnc.sh's websockify invocation to the installer
|
|
2
|
+
// that writes the file it reads, so either half could ship alone with the whole
|
|
3
|
+
// suite green. Two silent failures this closes:
|
|
4
|
+
//
|
|
5
|
+
// - installer writes the file, vnc.sh still hardcodes `[::]` → the Task 2089
|
|
6
|
+
// fix ships inert on every host, with every pickWebsockifyBind test passing;
|
|
7
|
+
// - vnc.sh reads the file, installer never writes it → resolve_websockify_bind
|
|
8
|
+
// exits 1 and the whole VNC stack is down on every host, not just cloud ones.
|
|
9
|
+
//
|
|
10
|
+
// Both files are read from disk at runtime, the same pattern as
|
|
11
|
+
// edge-bind-template-wiring.test.ts, which closes the equivalent gap for Task
|
|
12
|
+
// 2082's edge unit.
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, resolve } from "node:path";
|
|
18
|
+
// dist/__tests__/websockify-bind-wiring.test.js → ../../src/index.ts
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const INDEX_TS = resolve(here, "../../src/index.ts");
|
|
21
|
+
// dist/__tests__ → up four is the maxy-code root that holds platform/.
|
|
22
|
+
const VNC_SH = resolve(here, "../../../../platform/scripts/vnc.sh");
|
|
23
|
+
const SRC = readFileSync(INDEX_TS, "utf-8");
|
|
24
|
+
const VNC = readFileSync(VNC_SH, "utf-8");
|
|
25
|
+
test("vnc.sh no longer hardcodes the wildcard websockify bind", () => {
|
|
26
|
+
assert.doesNotMatch(VNC, /websockify --web \S+ "\[::\]:\$\{WEBSOCKIFY_PORT\}"/, "vnc.sh must take the websockify bind from platform/config/websockify-bind, not a literal [::]");
|
|
27
|
+
});
|
|
28
|
+
test("vnc.sh binds the resolved address and logs the decision", () => {
|
|
29
|
+
assert.match(VNC, /websockify --web \S+ "\$\{WEBSOCKIFY_BIND\}:\$\{WEBSOCKIFY_PORT\}"/, "the websockify invocation must interpolate the resolved bind address");
|
|
30
|
+
assert.match(VNC, /log "websockify-bind address=\$\{WEBSOCKIFY_BIND\} kind=\$\{WEBSOCKIFY_BIND_KIND\}/, "the websockify-bind token is the task's diagnostic grep — it must survive any edit");
|
|
31
|
+
});
|
|
32
|
+
test("vnc.sh resolves the bind before starting websockify", () => {
|
|
33
|
+
const resolveAt = VNC.indexOf("resolve_websockify_bind\n");
|
|
34
|
+
const startAt = VNC.indexOf("websockify --web");
|
|
35
|
+
assert.ok(resolveAt > 0, "vnc.sh must call resolve_websockify_bind");
|
|
36
|
+
assert.ok(resolveAt < startAt, "resolve_websockify_bind must run before the websockify invocation, or WEBSOCKIFY_BIND is unset");
|
|
37
|
+
});
|
|
38
|
+
test("vnc.sh refuses a bind address outside the two the mapper can produce", () => {
|
|
39
|
+
// The allow-list is what stops a hand-edited or half-written file handing
|
|
40
|
+
// websockify a wildcard on a cloud host.
|
|
41
|
+
assert.match(VNC, /"\[::\]"\|"127\.0\.0\.1"\) ;;/, "resolve_websockify_bind must allow-list the two addresses pickWebsockifyBind can emit");
|
|
42
|
+
});
|
|
43
|
+
test("the installer writes the file vnc.sh reads", () => {
|
|
44
|
+
assert.match(VNC, /config\/websockify-bind/, "vnc.sh must read platform/config/websockify-bind");
|
|
45
|
+
assert.match(SRC, /join\(configDir, "websockify-bind"\)/, "index.ts must write that same filename");
|
|
46
|
+
assert.match(SRC, /^\s*writeWebsockifyBindFile\(\);$/m, "index.ts defines the writer but never calls it — the file would never be written");
|
|
47
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import { classifyPortHolder } from "./preflight-port-classifier.js";
|
|
|
29
29
|
import { memoryControllerMissing, cmdlineAlreadyPatched, patchCmdline } from "./cgroup-memory-controller.js";
|
|
30
30
|
import { parsePluginList, computeInstallActions, parseExternalPlugins, findUnregisteredResyncs, discoverMarketplaceCandidateDirs, } from "./lib/plugin-install.js";
|
|
31
31
|
import { findPremiumMcpDirs } from "./lib/premium-mcp-discover.js";
|
|
32
|
-
import { pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, mergeSmbConf, formatSambaMarker, SAMBA_ENABLE_UNITS, } from "./samba-provision.js";
|
|
32
|
+
import { pickBindDecision, pickEdgeBindDecision, formatEdgeBindDecision, pickWebsockifyBind, formatWebsockifyBindFile, mergeSmbConf, formatSambaMarker, SAMBA_ENABLE_UNITS, } from "./samba-provision.js";
|
|
33
33
|
import { networkInterfaces, userInfo, cpus } from "node:os";
|
|
34
34
|
const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
|
|
35
35
|
// Brand manifest — read from payload to derive all brand-specific installation values.
|
|
@@ -641,6 +641,41 @@ function writeChromiumBinaryPathFile() {
|
|
|
641
641
|
console.log(` Wrote ${target} → ${RESOLVED_CHROMIUM_BIN}`);
|
|
642
642
|
logFile(` [snap-chromium] wrote ${target} contents=${RESOLVED_CHROMIUM_BIN}`);
|
|
643
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* Write the websockify bind address to
|
|
646
|
+
* `<INSTALL_DIR>/platform/config/websockify-bind` so vnc.sh gives the noVNC
|
|
647
|
+
* transport the same host posture the edge got in Task 2082. Task 2089.
|
|
648
|
+
*
|
|
649
|
+
* The carrier is a file rather than a unit environment variable because
|
|
650
|
+
* `vnc.sh start` has two callers and only one of them is the edge. The other is
|
|
651
|
+
* `ensureVnc()` (platform/ui/app/lib/vnc.ts), which shells the script from the
|
|
652
|
+
* maxy-ui process; the maxy-ui unit carries no EDGE_HOSTNAME, so an env-var
|
|
653
|
+
* carrier would silently restore a wildcard bind on every X-down recovery. A
|
|
654
|
+
* file is also correct for a hand-run `bash vnc.sh start`, which no unit
|
|
655
|
+
* environment reaches at all.
|
|
656
|
+
*
|
|
657
|
+
* Called immediately after writeChromiumBinaryPathFile for the same reason it
|
|
658
|
+
* is: platform/config is a payload subdirectory, so this must run after
|
|
659
|
+
* deployPayload. Linux-only — darwin runs no VNC stack (installServiceDarwin
|
|
660
|
+
* excludes every VNC/X11 variable for that reason).
|
|
661
|
+
*
|
|
662
|
+
* Idempotent: re-running the installer on an unchanged host rewrites the same
|
|
663
|
+
* bytes. A host that gains or loses an interface gets the new decision on the
|
|
664
|
+
* next install, which the console and install-log lines below make visible.
|
|
665
|
+
*/
|
|
666
|
+
function writeWebsockifyBindFile() {
|
|
667
|
+
if (process.platform !== "linux") {
|
|
668
|
+
logFile(` writeWebsockifyBindFile skipped: platform=${process.platform}`);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const decision = pickWebsockifyBind(pickEdgeBindDecision(networkInterfaces()));
|
|
672
|
+
const configDir = resolve(INSTALL_DIR, "platform/config");
|
|
673
|
+
mkdirSync(configDir, { recursive: true });
|
|
674
|
+
const target = join(configDir, "websockify-bind");
|
|
675
|
+
writeFileSync(target, formatWebsockifyBindFile(decision), { mode: 0o644 });
|
|
676
|
+
console.log(` Wrote ${target} → bind=${decision.bind} kind=${decision.kind}`);
|
|
677
|
+
logFile(` [websockify-bind] wrote ${target} bind=${decision.bind} kind=${decision.kind}`);
|
|
678
|
+
}
|
|
644
679
|
// Scrub any hardware-watchdog drop-in this installer (or an earlier version)
|
|
645
680
|
// left on the box, then daemon-reexec so the live RuntimeWatchdogUSec returns
|
|
646
681
|
// to systemd's default (0 = off). The hardware-watchdog feature is retired:
|
|
@@ -5333,6 +5368,9 @@ try {
|
|
|
5333
5368
|
// value. Must run after deployPayload (config dir is
|
|
5334
5369
|
// a payload subdirectory). Linux-only; no-op on darwin/non-linux.
|
|
5335
5370
|
writeChromiumBinaryPathFile();
|
|
5371
|
+
// Same install-time-file pattern and the same post-deployPayload
|
|
5372
|
+
// requirement: vnc.sh reads this to decide the websockify bind. Task 2089.
|
|
5373
|
+
writeWebsockifyBindFile();
|
|
5336
5374
|
// scrub plaintext neo4j passwords from any pre-fix install-*.log.
|
|
5337
5375
|
// Idempotent — re-running on already-redacted logs is a no-op. Runs after
|
|
5338
5376
|
// payload deploy so the bundled redact-install-logs.sh is on disk.
|
package/dist/samba-provision.js
CHANGED
|
@@ -224,6 +224,43 @@ export function formatEdgeBindDecision(d) {
|
|
|
224
224
|
return `[edge-bind-decision] kind=${d.kind} iface=${d.iface ?? "none"} ` +
|
|
225
225
|
`private=${list(d.privateAddresses)} public=${list(d.publicAddresses)} EDGE_HOSTNAME=${d.hostname}`;
|
|
226
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Map the edge's bind decision onto websockify. Task 2089.
|
|
229
|
+
*
|
|
230
|
+
* This deliberately takes `EdgeBindDecision` rather than the interface table:
|
|
231
|
+
* the noVNC page and the admin edge must never disagree about whether the box
|
|
232
|
+
* is publicly reachable, and reading the same decision is the only way to
|
|
233
|
+
* guarantee that. It is not a third classifier — `pickBindDecision` (Samba) and
|
|
234
|
+
* `pickEdgeBindDecision` (edge) remain the only two.
|
|
235
|
+
*
|
|
236
|
+
* The LAN outcome is the literal `[::]`, which is what shipped before this
|
|
237
|
+
* task, not `0.0.0.0`. avahi publishes an AAAA for `.local` names, so a client
|
|
238
|
+
* may reach the page over IPv6; narrowing the family would be a behaviour
|
|
239
|
+
* change this task did not ask for. `[::]` accepts IPv4 as well under the
|
|
240
|
+
* default `net.ipv6.bindv6only=0`, which is exactly why the loopback outcome
|
|
241
|
+
* has to be an explicit `127.0.0.1`.
|
|
242
|
+
*
|
|
243
|
+
* The loopback outcome binds `127.0.0.1` only, not `[::1]` and not both.
|
|
244
|
+
* websockify takes one bind address per process, so "both" would mean two
|
|
245
|
+
* processes. Measured on the netcup box 2026-07-29: `localhost` there resolves
|
|
246
|
+
* `::1` first (its image ships no `localhost` line in `/etc/hosts`), and a
|
|
247
|
+
* listener bound to `127.0.0.1` alone still served a request forwarded by
|
|
248
|
+
* `ssh -L <port>:localhost:<port>` — OpenSSH falls back from the refused `::1`.
|
|
249
|
+
* So the SSH-forward recovery path the installer prints keeps working.
|
|
250
|
+
*/
|
|
251
|
+
export function pickWebsockifyBind(d) {
|
|
252
|
+
return { bind: d.hostname === "0.0.0.0" ? "[::]" : "127.0.0.1", kind: d.kind };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Render the install-time carrier file read by `platform/scripts/vnc.sh`.
|
|
256
|
+
*
|
|
257
|
+
* The shape is load-bearing: vnc.sh takes `head -n1` and captures `bind=` and
|
|
258
|
+
* `kind=` with two seds, so this must stay one line of space-separated
|
|
259
|
+
* `key=value` pairs with a trailing newline.
|
|
260
|
+
*/
|
|
261
|
+
export function formatWebsockifyBindFile(d) {
|
|
262
|
+
return `bind=${d.bind} kind=${d.kind}\n`;
|
|
263
|
+
}
|
|
227
264
|
// ---------------------------------------------------------------------------
|
|
228
265
|
// smb.conf merge / remove
|
|
229
266
|
// ---------------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what SiteDesk ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:10d4f123ccb98ccdd68e27e8d22854fb1d9d5dfd985358ff1afa7f5361b25dfa
|
|
5
5
|
brand: sitedesk-code
|
|
6
6
|
product-name: SiteDesk
|
|
7
7
|
---
|
|
@@ -212,7 +212,7 @@ There is no dashboard, no settings panel, no menus. Everything is done through c
|
|
|
212
212
|
|
|
213
213
|
The chat input auto-grows as you type — it expands to fit your message and shrinks back when you delete text. You can also drag the resize handle above the input to set a custom height.
|
|
214
214
|
|
|
215
|
-
The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (database-operator, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with SiteDesk and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The sidebar's right edge is drag-resizable on every shell-hosted admin page (Sessions root, Chat, Graph, Browser, and Data): drag the handle to widen or narrow the sidebar, and your chosen width is remembered across reloads and shared across all of them. AdminShell mounts the drag handle itself, so every route that wraps its content in `<AdminShell>` gets sidebar resize for free; the handle does not render in the mobile drawer. The chat and artefact divider is also drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. The burger opens the navigation flyout: Dashboard, Data, Graph, Browser, and Chat at the top — Dashboard routes back to `/`, so every satellite page keeps a one-click way home, and the Chat item carries the Claude mark rather than a generic icon — then Public (the agents list) between the two dividers, and below the second divider the version row, Disconnect Claude, and Log out. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Download JSONL / View JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
|
|
215
|
+
The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (database-operator, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with SiteDesk and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The sidebar's right edge is drag-resizable on every shell-hosted admin page (Sessions root, Chat, Graph, Browser, and Data): drag the handle to widen or narrow the sidebar, and your chosen width is remembered across reloads and shared across all of them. AdminShell mounts the drag handle itself, so every route that wraps its content in `<AdminShell>` gets sidebar resize for free; the handle does not render in the mobile drawer. The chat and artefact divider is also drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. The conversation column on the Chat page has its own width control: a small handle sits half way up the page just inside each side of the column, and dragging either one widens or narrows the reading measure. The column stays centred, so both handles do the same thing — dragging the left one out and the right one out by the same distance give the same width. It will not go below its default 760px and will not go wider than the chat panel itself; the messages, the input box and the jump-to-latest button all move together. Double-click either handle to go back to 760px. Your chosen width is remembered across reloads. The handles do not appear on a phone (below 640px), where the conversation already fills the width. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. The burger opens the navigation flyout: Dashboard, Data, Graph, Browser, and Chat at the top — Dashboard routes back to `/`, so every satellite page keeps a one-click way home, and the Chat item carries the Claude mark rather than a generic icon — then Public (the agents list) between the two dividers, and below the second divider the version row, Disconnect Claude, and Log out. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Download JSONL / View JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
|
|
216
216
|
|
|
217
217
|
Page titles are brand-aware: the browser tab shows your product name (e.g. `Real Agent` instead of `SiteDesk`) on every shell — chat, graph, and data — so a non-default brand never leaks the default name in tab strips or browser history.
|
|
218
218
|
|
|
@@ -1215,7 +1215,7 @@ npx -y @rubytech/create-realagent-code@latest --uninstall
|
|
|
1215
1215
|
- **No SCP / rsync.** Updates are `npx -y @rubytech/create-maxy-code@latest …` again, never a file push from the operator's laptop.
|
|
1216
1216
|
- **Tunnel auth is OAuth; the API is permitted for the rest.** The tunnel's only auth path is `cloudflared tunnel login` in the noVNC browser over SSH forward. DNS, Pages, and D1 use the Cloudflare API with a short-lived narrow token the agent mints from an operator-provisioned master token (see the `cloudflare` plugin).
|
|
1217
1217
|
- **No shared state across brands.** Two brands on one server each have their own Neo4j port, systemd unit, VNC display, websockify port, tunnel, and persist directory.
|
|
1218
|
-
- **No public IPv4 exposure.** The Hetzner firewall opens port 22 only; every operator-facing surface is fronted by the Cloudflare tunnel. The provider firewall is no longer the only thing making that true for the
|
|
1218
|
+
- **No public IPv4 exposure.** The Hetzner firewall opens port 22 only; every operator-facing surface is fronted by the Cloudflare tunnel. The provider firewall is no longer the only thing making that true for the operator surfaces: this host carries a public IPv4, so the installer writes `EDGE_HOSTNAME=127.0.0.1` into the edge unit and records the same decision in `platform/config/websockify-bind`, which `vnc.sh` reads to bind the noVNC websockify transport. Both are loopback-only by construction, and that holds whether or not the box also has a Cloud Network attached. Confirm with `grep -E 'bind-decision|EDGE_HOSTNAME' $HOME/.<brand>/logs/install-*.log` and `grep websockify-bind $HOME/.<brand>/logs/vnc-boot.log`. Other listeners on the box are not covered; see [netcup.md](netcup.md) for what a host without a provider firewall still depends on `ufw` for.
|
|
1219
1219
|
|
|
1220
1220
|
## Smoke checklist
|
|
1221
1221
|
|
|
@@ -1415,21 +1415,22 @@ This stops and disables the systemd user-service, removes the unit file, removes
|
|
|
1415
1415
|
- **No SCP / rsync.** Updates are `npx -y @rubytech/create-maxy-code@latest …` again, never a file push from the operator's laptop.
|
|
1416
1416
|
- **Tunnel auth is OAuth; the API is permitted for the rest.** As on Hetzner.
|
|
1417
1417
|
- **No shared state across brands.** Two brands on one server each have their own Neo4j port, systemd unit, VNC display, websockify port, tunnel, and persist directory.
|
|
1418
|
-
- **No provider firewall, so `ufw` is the only network-level layer.** On Hetzner Cloud the provider firewall opens 22 only; netcup has nothing in front of the server. What keeps the
|
|
1418
|
+
- **No provider firewall, so `ufw` is the only network-level layer.** On Hetzner Cloud the provider firewall opens 22 only; netcup has nothing in front of the server. What keeps the operator surfaces private here is the bind itself. The installer classifies the host from its interface table and, because this box carries a public IPv4, binds **both** operator-facing listeners to loopback: it writes `EDGE_HOSTNAME=127.0.0.1` into the edge unit, and it records the same decision in `platform/config/websockify-bind`, which `vnc.sh` reads to bind the noVNC websockify transport. Neither surface is reachable from off the box by construction, so `ufw` is the backstop rather than the single control. Confirm both with `grep -E 'bind-decision|EDGE_HOSTNAME' $HOME/.<brand>/logs/install-*.log` and `grep websockify-bind $HOME/.<brand>/logs/vnc-boot.log`; on this host class they read `EDGE_HOSTNAME=127.0.0.1` and `address=127.0.0.1`. Other services on the box still bind all interfaces and still depend on `ufw` — Samba and CUPS among them — so anything that clears the ruleset (a flushed nftables set after a kernel change, `ufw disable` during debugging, a CCP image reinstall) exposes those. Verify from **off** the box, not with `ufw status` on it.
|
|
1419
1419
|
|
|
1420
1420
|
## Smoke checklist
|
|
1421
1421
|
|
|
1422
1422
|
Fresh-netcup smoke pass criteria:
|
|
1423
1423
|
|
|
1424
1424
|
1. Provision an RS 8000 G12 with Ubuntu 24.04 x86_64 and an SSH key; SSH in as `root`, create `admin`, switch.
|
|
1425
|
-
2. **From a machine off the server**, port 22 answers and the edge port does
|
|
1425
|
+
2. **From a machine off the server**, port 22 answers and neither the edge port nor the noVNC websockify port does. Two independent things make that true here: both listeners bind loopback on this host class, and `ufw` denies inbound. `ufw status` on the box states the intention; only an external probe confirms the outcome:
|
|
1426
1426
|
|
|
1427
1427
|
```bash
|
|
1428
1428
|
nc -z -w 5 <ipv4> 22 && echo "22 open (expected)"
|
|
1429
|
-
nc -z -w 5 <ipv4>
|
|
1429
|
+
nc -z -w 5 <ipv4> <edge port> && echo "EDGE EXPOSED — check the bind, then ufw"
|
|
1430
|
+
nc -z -w 5 <ipv4> <websockify port> && echo "noVNC EXPOSED — check the bind, then ufw"
|
|
1430
1431
|
```
|
|
1431
1432
|
|
|
1432
|
-
If
|
|
1433
|
+
If either answers, read the bind before the firewall. `grep -E 'bind-decision|EDGE_HOSTNAME' $HOME/.<brand>/logs/install-*.log` should show `EDGE_HOSTNAME=127.0.0.1`, `grep websockify-bind $HOME/.<brand>/logs/vnc-boot.log` should show `address=127.0.0.1`, and `ss -lntp` should show both on `127.0.0.1`. A wildcard or public address on either line is a classification fault, not a firewall fault.
|
|
1433
1434
|
|
|
1434
1435
|
Re-run this after every reboot, after any brand install, and after any CCP image reinstall.
|
|
1435
1436
|
3. Snapshot taken with the OS prepared and nothing installed.
|
|
@@ -1437,7 +1438,7 @@ Fresh-netcup smoke pass criteria:
|
|
|
1437
1438
|
5. `logs-read.sh --tail server 200 | grep 'op=capacity-resolved'` returns one line reporting `capacity=40`, with `totalMemGiB` matching the provisioned RAM. A `capacity=10` on a 64 GB box means the host resolved below the 56 GiB step and needs investigating before load.
|
|
1438
1439
|
6. The noVNC page reached over `ssh -L` displays the admin UI.
|
|
1439
1440
|
7. Cloudflare setup ends with `curl -I https://<hostname>.<your-zone>` returning `HTTP/2 200` from the operator's laptop.
|
|
1440
|
-
8. Reboot; both units come back up; the public URL is reachable again without any manual action; the step-2 external probe still shows the edge
|
|
1441
|
+
8. Reboot; both units come back up; the public URL is reachable again without any manual action; the step-2 external probe still shows the edge and noVNC ports closed.
|
|
1441
1442
|
9. Install a second brand with a different `--hostname`; both admin UIs are reachable on their own public hostnames and neither has touched the other's state.
|
|
1442
1443
|
10. Uninstall removes the systemd unit and the persist directory.
|
|
1443
1444
|
|
|
@@ -4008,15 +4009,58 @@ button** while it holds text — clicking empties the input, clears results
|
|
|
4008
4009
|
| Mount | Purpose |
|
|
4009
4010
|
|---|---|
|
|
4010
4011
|
| `/browser` | `POST /launch` — launches Chromium on the resolved transport (native display or VNC) on demand. Backs the standalone `/browser` operator page. Lifeline tag `[admin/browser/launch]`. |
|
|
4012
|
+
| `/browser` | `GET /screencast-targets` — the page-target list behind the screencast viewer. Server-side hop to the CDP HTTP surface, which is loopback-only; filtered to `type:'page'`. `501` off the Linux display stack, `502` when Chromium is unreachable. |
|
|
4013
|
+
| `/browser` | `POST /screencast-event` — relay for the client half of the `[browser/screencast]` lifeline. Accepts a fixed op allowlist (`request`, `screencast-started`, `frame`, `input`, `teardown`); the server-side ops are refused so a client cannot emit a line that reads as a server decision. |
|
|
4011
4014
|
|
|
4012
4015
|
The in-chat / artefact-embedded brand-VNC iframe components and the
|
|
4013
4016
|
`/browser-iframe` event-beacon route were retired and stay
|
|
4014
4017
|
deleted; the `/device-browser` (navigate-the-device-tab) route was also
|
|
4015
4018
|
removed and is not restored. The standalone `/browser` page is a
|
|
4016
|
-
sanctioned operator surface peer to `/graph` and `/data
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4019
|
+
sanctioned operator surface peer to `/graph` and `/data`.
|
|
4020
|
+
|
|
4021
|
+
**The human view is a CDP screencast, not a framebuffer.** `/browser`
|
|
4022
|
+
renders `CdpScreencastSurface`: frames arrive as CDP `Page.screencastFrame` and
|
|
4023
|
+
paint to a canvas, and the operator's clicks, scrolling and typing leave as
|
|
4024
|
+
`Input.dispatch*`. Interaction therefore lands on page coordinates rather than
|
|
4025
|
+
screen pixels, and the view is one tab chosen from a strip rather than the whole
|
|
4026
|
+
X display. `VncSurface` and `/vnc-viewer.html` still exist and still work, but
|
|
4027
|
+
nothing on `/browser` reaches them, and retiring that stack is separate work.
|
|
4028
|
+
|
|
4029
|
+
The transport is `/cdp-screencast` on the dashboard's own host and port, claimed
|
|
4030
|
+
by the edge listener before its `forwardUpgrade` catch-all. It is gated by
|
|
4031
|
+
`canAccessAdmin` and rejected on operator and public hosts (`reason` values
|
|
4032
|
+
`origin-operator-host` and `origin-public-host`, mirroring the websockify
|
|
4033
|
+
rejection), and it validates the target id against a fixed 32-hex charset because
|
|
4034
|
+
that id is interpolated into the upstream request line. Before replaying the
|
|
4035
|
+
upgrade to Chromium it drops the admin session cookie and the browser `Origin`:
|
|
4036
|
+
Chromium is launched with no `--remote-allow-origins`, so its DevTools endpoint
|
|
4037
|
+
has no allowlisted origin, and the proxy has already checked same-origin itself.
|
|
4038
|
+
|
|
4039
|
+
Chromium's debugging port is bound to `127.0.0.1` on `${CDP_PORT}` and is never
|
|
4040
|
+
tunnelled or bound off-localhost. That port carries no authentication and total
|
|
4041
|
+
control of the browser, which is why the proxy is the only path to it. Per-brand
|
|
4042
|
+
isolation is the `CDP_PORT` import and nothing else, so two co-resident brands
|
|
4043
|
+
each view their own Chromium.
|
|
4044
|
+
|
|
4045
|
+
Off the Linux display stack, and whenever Chromium is unreachable, the surface
|
|
4046
|
+
renders an explicit unavailable state. That is deliberate rather than cosmetic: a
|
|
4047
|
+
canvas that never receives a frame is indistinguishable from a black desktop,
|
|
4048
|
+
where the iframe it replaced failed visibly.
|
|
4049
|
+
|
|
4050
|
+
Observability is `[browser/screencast]` correlated by `sid` across
|
|
4051
|
+
`request → gate → cdp-connected → screencast-started → frame → input → teardown
|
|
4052
|
+
→ socket-closed`, plus `[cdp-ws-census] liveSockets=<N> sessions=<M>` on every
|
|
4053
|
+
open and close. Diagnostic path:
|
|
4054
|
+
|
|
4055
|
+
```
|
|
4056
|
+
logs-read.sh | grep -E '\[browser/screencast\]|\[cdp-ws-census\]'
|
|
4057
|
+
```
|
|
4058
|
+
|
|
4059
|
+
then filter to one `sid` for a whole session. Frame lines are sampled, so a
|
|
4060
|
+
frozen `seq` against a live socket is the frozen-viewer signature. `liveSockets`
|
|
4061
|
+
climbing while `sessions` stays flat is the leak signature: an operator tab that
|
|
4062
|
+
closes without a clean teardown emits no action log of its own, so the standing
|
|
4063
|
+
reconcile pass closes client-dead sockets and reports `op=orphan-reap closed=<N>`.
|
|
4020
4064
|
|
|
4021
4065
|
### Diagnostics
|
|
4022
4066
|
|
|
@@ -4317,6 +4361,29 @@ on every admin page; double-click resets to half the available width
|
|
|
4317
4361
|
(viewport minus sidebar), clamped to the per-pane min-width floors. The
|
|
4318
4362
|
chosen width is remembered across reloads.
|
|
4319
4363
|
|
|
4364
|
+
**The /chat reading measure is operator-set.** `--webchat-measure` was a fixed
|
|
4365
|
+
760px; it is now the FLOOR of a range the operator drags, written inline on
|
|
4366
|
+
`.webchat-page` by `WebchatSplitter` so the message column, the composer and the
|
|
4367
|
+
jump-to-latest button — all three read the same property — stay aligned. Two
|
|
4368
|
+
handles, one just inside each column edge, vertically centred. The column is
|
|
4369
|
+
centre-aligned, so one symmetric rule drives both: `measure = 2 × |pointerX −
|
|
4370
|
+
panelCentreX|`, which is why the two handles are interchangeable. Envelope:
|
|
4371
|
+
760px ≤ measure ≤ the chat panel's own width; double-click either handle to
|
|
4372
|
+
revert to 760px and clear the stored value; the measure persists in
|
|
4373
|
+
`localStorage` under `maxy-webchat-measure-px` and re-clamps on window resize and
|
|
4374
|
+
on any panel-width change (sidebar drag, artefact open/close). No handle renders
|
|
4375
|
+
below a 640px viewport, where the media query already gives the column the full
|
|
4376
|
+
width. A measure wider than its panel does not overflow — the column just fills
|
|
4377
|
+
the panel — so that state emits nothing on its own; the standing check is
|
|
4378
|
+
`[admin-ui] op=webchat-measure-audit measurePx= panelPx= availPx= columnPx=
|
|
4379
|
+
handles= governed= fit=`, emitted on mount and on every re-clamp with identical
|
|
4380
|
+
consecutive readings suppressed. `columnPx` is the measured `.wa-thread-scroll`
|
|
4381
|
+
rect, not the property read back, so a property written to the wrong element
|
|
4382
|
+
shows as `measurePx=1200 columnPx=760`; `columnPx=-1` means no transcript is
|
|
4383
|
+
mounted (the greeting panel). `fit=false` or `handles=` disagreeing with
|
|
4384
|
+
`governed=` is the failure. A completed drag logs `[admin-ui] webchat-resize
|
|
4385
|
+
px=<final>`; a bare click logs nothing.
|
|
4386
|
+
|
|
4320
4387
|
## Health vs version
|
|
4321
4388
|
|
|
4322
4389
|
Two endpoints, two surfaces, two restart-survival roles:
|
|
@@ -5314,7 +5381,7 @@ For estate-agent client work surfaced on the `/tasks` operator page, `work-creat
|
|
|
5314
5381
|
|
|
5315
5382
|
Two surfaces feed the gate. (1) **Workflow path:** `memory-write` accepts an optional `producedByTaskId` parameter. When set, an inbound `:PRODUCED` edge from that Task is composed into the write's relationships before the gate runs — the typical agent-side pattern is to call `work-create` at the start of an autonomous flow, capture `taskId`, and pass it as `producedByTaskId` on every subsequent `memory-write` for a gated label. The gate verifies Task and write share the same `accountId`; mismatch is rejected loud. (2) **Direct-ask path:** the admin server resolves the active `:AdminConversation`'s `sessionId` UUID and stamps it as `SESSION_NODE_ID` in the spawn env at PTY-spawn time. The same stamp propagates onto specialist subagent spawns the admin dispatches so listing-curator, content-producer, database-operator etc. inherit the same conversation anchor. The `contact-create` and `memory-write` wrappers call `injectConversationProvenance` (exported from [`@maxy/graph-write`](../../../lib/graph-write/src/conversation-provenance.ts)) which MATCHes `(c:Conversation {sessionId, accountId})` — account isolation is part of the natural key, not a separate gate — and prepends the synthetic `:PRODUCED` edge (composed by Neo4j elementId, which the helper reads off the MATCH). No agent-visible schema field changes. `memory-write` uses the env-stamp only as a fallback when `producedByTaskId` is unset; `contact-create` has no `producedByTaskId` parameter today and relies on the env-stamp alone. Autonomous (cron-driven) specialists with no parent conversation legitimately have no env-stamp; those must thread `producedByTaskId`.
|
|
5316
5383
|
|
|
5317
|
-
A task also carries a **workspace** edge, distinct from the two provenance ones above: `(t:Task)-[:WORKED_ON_IN]->(c:Conversation)` is the session an operator works the task in, written only by the `/tasks` page Session control. `RAISED_DURING` and `PART_OF` say where a task was *raised*, so neither can answer "which chat do I continue this task in" — a task raised in an unrelated conversation would resume that one. At most one such edge exists per task: the control's link route deletes any edge to a different Conversation before merging the new one
|
|
5384
|
+
A task also carries a **workspace** edge, distinct from the two provenance ones above: `(t:Task)-[:WORKED_ON_IN]->(c:Conversation)` is the session an operator works the task in, written only by the `/tasks` page Session control. `RAISED_DURING` and `PART_OF` say where a task was *raised*, so neither can answer "which chat do I continue this task in" — a task raised in an unrelated conversation would resume that one. At most one such edge exists per task: the control's link route deletes any edge to a different Conversation before merging the new one. The link route runs only on a first click, when the task has no session yet; a click on an already-linked task navigates straight to `/chat?session=<id>` and writes nothing, so an edge is never re-pointed after it is first written. The `:Task` also carries `sessionSpawnRequestedAt`, stamped before the spawn and removed when the link lands, so a session that was created but never linked is countable rather than invisible.
|
|
5318
5385
|
|
|
5319
5386
|
Operator audit cyphers:
|
|
5320
5387
|
- "What entities did this conversation's actions produce?" — `MATCH (c:AdminConversation {sessionId:$id})<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(e) RETURN labels(e), e.name, t.kind, t.status`
|
|
@@ -9,6 +9,19 @@ Invoked by the admin agent directly.
|
|
|
9
9
|
|
|
10
10
|
This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
|
|
11
11
|
|
|
12
|
+
## 2026-07-30 (0.1.527)
|
|
13
|
+
|
|
14
|
+
- The remote browser page now shows a live picture of the browser you can click and type into directly, replacing the old screen-sharing view.
|
|
15
|
+
- The Session button on an open task now opens the chat inside the dashboard instead of sending you out to a different site, and the new session starts already knowing the task's description and notes.
|
|
16
|
+
- Posting to LinkedIn from the browser extension works again. It was being stopped before the post ever left the browser, and when it failed it blamed the wrong thing; a missing permission and an unreachable device are now reported as the different problems they are.
|
|
17
|
+
- The page title is smaller and the chat column can be dragged wider or narrower.
|
|
18
|
+
|
|
19
|
+
## 2026-07-30 (0.1.526)
|
|
20
|
+
|
|
21
|
+
- Asking WhatsApp to reconnect now answers as soon as the reconnect starts, instead of waiting on the old connection to die and leaving you with no reply at all.
|
|
22
|
+
- The remote screen viewer is no longer reachable from every network the device sits on. It is now bound to the same posture chosen when the device was set up, and that choice is recorded so it stays put.
|
|
23
|
+
- A repeating routine that has lost its next run time is now reported by a standing check, so a routine that has quietly stopped being scheduled shows up instead of staying invisible.
|
|
24
|
+
|
|
12
25
|
## 2026-07-29 (0.1.525)
|
|
13
26
|
|
|
14
27
|
- A repeating routine whose start date had already gone by now runs on its next due occurrence instead of sitting silent forever. There is also a standing check that tells you when a routine has a start date that never fired.
|
|
@@ -783,15 +783,58 @@ button** while it holds text — clicking empties the input, clears results
|
|
|
783
783
|
| Mount | Purpose |
|
|
784
784
|
|---|---|
|
|
785
785
|
| `/browser` | `POST /launch` — launches Chromium on the resolved transport (native display or VNC) on demand. Backs the standalone `/browser` operator page. Lifeline tag `[admin/browser/launch]`. |
|
|
786
|
+
| `/browser` | `GET /screencast-targets` — the page-target list behind the screencast viewer. Server-side hop to the CDP HTTP surface, which is loopback-only; filtered to `type:'page'`. `501` off the Linux display stack, `502` when Chromium is unreachable. |
|
|
787
|
+
| `/browser` | `POST /screencast-event` — relay for the client half of the `[browser/screencast]` lifeline. Accepts a fixed op allowlist (`request`, `screencast-started`, `frame`, `input`, `teardown`); the server-side ops are refused so a client cannot emit a line that reads as a server decision. |
|
|
786
788
|
|
|
787
789
|
The in-chat / artefact-embedded brand-VNC iframe components and the
|
|
788
790
|
`/browser-iframe` event-beacon route were retired and stay
|
|
789
791
|
deleted; the `/device-browser` (navigate-the-device-tab) route was also
|
|
790
792
|
removed and is not restored. The standalone `/browser` page is a
|
|
791
|
-
sanctioned operator surface peer to `/graph` and `/data
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
793
|
+
sanctioned operator surface peer to `/graph` and `/data`.
|
|
794
|
+
|
|
795
|
+
**The human view is a CDP screencast, not a framebuffer.** `/browser`
|
|
796
|
+
renders `CdpScreencastSurface`: frames arrive as CDP `Page.screencastFrame` and
|
|
797
|
+
paint to a canvas, and the operator's clicks, scrolling and typing leave as
|
|
798
|
+
`Input.dispatch*`. Interaction therefore lands on page coordinates rather than
|
|
799
|
+
screen pixels, and the view is one tab chosen from a strip rather than the whole
|
|
800
|
+
X display. `VncSurface` and `/vnc-viewer.html` still exist and still work, but
|
|
801
|
+
nothing on `/browser` reaches them, and retiring that stack is separate work.
|
|
802
|
+
|
|
803
|
+
The transport is `/cdp-screencast` on the dashboard's own host and port, claimed
|
|
804
|
+
by the edge listener before its `forwardUpgrade` catch-all. It is gated by
|
|
805
|
+
`canAccessAdmin` and rejected on operator and public hosts (`reason` values
|
|
806
|
+
`origin-operator-host` and `origin-public-host`, mirroring the websockify
|
|
807
|
+
rejection), and it validates the target id against a fixed 32-hex charset because
|
|
808
|
+
that id is interpolated into the upstream request line. Before replaying the
|
|
809
|
+
upgrade to Chromium it drops the admin session cookie and the browser `Origin`:
|
|
810
|
+
Chromium is launched with no `--remote-allow-origins`, so its DevTools endpoint
|
|
811
|
+
has no allowlisted origin, and the proxy has already checked same-origin itself.
|
|
812
|
+
|
|
813
|
+
Chromium's debugging port is bound to `127.0.0.1` on `${CDP_PORT}` and is never
|
|
814
|
+
tunnelled or bound off-localhost. That port carries no authentication and total
|
|
815
|
+
control of the browser, which is why the proxy is the only path to it. Per-brand
|
|
816
|
+
isolation is the `CDP_PORT` import and nothing else, so two co-resident brands
|
|
817
|
+
each view their own Chromium.
|
|
818
|
+
|
|
819
|
+
Off the Linux display stack, and whenever Chromium is unreachable, the surface
|
|
820
|
+
renders an explicit unavailable state. That is deliberate rather than cosmetic: a
|
|
821
|
+
canvas that never receives a frame is indistinguishable from a black desktop,
|
|
822
|
+
where the iframe it replaced failed visibly.
|
|
823
|
+
|
|
824
|
+
Observability is `[browser/screencast]` correlated by `sid` across
|
|
825
|
+
`request → gate → cdp-connected → screencast-started → frame → input → teardown
|
|
826
|
+
→ socket-closed`, plus `[cdp-ws-census] liveSockets=<N> sessions=<M>` on every
|
|
827
|
+
open and close. Diagnostic path:
|
|
828
|
+
|
|
829
|
+
```
|
|
830
|
+
logs-read.sh | grep -E '\[browser/screencast\]|\[cdp-ws-census\]'
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
then filter to one `sid` for a whole session. Frame lines are sampled, so a
|
|
834
|
+
frozen `seq` against a live socket is the frozen-viewer signature. `liveSockets`
|
|
835
|
+
climbing while `sessions` stays flat is the leak signature: an operator tab that
|
|
836
|
+
closes without a clean teardown emits no action log of its own, so the standing
|
|
837
|
+
reconcile pass closes client-dead sockets and reports `op=orphan-reap closed=<N>`.
|
|
795
838
|
|
|
796
839
|
### Diagnostics
|
|
797
840
|
|
|
@@ -1092,6 +1135,29 @@ on every admin page; double-click resets to half the available width
|
|
|
1092
1135
|
(viewport minus sidebar), clamped to the per-pane min-width floors. The
|
|
1093
1136
|
chosen width is remembered across reloads.
|
|
1094
1137
|
|
|
1138
|
+
**The /chat reading measure is operator-set.** `--webchat-measure` was a fixed
|
|
1139
|
+
760px; it is now the FLOOR of a range the operator drags, written inline on
|
|
1140
|
+
`.webchat-page` by `WebchatSplitter` so the message column, the composer and the
|
|
1141
|
+
jump-to-latest button — all three read the same property — stay aligned. Two
|
|
1142
|
+
handles, one just inside each column edge, vertically centred. The column is
|
|
1143
|
+
centre-aligned, so one symmetric rule drives both: `measure = 2 × |pointerX −
|
|
1144
|
+
panelCentreX|`, which is why the two handles are interchangeable. Envelope:
|
|
1145
|
+
760px ≤ measure ≤ the chat panel's own width; double-click either handle to
|
|
1146
|
+
revert to 760px and clear the stored value; the measure persists in
|
|
1147
|
+
`localStorage` under `maxy-webchat-measure-px` and re-clamps on window resize and
|
|
1148
|
+
on any panel-width change (sidebar drag, artefact open/close). No handle renders
|
|
1149
|
+
below a 640px viewport, where the media query already gives the column the full
|
|
1150
|
+
width. A measure wider than its panel does not overflow — the column just fills
|
|
1151
|
+
the panel — so that state emits nothing on its own; the standing check is
|
|
1152
|
+
`[admin-ui] op=webchat-measure-audit measurePx= panelPx= availPx= columnPx=
|
|
1153
|
+
handles= governed= fit=`, emitted on mount and on every re-clamp with identical
|
|
1154
|
+
consecutive readings suppressed. `columnPx` is the measured `.wa-thread-scroll`
|
|
1155
|
+
rect, not the property read back, so a property written to the wrong element
|
|
1156
|
+
shows as `measurePx=1200 columnPx=760`; `columnPx=-1` means no transcript is
|
|
1157
|
+
mounted (the greeting panel). `fit=false` or `handles=` disagreeing with
|
|
1158
|
+
`governed=` is the failure. A completed drag logs `[admin-ui] webchat-resize
|
|
1159
|
+
px=<final>`; a bare click logs nothing.
|
|
1160
|
+
|
|
1095
1161
|
## Health vs version
|
|
1096
1162
|
|
|
1097
1163
|
Two endpoints, two surfaces, two restart-survival roles:
|
|
@@ -560,7 +560,7 @@ For estate-agent client work surfaced on the `/tasks` operator page, `work-creat
|
|
|
560
560
|
|
|
561
561
|
Two surfaces feed the gate. (1) **Workflow path:** `memory-write` accepts an optional `producedByTaskId` parameter. When set, an inbound `:PRODUCED` edge from that Task is composed into the write's relationships before the gate runs — the typical agent-side pattern is to call `work-create` at the start of an autonomous flow, capture `taskId`, and pass it as `producedByTaskId` on every subsequent `memory-write` for a gated label. The gate verifies Task and write share the same `accountId`; mismatch is rejected loud. (2) **Direct-ask path:** the admin server resolves the active `:AdminConversation`'s `sessionId` UUID and stamps it as `SESSION_NODE_ID` in the spawn env at PTY-spawn time. The same stamp propagates onto specialist subagent spawns the admin dispatches so listing-curator, content-producer, database-operator etc. inherit the same conversation anchor. The `contact-create` and `memory-write` wrappers call `injectConversationProvenance` (exported from [`@maxy/graph-write`](../../../lib/graph-write/src/conversation-provenance.ts)) which MATCHes `(c:Conversation {sessionId, accountId})` — account isolation is part of the natural key, not a separate gate — and prepends the synthetic `:PRODUCED` edge (composed by Neo4j elementId, which the helper reads off the MATCH). No agent-visible schema field changes. `memory-write` uses the env-stamp only as a fallback when `producedByTaskId` is unset; `contact-create` has no `producedByTaskId` parameter today and relies on the env-stamp alone. Autonomous (cron-driven) specialists with no parent conversation legitimately have no env-stamp; those must thread `producedByTaskId`.
|
|
562
562
|
|
|
563
|
-
A task also carries a **workspace** edge, distinct from the two provenance ones above: `(t:Task)-[:WORKED_ON_IN]->(c:Conversation)` is the session an operator works the task in, written only by the `/tasks` page Session control. `RAISED_DURING` and `PART_OF` say where a task was *raised*, so neither can answer "which chat do I continue this task in" — a task raised in an unrelated conversation would resume that one. At most one such edge exists per task: the control's link route deletes any edge to a different Conversation before merging the new one
|
|
563
|
+
A task also carries a **workspace** edge, distinct from the two provenance ones above: `(t:Task)-[:WORKED_ON_IN]->(c:Conversation)` is the session an operator works the task in, written only by the `/tasks` page Session control. `RAISED_DURING` and `PART_OF` say where a task was *raised*, so neither can answer "which chat do I continue this task in" — a task raised in an unrelated conversation would resume that one. At most one such edge exists per task: the control's link route deletes any edge to a different Conversation before merging the new one. The link route runs only on a first click, when the task has no session yet; a click on an already-linked task navigates straight to `/chat?session=<id>` and writes nothing, so an edge is never re-pointed after it is first written. The `:Task` also carries `sessionSpawnRequestedAt`, stamped before the spawn and removed when the link lands, so a session that was created but never linked is countable rather than invisible.
|
|
564
564
|
|
|
565
565
|
Operator audit cyphers:
|
|
566
566
|
- "What entities did this conversation's actions produce?" — `MATCH (c:AdminConversation {sessionId:$id})<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(e) RETURN labels(e), e.name, t.kind, t.status`
|
|
@@ -85,7 +85,7 @@ There is no dashboard, no settings panel, no menus. Everything is done through c
|
|
|
85
85
|
|
|
86
86
|
The chat input auto-grows as you type — it expands to fit your message and shrinks back when you delete text. You can also drag the resize handle above the input to set a custom height.
|
|
87
87
|
|
|
88
|
-
The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (database-operator, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with SiteDesk and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The sidebar's right edge is drag-resizable on every shell-hosted admin page (Sessions root, Chat, Graph, Browser, and Data): drag the handle to widen or narrow the sidebar, and your chosen width is remembered across reloads and shared across all of them. AdminShell mounts the drag handle itself, so every route that wraps its content in `<AdminShell>` gets sidebar resize for free; the handle does not render in the mobile drawer. The chat and artefact divider is also drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. The burger opens the navigation flyout: Dashboard, Data, Graph, Browser, and Chat at the top — Dashboard routes back to `/`, so every satellite page keeps a one-click way home, and the Chat item carries the Claude mark rather than a generic icon — then Public (the agents list) between the two dividers, and below the second divider the version row, Disconnect Claude, and Log out. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Download JSONL / View JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
|
|
88
|
+
The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (database-operator, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with SiteDesk and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The sidebar's right edge is drag-resizable on every shell-hosted admin page (Sessions root, Chat, Graph, Browser, and Data): drag the handle to widen or narrow the sidebar, and your chosen width is remembered across reloads and shared across all of them. AdminShell mounts the drag handle itself, so every route that wraps its content in `<AdminShell>` gets sidebar resize for free; the handle does not render in the mobile drawer. The chat and artefact divider is also drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. The conversation column on the Chat page has its own width control: a small handle sits half way up the page just inside each side of the column, and dragging either one widens or narrows the reading measure. The column stays centred, so both handles do the same thing — dragging the left one out and the right one out by the same distance give the same width. It will not go below its default 760px and will not go wider than the chat panel itself; the messages, the input box and the jump-to-latest button all move together. Double-click either handle to go back to 760px. Your chosen width is remembered across reloads. The handles do not appear on a phone (below 640px), where the conversation already fills the width. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. The burger opens the navigation flyout: Dashboard, Data, Graph, Browser, and Chat at the top — Dashboard routes back to `/`, so every satellite page keeps a one-click way home, and the Chat item carries the Claude mark rather than a generic icon — then Public (the agents list) between the two dividers, and below the second divider the version row, Disconnect Claude, and Log out. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Download JSONL / View JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
|
|
89
89
|
|
|
90
90
|
Page titles are brand-aware: the browser tab shows your product name (e.g. `Real Agent` instead of `SiteDesk`) on every shell — chat, graph, and data — so a non-default brand never leaks the default name in tab strips or browser history.
|
|
91
91
|
|