@rubytech/create-sitedesk-code 0.1.340 → 0.1.341
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__/known-brand-hostnames.test.js +40 -0
- package/dist/__tests__/neo4j-teardown.test.js +55 -0
- package/dist/known-brands.js +24 -0
- package/dist/neo4j-teardown.js +45 -0
- package/dist/uninstall.js +89 -40
- package/package.json +1 -1
- package/payload/platform/config/brand.json +2 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -1
- package/payload/platform/plugins/docs/references/admin-ui.md +2 -0
- package/payload/server/{chunk-NE7G5GT7.js → chunk-5BG6CHGH.js} +17 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{AdminLoginScreens-CHTeh_Vu.js → AdminLoginScreens-Brx8CmXN.js} +1 -1
- package/payload/server/public/assets/{AdminShell-DjoP7YoA.js → AdminShell-CHZMDX2u.js} +1 -1
- package/payload/server/public/assets/{Checkbox-D58GsKoQ.js → Checkbox-aePjWzRH.js} +1 -1
- package/payload/server/public/assets/{OperatorConversations-CDdp2nVn.css → OperatorConversations-BMIZQR9t.css} +1 -1
- package/payload/server/public/assets/{OperatorConversations-RmqANYz8.js → OperatorConversations-DpjPPIOp.js} +1 -1
- package/payload/server/public/assets/{admin-LGICBqil.js → admin-DIDvfti6.js} +1 -1
- package/payload/server/public/assets/{browser-CRgweVtw.js → browser-Bp5kGgyr.js} +1 -1
- package/payload/server/public/assets/chat-C0IWx7FL.js +1 -0
- package/payload/server/public/assets/{data-CttrzhfL.js → data-RsMye_06.js} +1 -1
- package/payload/server/public/assets/{graph-CIBba84R.js → graph-Bnsvbnkf.js} +1 -1
- package/payload/server/public/assets/{graph-labels-Bi0fu8Ns.js → graph-labels-jduMtwXb.js} +1 -1
- package/payload/server/public/assets/{operator-mwkYv8g5.js → operator-9K-TElDd.js} +1 -1
- package/payload/server/public/assets/page-BT9hkXHm.js +30 -0
- package/payload/server/public/assets/{public-CiUboUwu.js → public-DvL1Zov1.js} +1 -1
- package/payload/server/public/brand/sitedesk-og-image.png +0 -0
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/chat.html +5 -5
- package/payload/server/public/data.html +4 -4
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +6 -6
- package/payload/server/public/operator.html +7 -7
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +183 -26
- package/payload/server/public/assets/chat-CXPRTVW7.js +0 -1
- package/payload/server/public/assets/page-BpxHz1N-.js +0 -30
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Drift guard: every shipped brand's hostname must appear in
|
|
2
|
+
// KNOWN_BRAND_HOSTNAMES, the allowlist that peerBrandPresent() matches against
|
|
3
|
+
// to decide whether a co-resident brand is still installed. A shipped brand
|
|
4
|
+
// missing from the list makes peerBrandPresent() report "no peer" while that
|
|
5
|
+
// brand survives, so the uninstall wipes device-wide singletons (~/.claude
|
|
6
|
+
// OAuth, ~/.ollama cache, shared Neo4j data, apt packages) it still depends on.
|
|
7
|
+
//
|
|
8
|
+
// The list is intentionally a SUPERSET of brands/ (it carries legacy hostnames
|
|
9
|
+
// and reserved slots that have no brand.json), so this is a subset check, not
|
|
10
|
+
// an equality check. The reverse direction is not constrained.
|
|
11
|
+
//
|
|
12
|
+
// Runs via Node's built-in test runner (the codebase convention — see
|
|
13
|
+
// peer-brand-detect.test.ts). Path resolution mirrors base-toolchain-deps.test.ts.
|
|
14
|
+
import test from "node:test";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { dirname, resolve, join } from "node:path";
|
|
19
|
+
import { KNOWN_BRAND_HOSTNAMES } from "../known-brands.js";
|
|
20
|
+
// dist/__tests__/known-brand-hostnames.test.js → maxy-code/brands
|
|
21
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const BRANDS_DIR = resolve(here, "../../../../brands");
|
|
23
|
+
function shippedBrandHostnames() {
|
|
24
|
+
return readdirSync(BRANDS_DIR, { withFileTypes: true })
|
|
25
|
+
.filter((e) => e.isDirectory())
|
|
26
|
+
.map((e) => join(BRANDS_DIR, e.name, "brand.json"))
|
|
27
|
+
.filter((p) => existsSync(p))
|
|
28
|
+
.map((p) => JSON.parse(readFileSync(p, "utf-8")).hostname);
|
|
29
|
+
}
|
|
30
|
+
test("brands/ directory is discoverable from the test location", () => {
|
|
31
|
+
assert.ok(existsSync(BRANDS_DIR), `brands dir not found at ${BRANDS_DIR}`);
|
|
32
|
+
assert.ok(shippedBrandHostnames().length > 0, "no shipped brand.json hostnames found");
|
|
33
|
+
});
|
|
34
|
+
test("every shipped brand hostname is in KNOWN_BRAND_HOSTNAMES", () => {
|
|
35
|
+
const allow = new Set(KNOWN_BRAND_HOSTNAMES);
|
|
36
|
+
for (const hostname of shippedBrandHostnames()) {
|
|
37
|
+
assert.ok(allow.has(hostname), `brands/ ships hostname "${hostname}" but it is absent from KNOWN_BRAND_HOSTNAMES; ` +
|
|
38
|
+
`peerBrandPresent() would not detect a co-resident "${hostname}" install`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Contract grid for resolveDedicatedNeo4jTeardown — the pure decision behind
|
|
2
|
+
// the uninstall's dedicated-Neo4j decommission. The wrapper in uninstall.ts
|
|
3
|
+
// owns fs/systemctl; this suite is decision-only, no I/O.
|
|
4
|
+
//
|
|
5
|
+
// The load-bearing guarantee: the dedicated-removal path NEVER targets the
|
|
6
|
+
// bare `neo4j` service / port 7687, including when the brand port is
|
|
7
|
+
// unreadable (undefined). Runs via Node's built-in test runner (the codebase
|
|
8
|
+
// convention — see peer-brand-detect.test.ts).
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { resolveDedicatedNeo4jTeardown, SHARED_NEO4J_PORT } from "../neo4j-teardown.js";
|
|
12
|
+
const HOST = "sitedesk-code";
|
|
13
|
+
test("dedicated port → full teardown plan with install-parity names", () => {
|
|
14
|
+
const plan = resolveDedicatedNeo4jTeardown({ brandHostname: HOST, neo4jPort: 7689 });
|
|
15
|
+
assert.notEqual(plan, null);
|
|
16
|
+
// Names derive from BRAND.hostname exactly as setupDedicatedNeo4j creates them.
|
|
17
|
+
assert.deepEqual(plan, {
|
|
18
|
+
service: "neo4j-sitedesk-code",
|
|
19
|
+
unitFile: "/etc/systemd/system/neo4j-sitedesk-code.service",
|
|
20
|
+
confDir: "/etc/neo4j-sitedesk-code",
|
|
21
|
+
dataDir: "/var/lib/neo4j-sitedesk-code",
|
|
22
|
+
logDir: "/var/log/neo4j-sitedesk-code",
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
test("dedicated plan never names the shared `neo4j` service", () => {
|
|
26
|
+
const plan = resolveDedicatedNeo4jTeardown({ brandHostname: HOST, neo4jPort: 7689 });
|
|
27
|
+
assert.notEqual(plan, null);
|
|
28
|
+
assert.notEqual(plan.service, "neo4j");
|
|
29
|
+
for (const p of [plan.unitFile, plan.confDir, plan.dataDir, plan.logDir]) {
|
|
30
|
+
// No teardown path is the shared apt-managed location.
|
|
31
|
+
assert.notEqual(p, "/var/lib/neo4j");
|
|
32
|
+
assert.notEqual(p, "/etc/neo4j");
|
|
33
|
+
assert.notEqual(p, "/var/log/neo4j");
|
|
34
|
+
assert.notEqual(p, "/etc/systemd/system/neo4j.service");
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
test("shared port 7687 → null (shared path, never the dedicated removal)", () => {
|
|
38
|
+
assert.equal(resolveDedicatedNeo4jTeardown({ brandHostname: HOST, neo4jPort: SHARED_NEO4J_PORT }), null);
|
|
39
|
+
});
|
|
40
|
+
test("unreadable brand port (undefined) → null, never selects shared", () => {
|
|
41
|
+
// The defect this guards: a port-read failure must NOT fall through to the
|
|
42
|
+
// shared `neo4j`/7687. null tells the caller to take the shared path only
|
|
43
|
+
// when the brand is genuinely shared, never on read failure of a dedicated brand.
|
|
44
|
+
assert.equal(resolveDedicatedNeo4jTeardown({ brandHostname: HOST, neo4jPort: undefined }), null);
|
|
45
|
+
});
|
|
46
|
+
test("empty hostname → null (cannot form a dedicated unit)", () => {
|
|
47
|
+
assert.equal(resolveDedicatedNeo4jTeardown({ brandHostname: "", neo4jPort: 7689 }), null);
|
|
48
|
+
});
|
|
49
|
+
test("a different dedicated port still yields a hostname-derived plan", () => {
|
|
50
|
+
// Unit/dir names depend on hostname, not the port value — a forced
|
|
51
|
+
// --neo4j-port that differs from brand.json still tears down the same unit.
|
|
52
|
+
const plan = resolveDedicatedNeo4jTeardown({ brandHostname: "realagent-code", neo4jPort: 7700 });
|
|
53
|
+
assert.equal(plan.service, "neo4j-realagent-code");
|
|
54
|
+
assert.equal(plan.dataDir, "/var/lib/neo4j-realagent-code");
|
|
55
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Allowlist of brand hostnames in the Maxy ecosystem. peerBrandPresent() in
|
|
2
|
+
// uninstall.ts matches systemd unit filenames (`<hostname>.service` and
|
|
3
|
+
// `<hostname>-edge.service`) against this list to decide whether a co-resident
|
|
4
|
+
// brand is still installed — the gate on every device-wide teardown step
|
|
5
|
+
// (shared ~/.claude / ~/.ollama wipe, shared Neo4j data wipe, apt purge,
|
|
6
|
+
// Ollama/Samba teardown). Matching only these filenames, not any stray
|
|
7
|
+
// `.service` file, keeps stale units and unrelated user services from counting
|
|
8
|
+
// as peer evidence.
|
|
9
|
+
//
|
|
10
|
+
// This list is a SUPERSET of the brands shipped under `brands/`: `maxy` and
|
|
11
|
+
// `realagent` are legacy hostnames, `maxy-2/3/4` are reserved slots. Every
|
|
12
|
+
// hostname under `brands/*/brand.json` MUST appear here, or peerBrandPresent()
|
|
13
|
+
// fails to detect that brand as a peer. known-brand-hostnames.test.ts enforces
|
|
14
|
+
// that subset relationship and fails when a new shipped brand is missing.
|
|
15
|
+
export const KNOWN_BRAND_HOSTNAMES = [
|
|
16
|
+
"maxy",
|
|
17
|
+
"maxy-code",
|
|
18
|
+
"realagent",
|
|
19
|
+
"realagent-code",
|
|
20
|
+
"sitedesk-code",
|
|
21
|
+
"maxy-2",
|
|
22
|
+
"maxy-3",
|
|
23
|
+
"maxy-4",
|
|
24
|
+
];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Pure dedicated-Neo4j teardown resolution. Extracted from uninstall.ts so the
|
|
2
|
+
// "does this brand run a dedicated Neo4j, and what does its full decommission
|
|
3
|
+
// target?" decision can be unit-tested with concrete inputs, no fs/systemctl.
|
|
4
|
+
// Mirrors the peer-brand-detect.ts pattern: inputs in, plan out, no I/O.
|
|
5
|
+
//
|
|
6
|
+
// The uninstaller wraps this with the privileged stop/disable/rm and the
|
|
7
|
+
// daemon-reload; this module owns only the naming + the shared-instance guard.
|
|
8
|
+
/** The apt-installed shared Neo4j binds this bolt port. A brand on this port
|
|
9
|
+
* is NOT dedicated — it shares the device-wide instance. */
|
|
10
|
+
export const SHARED_NEO4J_PORT = 7687;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the dedicated-Neo4j teardown plan for a brand, or `null` when the
|
|
13
|
+
* brand does not run a dedicated instance.
|
|
14
|
+
*
|
|
15
|
+
* Returns `null` — i.e. the dedicated-removal path must NOT act, and the caller
|
|
16
|
+
* falls back to its shared handling — in every case where targeting a dedicated
|
|
17
|
+
* unit would be wrong or unsafe:
|
|
18
|
+
* - `neo4jPort === undefined`: the port is unreadable. A read failure must
|
|
19
|
+
* never select the shared `neo4j`/7687 service; returning null keeps the
|
|
20
|
+
* dedicated path inert rather than guessing.
|
|
21
|
+
* - `neo4jPort === SHARED_NEO4J_PORT`: the brand genuinely shares 7687.
|
|
22
|
+
* - empty `brandHostname`: no hostname means no `neo4j-<hostname>` unit can
|
|
23
|
+
* exist; refuse rather than form a degenerate `neo4j-` name.
|
|
24
|
+
*
|
|
25
|
+
* When a plan IS returned, `service` is always `neo4j-<hostname>` and can never
|
|
26
|
+
* equal the bare `neo4j` — the hard guard against ever stopping or deleting the
|
|
27
|
+
* shared instance.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveDedicatedNeo4jTeardown(args) {
|
|
30
|
+
const { brandHostname, neo4jPort } = args;
|
|
31
|
+
if (neo4jPort === undefined)
|
|
32
|
+
return null;
|
|
33
|
+
if (neo4jPort === SHARED_NEO4J_PORT)
|
|
34
|
+
return null;
|
|
35
|
+
if (!brandHostname)
|
|
36
|
+
return null;
|
|
37
|
+
const service = `neo4j-${brandHostname}`;
|
|
38
|
+
return {
|
|
39
|
+
service,
|
|
40
|
+
unitFile: `/etc/systemd/system/${service}.service`,
|
|
41
|
+
confDir: `/etc/neo4j-${brandHostname}`,
|
|
42
|
+
dataDir: `/var/lib/neo4j-${brandHostname}`,
|
|
43
|
+
logDir: `/var/log/neo4j-${brandHostname}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
package/dist/uninstall.js
CHANGED
|
@@ -4,6 +4,8 @@ import { resolve, join, dirname } from "node:path";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import { removeBrandStanza, hasAnyBrandStanza } from "./samba-provision.js";
|
|
7
|
+
import { resolveDedicatedNeo4jTeardown } from "./neo4j-teardown.js";
|
|
8
|
+
import { KNOWN_BRAND_HOSTNAMES } from "./known-brands.js";
|
|
7
9
|
const HOME = homedir();
|
|
8
10
|
const PAYLOAD_DIR = resolve(import.meta.dirname, "../payload");
|
|
9
11
|
// Brand manifest — read from payload to derive brand-specific installation paths.
|
|
@@ -97,13 +99,6 @@ function commandExists(cmd) {
|
|
|
97
99
|
export function isMaxyInstalled() {
|
|
98
100
|
return existsSync(INSTALL_DIR);
|
|
99
101
|
}
|
|
100
|
-
/** Known brand hostnames in the Maxy ecosystem. Each brand ships a main unit
|
|
101
|
-
* (`<hostname>.service`) and a per-brand edge unit
|
|
102
|
-
* (`<hostname>-edge.service`). Peer detection matches only these filenames, not any stray
|
|
103
|
-
* `.service` file — stale units, gnome-keyring disable markers, and unrelated
|
|
104
|
-
* user services are not peer evidence. When a third brand is
|
|
105
|
-
* added under `brands/`, append its hostname here. */
|
|
106
|
-
const KNOWN_BRAND_HOSTNAMES = ["maxy", "maxy-code", "realagent", "realagent-code", "maxy-2", "maxy-3", "maxy-4"];
|
|
107
102
|
/** Detect whether another brand is installed on this device.
|
|
108
103
|
* device-wide steps (apt package purge, Ollama binary removal, apt
|
|
109
104
|
* repo cleanup, ~/.claude / ~/.ollama wipes) must skip when a peer brand is
|
|
@@ -166,20 +161,26 @@ function stopServices() {
|
|
|
166
161
|
catch {
|
|
167
162
|
console.log(` ${edgeUnitShort} not running`);
|
|
168
163
|
}
|
|
169
|
-
// Stop Neo4j
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
164
|
+
// Stop Neo4j. A dedicated brand's instance is stopped AND disabled here so it
|
|
165
|
+
// does not return at the next boot; the unit file and data/config/log dirs are
|
|
166
|
+
// removed in step 5 (removeNeo4jData). Hard guard: when the brand is dedicated
|
|
167
|
+
// the target is always `neo4j-<hostname>` — the shared `neo4j`/7687 instance is
|
|
168
|
+
// never stopped, even if `.env` is unreadable (dedicatedNeo4jTeardown falls
|
|
169
|
+
// back to brand.json, never to the shared service). A genuinely shared brand
|
|
170
|
+
// (port 7687 / no dedicated port) stops the shared `neo4j` as before.
|
|
171
|
+
const neo4jTeardown = dedicatedNeo4jTeardown();
|
|
172
|
+
if (neo4jTeardown) {
|
|
173
|
+
if (privilegedSystemctl("stop", neo4jTeardown.service)) {
|
|
174
|
+
console.log(` Stopped ${neo4jTeardown.service}`);
|
|
175
|
+
}
|
|
176
|
+
if (privilegedSystemctl("disable", neo4jTeardown.service)) {
|
|
177
|
+
console.log(` Disabled ${neo4jTeardown.service}`);
|
|
178
|
+
}
|
|
180
179
|
}
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
else {
|
|
181
|
+
if (privilegedSystemctl("stop", "neo4j")) {
|
|
182
|
+
console.log(" Stopped neo4j (shared instance)");
|
|
183
|
+
}
|
|
183
184
|
}
|
|
184
185
|
// Stop the cloudflared service for this brand (Task 757 — the connector now
|
|
185
186
|
// runs as a supervised cloudflared-<brand>.service, not a transient scope).
|
|
@@ -438,22 +439,36 @@ function removeNeo4jData() {
|
|
|
438
439
|
// 7687 data is skipped entirely when a peer brand is present. Dedicated
|
|
439
440
|
// branded instances live at /var/lib/neo4j-<hostname>/ and are always
|
|
440
441
|
// this-brand-owned by construction.
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
442
|
+
const teardown = dedicatedNeo4jTeardown();
|
|
443
|
+
if (teardown) {
|
|
444
|
+
// Full decommission of the dedicated instance. Stop + disable already ran
|
|
445
|
+
// in step 1 (stopServices); here we remove every artifact the installer's
|
|
446
|
+
// setupDedicatedNeo4j() created — unit file, config dir, data dir, log dir —
|
|
447
|
+
// then daemon-reload so systemd forgets the unit. Removing the whole data
|
|
448
|
+
// dir (not just data/) also clears the install's plugins/ and import/ trees;
|
|
449
|
+
// the log dir is /var/log/neo4j-<hostname> (server.directories.logs), not a
|
|
450
|
+
// subdir of the data dir.
|
|
451
|
+
const paths = [teardown.unitFile, teardown.confDir, teardown.dataDir, teardown.logDir];
|
|
452
|
+
for (const p of paths) {
|
|
453
|
+
if (existsSync(p)) {
|
|
454
|
+
try {
|
|
455
|
+
shell("rm", ["-rf", p], { sudo: true });
|
|
456
|
+
console.log(` Removed ${p}`);
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
console.log(` Failed to remove ${p}: ${err instanceof Error ? err.message : String(err)}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
privilegedSystemctl("daemon-reload");
|
|
446
464
|
return;
|
|
447
465
|
}
|
|
448
|
-
|
|
449
|
-
if (
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
// neo4j-<hostname>.service. Shared instance owns neither.
|
|
453
|
-
paths.push(`/etc/neo4j-${BRAND.hostname}`);
|
|
454
|
-
paths.push(`/etc/systemd/system/neo4j-${BRAND.hostname}.service`);
|
|
466
|
+
// Shared instance on 7687 — skip entirely when a peer brand still depends on it.
|
|
467
|
+
if (peerBrandPresent()) {
|
|
468
|
+
console.log(` Shared Neo4j instance on 7687 — peer brand present, skipping data wipe.`);
|
|
469
|
+
return;
|
|
455
470
|
}
|
|
456
|
-
for (const p of
|
|
471
|
+
for (const p of ["/var/lib/neo4j/data", "/var/lib/neo4j/logs"]) {
|
|
457
472
|
if (existsSync(p)) {
|
|
458
473
|
try {
|
|
459
474
|
shell("rm", ["-rf", p], { sudo: true });
|
|
@@ -464,15 +479,9 @@ function removeNeo4jData() {
|
|
|
464
479
|
}
|
|
465
480
|
}
|
|
466
481
|
}
|
|
467
|
-
if (isDedicated) {
|
|
468
|
-
try {
|
|
469
|
-
spawnSync("sudo", ["systemctl", "daemon-reload"], { stdio: "pipe" });
|
|
470
|
-
}
|
|
471
|
-
catch { /* ignore */ }
|
|
472
|
-
}
|
|
473
482
|
}
|
|
474
483
|
/** Read NEO4J_URI port from this brand's .env. Returns undefined when the
|
|
475
|
-
* file is missing or malformed — caller
|
|
484
|
+
* file is missing or malformed — caller folds in the brand.json fallback. */
|
|
476
485
|
function readNeo4jPortFromEnv() {
|
|
477
486
|
const envPath = join(CONFIG_DIR, ".env");
|
|
478
487
|
if (!existsSync(envPath))
|
|
@@ -488,6 +497,46 @@ function readNeo4jPortFromEnv() {
|
|
|
488
497
|
return undefined;
|
|
489
498
|
}
|
|
490
499
|
}
|
|
500
|
+
/** Resolve this brand's dedicated-Neo4j teardown plan, or null when the brand
|
|
501
|
+
* runs the shared instance. The port is `.env` (the actual running config,
|
|
502
|
+
* preserving the installer's own port priority — this is where a `--neo4j-port`
|
|
503
|
+
* override lands) with brand.json as the robust fallback. The result is
|
|
504
|
+
* memoised on first call: stopServices (step 1) is the first caller and runs
|
|
505
|
+
* while `~/<configDir>/.env` still exists; removeAppDirs (step 4) deletes that
|
|
506
|
+
* `.env` before removeNeo4jData (step 5) calls again, so without the cache the
|
|
507
|
+
* two call sites could classify dedication differently (step 1 sees the
|
|
508
|
+
* override port, step 5 sees only brand.json) and orphan the dedicated dirs.
|
|
509
|
+
* Caching the first (env-informed) resolution keeps stop/disable and dir
|
|
510
|
+
* removal targeting the same instance. A read failure can never fall through
|
|
511
|
+
* to the shared `neo4j`/7687 service. Dedicated names are `neo4j-<hostname>`,
|
|
512
|
+
* matching `setupDedicatedNeo4j()`. */
|
|
513
|
+
let _dedicatedNeo4jTeardownResolved = false;
|
|
514
|
+
let _dedicatedNeo4jTeardownPlan = null;
|
|
515
|
+
function dedicatedNeo4jTeardown() {
|
|
516
|
+
if (_dedicatedNeo4jTeardownResolved)
|
|
517
|
+
return _dedicatedNeo4jTeardownPlan;
|
|
518
|
+
const neo4jPort = readNeo4jPortFromEnv() ?? BRAND.neo4jPort;
|
|
519
|
+
_dedicatedNeo4jTeardownPlan = resolveDedicatedNeo4jTeardown({ brandHostname: BRAND.hostname, neo4jPort });
|
|
520
|
+
_dedicatedNeo4jTeardownResolved = true;
|
|
521
|
+
return _dedicatedNeo4jTeardownPlan;
|
|
522
|
+
}
|
|
523
|
+
/** Run a privileged `systemctl <args...>` interactively so a password prompt is
|
|
524
|
+
* visible (the silent `stdio:"pipe"` no-op is the defect being fixed). Exit 5
|
|
525
|
+
* (no such unit) is an acceptable end-state — the unit is already absent, which
|
|
526
|
+
* keeps re-runs idempotent. Any other non-zero is a genuine failure (e.g. sudo
|
|
527
|
+
* auth declined) and is surfaced loudly rather than swallowed. Returns true on
|
|
528
|
+
* success or "already absent". Cannot delegate to shell(): shell() throws on
|
|
529
|
+
* any non-zero, which would abort the uninstall on an already-absent unit. */
|
|
530
|
+
function privilegedSystemctl(...args) {
|
|
531
|
+
const result = spawnSync("sudo", ["systemctl", ...args], { stdio: "inherit", timeout: 30_000 });
|
|
532
|
+
if (result.status === 0)
|
|
533
|
+
return true;
|
|
534
|
+
if (result.status === 5)
|
|
535
|
+
return true; // no such unit — already gone
|
|
536
|
+
const reason = result.signal ? `signal ${result.signal}` : `exit ${result.status}`;
|
|
537
|
+
console.error(` FAILED: sudo systemctl ${args.join(" ")} (${reason}) — privileged step did not complete`);
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
491
540
|
// ---------------------------------------------------------------------------
|
|
492
541
|
// Step 6: Purge system packages
|
|
493
542
|
// ---------------------------------------------------------------------------
|
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:f55a0f2ad57f4878b55fa1f2359de86050c9c88e61e16e9fe8d84bcb904b30b9
|
|
5
5
|
brand: sitedesk-code
|
|
6
6
|
product-name: SiteDesk
|
|
7
7
|
---
|
|
@@ -2334,6 +2334,8 @@ either is a regression.
|
|
|
2334
2334
|
|
|
2335
2335
|
**`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp reader (`/whatsapp`) omits the prop and keeps the default `renderTimeline`, so its rendering is unchanged; both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
|
|
2336
2336
|
|
|
2337
|
+
**`/chat` live activity line.** While a turn is in flight the transcript tail shows one ephemeral `ChatActivity` line (mounted only while `busy`, admin/operator only). It tracks real activity, not a timer: while a `Task` subagent runs it shows that subagent's headline (`agentType · description`, prefixed `N agents ·` when ≥2 are concurrent), sourced from `agent-<hex>.meta.json` via named `activity` SSE events the admin reader pushes from the session's `subagents/` dir; with no subagent active it shows a neutral word that advances only on a real turn arrival. The line carries a turn-elapsed clock and flips to a `stalled` state once nothing has been written for 5 minutes (`now − lastEmitAt`), so the operator can tell a wedge from progress without SSH. It is never added to the persisted timeline. Detail and the `[webchat-activity]` observability live in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
|
|
2338
|
+
|
|
2337
2339
|
**maxy title for public sessions.** A `role=public` webchat spawn never produces a useful Claude Code `ai-title` (an anonymous one-line visitor turn), so every public row would otherwise read identically. The webchat route (`chat.ts`) composes a deterministic title — `Web · <senderId[:8]>[ · <personId>] · <UTC YYYY-MM-DD HH:mm>` (personId present only for gated visitors) — and threads it through the native webchat gateway's public spawn (`handleInbound` → `buildPublicWebchatSpawnRequest` → `managerSpawn`) to the manager `POST /public-spawn` body as `name`. `/spawn` validates it with `validateUserTitle` and, for `role=public` only, writes it into `UserTitleStore` so it occupies the operator-rename tier and wins over `ai-title`. Admin (`/rc-spawn`) and WhatsApp titling are unchanged. Observability: every public spawn logs `[spawn] role=public … title="…"` (or `title=missing`); the manager's row builder emits `[public-title] sessionId=… unexpected titleSource=<ai|null>` on the next list read for any public row that did not resolve from the user tier.
|
|
2338
2340
|
|
|
2339
2341
|
**Public visitor surface.** The public-host root (`GET /`), the `/:slug` agent routes, and the admin-host `/public` / `/public-chat` previews all serve one shell, `public.html` → [`app/public-entry.tsx`](../../../ui/app/public-entry.tsx) → `PublicChat`. `PublicChat` reuses `useSession` purely as the magic-link gatekeeper — it drives `AccessGate`, resolves the agent slug from the path, and handles `?token=` verification; once the grant is satisfied it mounts `ChatSurface variant="public"`, whose transcript reads the visitor-scoped, **delivered-only** stream `GET /api/public-reader/stream` ([`server/routes/public-reader.ts`](../../../ui/server/routes/public-reader.ts)) — the visitor sees the agent's delivered prose, never the tool/tool-result/directive bytes the legacy `/api/chat` SSE render exposed. The human visitor's branding is resolved client-side by `useSession`. For the head only, the server injects per-agent link-preview meta — `<title>`, `og:title`/`og:description`/`og:image`, `theme-color`, and a per-agent favicon, resolved from the same branding cache `useSession` reads — into the served shell, so a link-preview crawler (which never runs the client bundle) gets a branded card per agent instead of one generic shell; an agent with no branding cache gets the clean brand-default shell with no empty meta tags. There is exactly one public client surface and it is 1:1 visitor↔agent: the earlier `?surface=next` A/B handle is retired, group messaging is retired (not a supported product surface), and a former `/g/<slug>` group URL now serves the same 1:1 shell — a stale bookmark gets the 1:1 chat, never a broken render.
|
|
@@ -69,6 +69,8 @@ either is a regression.
|
|
|
69
69
|
|
|
70
70
|
**`/chat` Claude-desktop transcript presentation.** The admin webchat keeps the shared `Transcript` shell (stream, follow-tail) but injects a /chat-only item renderer via the component's optional `renderItems` prop: `renderChatTimeline` in `app/chat/transcript-render.tsx`. Presentation: operator turns render as a right-aligned grey bubble showing the message text and, beneath it, a subtle always-visible time-of-day stamp (no label); delivered agent replies render as plain prose with the same time-of-day stamp beneath (the reply-document filename line stays, as prose); the stamp is HH:MM from the turn's `ts` (locale-formatted, right-aligned inside the operator bubble, left-aligned under agent prose), and a turn whose `ts` is null/unparseable shows no stamp, never an empty line — so only delivered operator and agent-reply turns are stamped, while tool runs, the collapsed "Thinking" block, and the agent-error banner stay time-free; a maximal consecutive run of `tool-call`/`tool-result` turns renders as one collapsed grey one-liner ("Used N tools ›") whose expansion shows every call and result payload — a lone call still gets the one-liner, and prose or a directive row ends a run. The WhatsApp reader (`/whatsapp`) omits the prop and keeps the default `renderTimeline`, so its rendering is unchanged; both presentations are test-pinned (`app/whatsapp/__tests__/Transcript-*.test.tsx`, `app/chat/__tests__/transcript-render.test.tsx`). **Day-divider:** both renderers insert a centered `.day-divider` row between two consecutive timeline items on different local calendar days (label `Today`/`Yesterday`, else `Sat 14 Jun 2026`), so a thread spanning midnight is never an ambiguous run of HH:MM; the first dated item gets a leading divider, a null/unparseable `ts` marks no boundary, dividers are computed over the filtered `visibleItems`, and in `/chat` a day crossover flushes any open tool/think run first so a collapsed run never spans it. Per-bubble HH:MM stamps are unchanged. Shared helpers `dayKey`/`dayLabel`/`itemTs`/`DayDivider` live in `app/whatsapp/Transcript.tsx`.
|
|
71
71
|
|
|
72
|
+
**`/chat` live activity line.** While a turn is in flight the transcript tail shows one ephemeral `ChatActivity` line (mounted only while `busy`, admin/operator only). It tracks real activity, not a timer: while a `Task` subagent runs it shows that subagent's headline (`agentType · description`, prefixed `N agents ·` when ≥2 are concurrent), sourced from `agent-<hex>.meta.json` via named `activity` SSE events the admin reader pushes from the session's `subagents/` dir; with no subagent active it shows a neutral word that advances only on a real turn arrival. The line carries a turn-elapsed clock and flips to a `stalled` state once nothing has been written for 5 minutes (`now − lastEmitAt`), so the operator can tell a wedge from progress without SSH. It is never added to the persisted timeline. Detail and the `[webchat-activity]` observability live in [`admin-webchat-native-channel.md`](../../../.docs/admin-webchat-native-channel.md).
|
|
73
|
+
|
|
72
74
|
**maxy title for public sessions.** A `role=public` webchat spawn never produces a useful Claude Code `ai-title` (an anonymous one-line visitor turn), so every public row would otherwise read identically. The webchat route (`chat.ts`) composes a deterministic title — `Web · <senderId[:8]>[ · <personId>] · <UTC YYYY-MM-DD HH:mm>` (personId present only for gated visitors) — and threads it through the native webchat gateway's public spawn (`handleInbound` → `buildPublicWebchatSpawnRequest` → `managerSpawn`) to the manager `POST /public-spawn` body as `name`. `/spawn` validates it with `validateUserTitle` and, for `role=public` only, writes it into `UserTitleStore` so it occupies the operator-rename tier and wins over `ai-title`. Admin (`/rc-spawn`) and WhatsApp titling are unchanged. Observability: every public spawn logs `[spawn] role=public … title="…"` (or `title=missing`); the manager's row builder emits `[public-title] sessionId=… unexpected titleSource=<ai|null>` on the next list read for any public row that did not resolve from the user tier.
|
|
73
75
|
|
|
74
76
|
**Public visitor surface.** The public-host root (`GET /`), the `/:slug` agent routes, and the admin-host `/public` / `/public-chat` previews all serve one shell, `public.html` → [`app/public-entry.tsx`](../../../ui/app/public-entry.tsx) → `PublicChat`. `PublicChat` reuses `useSession` purely as the magic-link gatekeeper — it drives `AccessGate`, resolves the agent slug from the path, and handles `?token=` verification; once the grant is satisfied it mounts `ChatSurface variant="public"`, whose transcript reads the visitor-scoped, **delivered-only** stream `GET /api/public-reader/stream` ([`server/routes/public-reader.ts`](../../../ui/server/routes/public-reader.ts)) — the visitor sees the agent's delivered prose, never the tool/tool-result/directive bytes the legacy `/api/chat` SSE render exposed. The human visitor's branding is resolved client-side by `useSession`. For the head only, the server injects per-agent link-preview meta — `<title>`, `og:title`/`og:description`/`og:image`, `theme-color`, and a per-agent favicon, resolved from the same branding cache `useSession` reads — into the served shell, so a link-preview crawler (which never runs the client bundle) gets a branded card per agent instead of one generic shell; an agent with no branding cache gets the clean brand-default shell with no empty meta tags. There is exactly one public client surface and it is 1:1 visitor↔agent: the earlier `?surface=next` A/B handle is retired, group messaging is retired (not a supported product surface), and a former `/g/<slug>` group URL now serves the same 1:1 shell — a stale bookmark gets the 1:1 chat, never a broken render.
|
|
@@ -3113,6 +3113,22 @@ function renderLoginPage(opts) {
|
|
|
3113
3113
|
const displayFont = opts?.displayFont ?? "'Newsreader', Georgia, serif";
|
|
3114
3114
|
const bodyFont = opts?.bodyFont ?? "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif";
|
|
3115
3115
|
const logoContainsName = opts?.logoContainsName ?? false;
|
|
3116
|
+
const tagline = opts?.tagline ?? "";
|
|
3117
|
+
const origin = opts?.origin ?? "";
|
|
3118
|
+
const ogCardPath = opts?.ogCardPath ?? "";
|
|
3119
|
+
const ogBlock = origin && ogCardPath ? [
|
|
3120
|
+
`<meta property="og:title" content="Sign in \u2014 ${escapeHtml(productName)}">`,
|
|
3121
|
+
`<meta property="og:description" content="${escapeHtml(tagline)}">`,
|
|
3122
|
+
`<meta property="og:type" content="website">`,
|
|
3123
|
+
`<meta property="og:url" content="${escapeHtml(origin)}">`,
|
|
3124
|
+
`<meta property="og:image" content="${escapeHtml(origin + ogCardPath)}">`,
|
|
3125
|
+
`<meta property="og:image:width" content="1200">`,
|
|
3126
|
+
`<meta property="og:image:height" content="630">`,
|
|
3127
|
+
`<meta name="twitter:card" content="summary_large_image">`,
|
|
3128
|
+
`<meta name="twitter:title" content="Sign in \u2014 ${escapeHtml(productName)}">`,
|
|
3129
|
+
`<meta name="twitter:description" content="${escapeHtml(tagline)}">`,
|
|
3130
|
+
`<meta name="twitter:image" content="${escapeHtml(origin + ogCardPath)}">`
|
|
3131
|
+
].join("\n ") : "";
|
|
3116
3132
|
const errorHtml = error ? `<p class="msg msg--error">${escapeHtml(error)}</p>` : "";
|
|
3117
3133
|
const changeErrorHtml = changeError ? `<p class="msg msg--error">${escapeHtml(changeError)}</p>` : "";
|
|
3118
3134
|
const successHtml = success ? `<p class="msg msg--success">${escapeHtml(success)}</p>` : "";
|
|
@@ -3148,6 +3164,7 @@ function renderLoginPage(opts) {
|
|
|
3148
3164
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3149
3165
|
<title>Sign in \u2014 ${escapeHtml(productName)}</title>
|
|
3150
3166
|
<link rel="icon" href="${escapeHtml(faviconPath)}">
|
|
3167
|
+
${ogBlock}
|
|
3151
3168
|
${googleFontsLink}
|
|
3152
3169
|
<style>
|
|
3153
3170
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
package/payload/server/public/assets/{AdminLoginScreens-CHTeh_Vu.js → AdminLoginScreens-Brx8CmXN.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as e}from"./chunk-Pqm5yXtL.js";import{H as t,R as n,S as r,b as i,k as a,y as o}from"./OperatorConversations-RmqANYz8.js";import{i as s}from"./admin-types-CJrGd46U.js";import{g as c,u as l}from"./AdminShell-DjoP7YoA.js";import{t as u}from"./Checkbox-D58GsKoQ.js";var d=`admin-landing-redirected`,f=`/graph`;function p(e){return e.variant===`operator`?!1:e.appState===`chat`&&!e.alreadyRedirected}var m=e(t(),1);function h(e=`admin`){let[t,r]=(0,m.useState)(`loading`),[i,a]=(0,m.useState)(``),[o,l]=(0,m.useState)(``),[u,h]=(0,m.useState)(``),[g,_]=(0,m.useState)(!1),[v,y]=(0,m.useState)(!1),[b,x]=(0,m.useState)(!1),[S,C]=(0,m.useState)(!1),[w,T]=(0,m.useState)(!1),[E,D]=(0,m.useState)(null),[O,k]=(0,m.useState)(null),[A,j]=(0,m.useState)(void 0),[M,N]=(0,m.useState)(null),[P,F]=(0,m.useState)(void 0),[I,L]=(0,m.useState)(null),[ee,R]=(0,m.useState)(null),[z,B]=(0,m.useState)([]),[V,H]=(0,m.useState)(!1),[U,W]=(0,m.useState)(void 0),G=(0,m.useRef)(void 0),[K,q]=(0,m.useState)(!1);(0,m.useEffect)(()=>{typeof window>`u`||fetch(`/api/remote-auth/status`).then(e=>e.ok?e.json():null).then(e=>{e?.configured&&q(!0)}).catch(()=>{})},[]);let J=(0,m.useRef)(null),Y=(0,m.useRef)(null);(0,m.useEffect)(()=>{async function e(){let e=null;try{e=sessionStorage.getItem(`maxy-admin-session-key`)}catch{}if(!e)return!1;try{let t=await fetch(`/api/admin/session?session_key=${encodeURIComponent(e)}`);if(t.status===401){try{sessionStorage.removeItem(`maxy-admin-session-key`)}catch{}return!1}if(!t.ok)return!1;let n=await t.json();D(n.session_key),R(n.sessionId??null),j(n.businessName),N(n.role??null),F(n.userName===void 0?null:n.userName),L(n.avatar??null);let i=s(n.thinkingView);return G.current=i,W(i),r(`chat`),!0}catch(e){return console.error(`[admin] session restore failed:`,e),!1}}async function t(n=2){try{let i=await fetch(`/api/health`);if(!i.ok){if(n>0)return await new Promise(e=>setTimeout(e,1500)),t(n-1);console.error(`[admin] health check returned ${i.status} after retries`),r(`set-pin`);return}let a=await i.json();if(!a.pin_configured){r(`set-pin`);return}if(!a.claude_authenticated){r(`connect-claude`);return}if(await e())return;r(`enter-pin`)}catch(e){if(n>0)return await new Promise(e=>setTimeout(e,1500)),t(n-1);console.error(`[admin] health check failed:`,e),r(`set-pin`)}}t()},[]),(0,m.useEffect)(()=>{t===`chat`&&fetch(`/api/admin/claude-info`).then(e=>{if(e.ok)return e.json()}).then(e=>{e&&k(e)}).catch(()=>{})},[t]),(0,m.useEffect)(()=>{if(typeof window>`u`)return;let n=!1;try{n=sessionStorage.getItem(d)===`1`}catch{}if(p({appState:t,alreadyRedirected:n,variant:e})){try{sessionStorage.setItem(d,`1`)}catch{}console.info(`[admin-ui] landing-redirect target=${f}`),window.location.replace(f)}},[t,e]);let X=(0,m.useRef)(null);(0,m.useEffect)(()=>{if(t!==`chat`)return;let e=setInterval(async()=>{try{let e=await fetch(`/api/health`);if(e.ok){let t=await e.json();if(t.auth_status===`dead`||t.auth_status===`missing`){r(`connect-claude`);return}}}catch{}if(E)try{let e=await fetch(`/api/admin/session?session_key=${encodeURIComponent(E)}`);if(e.status!==401)return;let t=(await e.clone().json().catch(()=>null))?.code??`unknown-401`;if(t===`remote-auth-required`){n(`heartbeat`,`/api/admin/session`);return}console.warn(`[admin-auth] outcome=heartbeat-detected-expiry code=${t}`),X.current?.()}catch{}},300*1e3);return()=>clearInterval(e)},[t,E]),(0,m.useEffect)(()=>{t===`connect-claude`&&fetch(`/api/health`).then(e=>e.ok?e.json():null).then(e=>{e?.claude_authenticated&&r(`enter-pin`)}).catch(()=>{})},[t]);async function Z(e,t){y(!0);try{let n=await fetch(`/api/admin/session`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({pin:e,...t?{accountId:t}:{}})});if(!n.ok){h((await n.json().catch(()=>({}))).error||`Invalid PIN`);return}let i=await n.json();if(i.accounts&&!i.session_key){console.log(`[admin] account picker shown: userId=${i.userId} accountCount=${i.accounts.length}`),B(i.accounts),r(`account-picker`);return}D(i.session_key),R(i.sessionId??null),j(i.businessName),N(i.role??null),F(i.userName===void 0?null:i.userName),L(i.avatar??null);let o=s(i.thinkingView);if(G.current=o,W(o),t)try{sessionStorage.setItem(`maxy-account-id`,t)}catch{}try{sessionStorage.setItem(`maxy-admin-session-key`,i.session_key)}catch{}a(``),r(`chat`)}catch(e){console.error(`[admin] connection error:`,e),h(`Could not connect.`)}finally{y(!1),H(!1)}}let Q=(0,m.useCallback)(async e=>{if(e.preventDefault(),v)return;h(``);let t=o.trim();if(!t){h(`Please enter your name.`);return}if(i.length<4){h(`PIN must be at least 4 characters.`);return}let n=i;y(!0);try{let e=await fetch(`/api/onboarding/set-pin`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({pin:n,name:t})});if(!e.ok){let t=await e.json().catch(()=>({}));if(e.status===409){console.log(`[admin] PIN already configured — re-checking health`);try{let e=await fetch(`/api/health`);if(e.ok){let n=await e.json();n.pin_configured&&n.claude_authenticated?r(`enter-pin`):n.pin_configured?r(`connect-claude`):h(t.error||`Failed to set PIN.`)}else r(`enter-pin`)}catch{r(`enter-pin`)}return}h(t.error||`Failed to set PIN.`);return}let i=await fetch(`/api/health`);if((i.ok?await i.json():null)?.claude_authenticated){await Z(n);return}a(``),r(`connect-claude`)}catch(e){console.error(`[admin] connection error:`,e),h(`Could not connect.`)}finally{y(!1)}},[i,v,o]),te=(0,m.useCallback)(async e=>{e.preventDefault(),h(``),await Z(i)},[i]),ne=(0,m.useCallback)(async()=>{T(!0);try{if(!await c())return console.warn(`[admin-ui] claude-disconnect not verified — credentials may persist; staying put`),!1;D(null),N(null),F(void 0),L(null);try{sessionStorage.removeItem(`maxy-admin-session-key`),sessionStorage.removeItem(`maxy-account-id`),sessionStorage.removeItem(d)}catch{}return r(`connect-claude`),!0}finally{T(!1)}},[]),$=(0,m.useCallback)(()=>{D(null),N(null),F(void 0),L(null);try{sessionStorage.removeItem(`maxy-admin-session-key`),sessionStorage.removeItem(`maxy-account-id`),sessionStorage.removeItem(d)}catch{}a(``),h(``),r(`enter-pin`)},[]);return(0,m.useEffect)(()=>{X.current=$},[$]),{appState:t,setAppState:r,pin:i,setPin:a,operatorName:o,setOperatorName:l,pinError:u,setPinError:h,showPin:g,setShowPin:_,pinLoading:v,authPolling:b,setAuthPolling:x,authLoading:S,setAuthLoading:C,disconnecting:w,cacheKey:E,setCacheKey:D,claudeInfo:O,setClaudeInfo:k,businessName:A,role:M,userName:P,userAvatar:I,sessionId:ee,setSessionId:R,accounts:z,accountPickerLoading:V,expandAll:U,setExpandAll:W,expandAllDefaultRef:G,remoteAuthEnabled:K,pinInputRef:J,setPinFormRef:Y,handleSetPin:Q,handleLogin:te,handleAccountSelect:(0,m.useCallback)(async e=>{H(!0),h(``),await Z(i,e)},[i]),handleDisconnect:ne,handleLogout:$,handleChangePin:(0,m.useCallback)(async()=>{if(!i){h(`Enter your current PIN first.`);return}y(!0),h(``);try{let e=await fetch(`/api/onboarding/set-pin`,{method:`DELETE`,headers:{"Content-Type":`application/json`},body:JSON.stringify({currentPin:i})});if(!e.ok){h((await e.json().catch(()=>({error:`Incorrect PIN.`}))).error||`Incorrect PIN.`);return}a(``),h(``),r(`set-pin`)}catch(e){console.error(`[admin-auth] change pin failed:`,e),h(e instanceof Error?e.message:String(e))}finally{y(!1)}},[i])}}var g=r();function _({inputRef:e,value:t,onChange:n,onComplete:r,showPin:i,autoFocus:a}){let o=(0,m.useRef)([]);function s(e,r){r.key===`Backspace`?(r.preventDefault(),t[e]?n(t.slice(0,e)+t.slice(e+1)):e>0&&(n(t.slice(0,e-1)+t.slice(e)),o.current[e-1]?.focus())):r.key===`ArrowLeft`&&e>0?o.current[e-1]?.focus():r.key===`ArrowRight`&&e<5?o.current[e+1]?.focus():r.key===`Enter`&&(r.preventDefault(),r.currentTarget.form?.requestSubmit())}function c(e,i){let a=i.nativeEvent.data;if(!a||!/^\d$/.test(a))return;let s=t.split(``);for(s[e]=a;s.length<e;)s.push(``);let c=s.join(``).replace(/\D/g,``).slice(0,6);n(c),c.length===6?r?.(c):e<5&&o.current[e+1]?.focus()}function l(e){e.preventDefault();let t=e.clipboardData.getData(`text`).replace(/\D/g,``).slice(0,6);t&&(n(t),t.length===6?r?.(t):o.current[t.length]?.focus())}return(0,g.jsx)(`div`,{className:`pin-field`,children:Array.from({length:6}).map((n,r)=>(0,g.jsx)(`input`,{ref:t=>{o.current[r]=t,r===0&&e&&(e.current=t)},type:`text`,inputMode:`numeric`,className:`pin-box${t[r]?` pin-box-filled`:``}`,value:t[r]?i?t[r]:`•`:``,onKeyDown:e=>s(r,e),onInput:e=>c(r,e),onPaste:l,onFocus:e=>e.target.select(),autoFocus:a&&r===0,autoComplete:`off`,maxLength:1,"aria-label":`PIN digit ${r+1}`},r))})}function v(e){let{pin:t,setPin:n,showPin:r,setShowPin:a,pinLoading:s,pinError:c,pinInputRef:d,setPinFormRef:f,onSubmit:p,operatorName:m,setOperatorName:h}=e;return(0,g.jsx)(`div`,{className:`connect-page`,children:(0,g.jsxs)(`div`,{className:`connect-content`,children:[(0,g.jsx)(`img`,{src:i,alt:o.productName,className:`connect-logo connect-logo--maxy`}),!o.logoContainsName&&(0,g.jsxs)(`h1`,{className:`connect-title`,children:[`Welcome to `,o.productName]}),(0,g.jsxs)(`p`,{className:`connect-subtitle`,children:[`Tell `,o.productName,` who you are, then choose a PIN.`]}),(0,g.jsxs)(`form`,{ref:f,onSubmit:p,className:`connect-pin-form`,children:[(0,g.jsxs)(`div`,{className:`pin-input-row`,children:[(0,g.jsx)(`input`,{type:`text`,className:`connect-name-input`,placeholder:`Your full name`,value:m,onChange:e=>h(e.target.value),autoComplete:`name`,autoFocus:!0,required:!0,"aria-label":`Your full name`}),(0,g.jsx)(`div`,{style:{width:38,flexShrink:0},"aria-hidden":`true`})]}),(0,g.jsxs)(`div`,{className:`pin-input-row`,children:[(0,g.jsx)(_,{inputRef:d,value:t,onChange:n,onComplete:()=>{},showPin:r}),(0,g.jsx)(l,{variant:`send`,type:`submit`,disabled:!t||!m.trim(),loading:s,"aria-label":`Set PIN`,children:(0,g.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,g.jsx)(`line`,{x1:`5`,y1:`12`,x2:`19`,y2:`12`}),(0,g.jsx)(`polyline`,{points:`12 5 19 12 12 19`})]})})]}),(0,g.jsx)(u,{checked:r,onChange:()=>a(e=>!e),label:`Show PIN`})]}),c&&(0,g.jsx)(`p`,{className:`admin-pin-error`,children:c})]})})}function y(e){let{pin:t,setPin:n,showPin:r,setShowPin:a,pinLoading:s,pinError:c,pinInputRef:d,onSubmit:f,onChangePin:p,remoteAuthEnabled:m,onSignOutRemote:h}=e;return(0,g.jsxs)(`div`,{className:`connect-page`,children:[m&&h&&(0,g.jsx)(`button`,{type:`button`,className:`connect-signout`,onClick:h,children:`Sign out`}),(0,g.jsxs)(`div`,{className:`connect-content`,children:[(0,g.jsx)(`img`,{src:i,alt:o.productName,className:`connect-logo connect-logo--maxy`}),!o.logoContainsName&&(0,g.jsx)(`h1`,{className:`connect-title`,children:o.productName}),(0,g.jsxs)(`form`,{onSubmit:f,className:`connect-pin-form`,children:[(0,g.jsxs)(`div`,{className:`pin-input-row`,children:[(0,g.jsx)(_,{inputRef:d,value:t,onChange:n,onComplete:()=>{},showPin:r,autoFocus:!0}),(0,g.jsx)(l,{variant:`send`,type:`submit`,disabled:!t,loading:s,children:(0,g.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,g.jsx)(`line`,{x1:`5`,y1:`12`,x2:`19`,y2:`12`}),(0,g.jsx)(`polyline`,{points:`12 5 19 12 12 19`})]})})]}),(0,g.jsxs)(`div`,{className:`pin-options`,children:[(0,g.jsx)(u,{checked:r,onChange:()=>a(e=>!e),label:`Show PIN`}),(0,g.jsx)(l,{type:`button`,variant:`ghost`,onClick:p,children:`Change PIN`})]})]}),c&&(0,g.jsx)(`p`,{className:`admin-pin-error`,children:c})]})]})}function b(e){let{accounts:t,loading:n,error:r,onSelect:s}=e;return(0,g.jsx)(`div`,{className:`connect-page`,children:(0,g.jsxs)(`div`,{className:`connect-content`,children:[(0,g.jsx)(`img`,{src:i,alt:o.productName,className:`connect-logo connect-logo--maxy`}),!o.logoContainsName&&(0,g.jsx)(`h1`,{className:`connect-title`,children:o.productName}),(0,g.jsx)(`p`,{className:`connect-subtitle`,children:`Select an account`}),(0,g.jsx)(`div`,{className:`account-picker-list`,children:t.map(e=>(0,g.jsxs)(`button`,{className:`account-picker-card`,onClick:()=>s(e.accountId),disabled:n,type:`button`,children:[(0,g.jsx)(`span`,{className:`account-picker-name`,children:e.businessName||e.accountId}),(0,g.jsx)(`span`,{className:`account-picker-role`,children:e.role}),n&&(0,g.jsx)(a,{className:`account-picker-spinner`,size:16})]},e.accountId))}),r&&(0,g.jsx)(`p`,{className:`admin-pin-error`,children:r})]})})}function x(e){let{authPolling:t,setAuthPolling:n,authLoading:r,setAuthLoading:a,pinError:s,setPinError:c,setAppState:u}=e,[d,f]=(0,m.useState)(!1),[p,h]=(0,m.useState)(!1);async function _(){h(!0),c(``);try{let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`launch-browser`})})).json();e.launched?f(!0):e.error&&c(e.error)}catch(e){console.error(`[admin] browser launch error:`,e),c(`Could not launch browser.`)}h(!1)}async function v(){a(!0),c(``);try{let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`})).json();if(e.started){n(!0),f(!0),a(!1);for(let e=0;e<120;e++)if(await new Promise(e=>setTimeout(e,2e3)),(await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`wait`})})).json()).authenticated){await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`stop`})}),u(`enter-pin`);return}c(`Timed out waiting for sign-in. Try again.`),n(!1)}else e.error&&c(e.error)}catch(e){console.error(`[admin] auth flow error:`,e),c(`Could not start auth flow.`)}a(!1)}async function y(){await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`stop`})}),n(!1),c(``)}return t||d?(0,g.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,height:`100dvh`,overflow:`auto`},children:[(0,g.jsxs)(`header`,{className:`chat-header`,style:{paddingBottom:`12px`,flexShrink:0,position:`relative`,maxWidth:`680px`,width:`100%`,margin:`0 auto`,padding:`24px 20px 12px`},children:[t?(0,g.jsx)(`button`,{onClick:y,style:{position:`absolute`,top:`12px`,right:`12px`,background:`none`,border:`none`,color:`#999`,fontSize:`13px`,cursor:`pointer`,padding:`4px 8px`},"aria-label":`Cancel`,children:`✕`}):(0,g.jsx)(`button`,{onClick:()=>f(!1),style:{position:`absolute`,top:`12px`,right:`12px`,background:`none`,border:`none`,color:`#999`,fontSize:`13px`,cursor:`pointer`,padding:`4px 8px`},"aria-label":`Close browser`,children:`✕`}),(0,g.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`chat-logo`}),(0,g.jsx)(`h1`,{className:`chat-tagline`,children:`Connect Claude`}),(0,g.jsx)(`p`,{className:`chat-intro`,children:t?`Sign in and authorize in the browser below.`:`Open your email or prepare your accounts, then sign in.`}),!t&&(0,g.jsx)(`div`,{style:{marginTop:`12px`},children:(0,g.jsx)(l,{variant:`primary`,onClick:v,disabled:r,children:r?(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`span`,{className:`spin`,style:{display:`inline-block`},children:`✱`}),` Connecting…`]}):`Sign in to Claude`})})]}),(0,g.jsx)(`div`,{style:{flex:1,display:`flex`,flexDirection:`column`,minHeight:0,gap:`10px`,padding:`0 0 16px`},children:(0,g.jsx)(`iframe`,{src:`/vnc-viewer.html`,style:{flex:1,width:`100%`,minHeight:0,border:`none`,background:`#111`,display:`block`},title:`Claude Sign-in`})}),s&&(0,g.jsx)(`p`,{className:`admin-pin-error`,style:{textAlign:`center`,padding:`0 20px 16px`},children:s})]}):(0,g.jsx)(`div`,{className:`connect-page`,children:(0,g.jsxs)(`div`,{className:`connect-content`,children:[(0,g.jsxs)(`div`,{className:`connect-logos`,children:[(0,g.jsx)(`div`,{className:`connect-logo-wrap`,children:(0,g.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`connect-logo`})}),(0,g.jsx)(`svg`,{className:`connect-arrow`,viewBox:`0 0 48 24`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:(0,g.jsx)(`path`,{d:`M0 12h44m0 0l-8-8m8 8l-8 8`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`})}),(0,g.jsxs)(`div`,{className:`connect-logo-wrap`,children:[(0,g.jsx)(`img`,{src:i,alt:o.productName,className:`connect-logo connect-logo--maxy`}),!o.logoContainsName&&(0,g.jsx)(`span`,{className:`connect-logo-label`,children:o.productName})]})]}),(0,g.jsxs)(`h1`,{className:`connect-title`,children:[`Connect Claude to power `,o.productName]}),(0,g.jsx)(`p`,{className:`connect-subtitle`,children:`Sign in with your Anthropic account to get started.`}),(0,g.jsx)(l,{variant:`primary`,onClick:v,disabled:r,children:r?(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`span`,{className:`spin`,style:{display:`inline-block`},children:`✱`}),` Connecting…`]}):`Sign in to Claude`}),(0,g.jsx)(`p`,{style:{marginTop:`6px`,fontSize:`11px`,color:`#999`,maxWidth:`300px`,textAlign:`center`,lineHeight:`1.4`},children:`First time? You may need to sign into your email and Anthropic account in the browser before connecting.`}),(0,g.jsx)(`button`,{onClick:_,disabled:p,style:{marginTop:`12px`,background:`none`,border:`none`,color:`var(--color-primary, #666)`,fontSize:`13px`,cursor:`pointer`,textDecoration:`underline`,textUnderlineOffset:`3px`},children:p?`Launching…`:`Open browser first`}),s&&(0,g.jsx)(`p`,{className:`admin-pin-error`,children:s})]})})}function S({auth:e}){return e.appState===`loading`?(0,g.jsx)(`div`,{className:`connect-page`}):e.appState===`set-pin`?(0,g.jsx)(v,{pin:e.pin,setPin:e.setPin,showPin:e.showPin,setShowPin:e.setShowPin,pinLoading:e.pinLoading,pinError:e.pinError,pinInputRef:e.pinInputRef,setPinFormRef:e.setPinFormRef,onSubmit:e.handleSetPin,operatorName:e.operatorName,setOperatorName:e.setOperatorName}):e.appState===`connect-claude`?(0,g.jsx)(x,{authPolling:e.authPolling,setAuthPolling:e.setAuthPolling,authLoading:e.authLoading,setAuthLoading:e.setAuthLoading,pinError:e.pinError,setPinError:e.setPinError,setAppState:e.setAppState}):e.appState===`enter-pin`?(0,g.jsx)(y,{pin:e.pin,setPin:e.setPin,showPin:e.showPin,setShowPin:e.setShowPin,pinLoading:e.pinLoading,pinError:e.pinError,pinInputRef:e.pinInputRef,onSubmit:e.handleLogin,onChangePin:e.handleChangePin,remoteAuthEnabled:e.remoteAuthEnabled,onSignOutRemote:()=>{console.info(`[admin-ui] remote-auth sign-out → /__remote-auth/logout`),window.location.href=`/__remote-auth/logout`}}):e.appState===`account-picker`?(0,g.jsx)(b,{accounts:e.accounts,loading:e.accountPickerLoading,error:e.pinError,onSelect:e.handleAccountSelect}):null}export{h as n,S as t};
|
|
1
|
+
import{o as e}from"./chunk-Pqm5yXtL.js";import{H as t,R as n,S as r,b as i,k as a,y as o}from"./OperatorConversations-DpjPPIOp.js";import{i as s}from"./admin-types-CJrGd46U.js";import{g as c,u as l}from"./AdminShell-CHZMDX2u.js";import{t as u}from"./Checkbox-aePjWzRH.js";var d=`admin-landing-redirected`,f=`/graph`;function p(e){return e.variant===`operator`?!1:e.appState===`chat`&&!e.alreadyRedirected}var m=e(t(),1);function h(e=`admin`){let[t,r]=(0,m.useState)(`loading`),[i,a]=(0,m.useState)(``),[o,l]=(0,m.useState)(``),[u,h]=(0,m.useState)(``),[g,_]=(0,m.useState)(!1),[v,y]=(0,m.useState)(!1),[b,x]=(0,m.useState)(!1),[S,C]=(0,m.useState)(!1),[w,T]=(0,m.useState)(!1),[E,D]=(0,m.useState)(null),[O,k]=(0,m.useState)(null),[A,j]=(0,m.useState)(void 0),[M,N]=(0,m.useState)(null),[P,F]=(0,m.useState)(void 0),[I,L]=(0,m.useState)(null),[ee,R]=(0,m.useState)(null),[z,B]=(0,m.useState)([]),[V,H]=(0,m.useState)(!1),[U,W]=(0,m.useState)(void 0),G=(0,m.useRef)(void 0),[K,q]=(0,m.useState)(!1);(0,m.useEffect)(()=>{typeof window>`u`||fetch(`/api/remote-auth/status`).then(e=>e.ok?e.json():null).then(e=>{e?.configured&&q(!0)}).catch(()=>{})},[]);let J=(0,m.useRef)(null),Y=(0,m.useRef)(null);(0,m.useEffect)(()=>{async function e(){let e=null;try{e=sessionStorage.getItem(`maxy-admin-session-key`)}catch{}if(!e)return!1;try{let t=await fetch(`/api/admin/session?session_key=${encodeURIComponent(e)}`);if(t.status===401){try{sessionStorage.removeItem(`maxy-admin-session-key`)}catch{}return!1}if(!t.ok)return!1;let n=await t.json();D(n.session_key),R(n.sessionId??null),j(n.businessName),N(n.role??null),F(n.userName===void 0?null:n.userName),L(n.avatar??null);let i=s(n.thinkingView);return G.current=i,W(i),r(`chat`),!0}catch(e){return console.error(`[admin] session restore failed:`,e),!1}}async function t(n=2){try{let i=await fetch(`/api/health`);if(!i.ok){if(n>0)return await new Promise(e=>setTimeout(e,1500)),t(n-1);console.error(`[admin] health check returned ${i.status} after retries`),r(`set-pin`);return}let a=await i.json();if(!a.pin_configured){r(`set-pin`);return}if(!a.claude_authenticated){r(`connect-claude`);return}if(await e())return;r(`enter-pin`)}catch(e){if(n>0)return await new Promise(e=>setTimeout(e,1500)),t(n-1);console.error(`[admin] health check failed:`,e),r(`set-pin`)}}t()},[]),(0,m.useEffect)(()=>{t===`chat`&&fetch(`/api/admin/claude-info`).then(e=>{if(e.ok)return e.json()}).then(e=>{e&&k(e)}).catch(()=>{})},[t]),(0,m.useEffect)(()=>{if(typeof window>`u`)return;let n=!1;try{n=sessionStorage.getItem(d)===`1`}catch{}if(p({appState:t,alreadyRedirected:n,variant:e})){try{sessionStorage.setItem(d,`1`)}catch{}console.info(`[admin-ui] landing-redirect target=${f}`),window.location.replace(f)}},[t,e]);let X=(0,m.useRef)(null);(0,m.useEffect)(()=>{if(t!==`chat`)return;let e=setInterval(async()=>{try{let e=await fetch(`/api/health`);if(e.ok){let t=await e.json();if(t.auth_status===`dead`||t.auth_status===`missing`){r(`connect-claude`);return}}}catch{}if(E)try{let e=await fetch(`/api/admin/session?session_key=${encodeURIComponent(E)}`);if(e.status!==401)return;let t=(await e.clone().json().catch(()=>null))?.code??`unknown-401`;if(t===`remote-auth-required`){n(`heartbeat`,`/api/admin/session`);return}console.warn(`[admin-auth] outcome=heartbeat-detected-expiry code=${t}`),X.current?.()}catch{}},300*1e3);return()=>clearInterval(e)},[t,E]),(0,m.useEffect)(()=>{t===`connect-claude`&&fetch(`/api/health`).then(e=>e.ok?e.json():null).then(e=>{e?.claude_authenticated&&r(`enter-pin`)}).catch(()=>{})},[t]);async function Z(e,t){y(!0);try{let n=await fetch(`/api/admin/session`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({pin:e,...t?{accountId:t}:{}})});if(!n.ok){h((await n.json().catch(()=>({}))).error||`Invalid PIN`);return}let i=await n.json();if(i.accounts&&!i.session_key){console.log(`[admin] account picker shown: userId=${i.userId} accountCount=${i.accounts.length}`),B(i.accounts),r(`account-picker`);return}D(i.session_key),R(i.sessionId??null),j(i.businessName),N(i.role??null),F(i.userName===void 0?null:i.userName),L(i.avatar??null);let o=s(i.thinkingView);if(G.current=o,W(o),t)try{sessionStorage.setItem(`maxy-account-id`,t)}catch{}try{sessionStorage.setItem(`maxy-admin-session-key`,i.session_key)}catch{}a(``),r(`chat`)}catch(e){console.error(`[admin] connection error:`,e),h(`Could not connect.`)}finally{y(!1),H(!1)}}let Q=(0,m.useCallback)(async e=>{if(e.preventDefault(),v)return;h(``);let t=o.trim();if(!t){h(`Please enter your name.`);return}if(i.length<4){h(`PIN must be at least 4 characters.`);return}let n=i;y(!0);try{let e=await fetch(`/api/onboarding/set-pin`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({pin:n,name:t})});if(!e.ok){let t=await e.json().catch(()=>({}));if(e.status===409){console.log(`[admin] PIN already configured — re-checking health`);try{let e=await fetch(`/api/health`);if(e.ok){let n=await e.json();n.pin_configured&&n.claude_authenticated?r(`enter-pin`):n.pin_configured?r(`connect-claude`):h(t.error||`Failed to set PIN.`)}else r(`enter-pin`)}catch{r(`enter-pin`)}return}h(t.error||`Failed to set PIN.`);return}let i=await fetch(`/api/health`);if((i.ok?await i.json():null)?.claude_authenticated){await Z(n);return}a(``),r(`connect-claude`)}catch(e){console.error(`[admin] connection error:`,e),h(`Could not connect.`)}finally{y(!1)}},[i,v,o]),te=(0,m.useCallback)(async e=>{e.preventDefault(),h(``),await Z(i)},[i]),ne=(0,m.useCallback)(async()=>{T(!0);try{if(!await c())return console.warn(`[admin-ui] claude-disconnect not verified — credentials may persist; staying put`),!1;D(null),N(null),F(void 0),L(null);try{sessionStorage.removeItem(`maxy-admin-session-key`),sessionStorage.removeItem(`maxy-account-id`),sessionStorage.removeItem(d)}catch{}return r(`connect-claude`),!0}finally{T(!1)}},[]),$=(0,m.useCallback)(()=>{D(null),N(null),F(void 0),L(null);try{sessionStorage.removeItem(`maxy-admin-session-key`),sessionStorage.removeItem(`maxy-account-id`),sessionStorage.removeItem(d)}catch{}a(``),h(``),r(`enter-pin`)},[]);return(0,m.useEffect)(()=>{X.current=$},[$]),{appState:t,setAppState:r,pin:i,setPin:a,operatorName:o,setOperatorName:l,pinError:u,setPinError:h,showPin:g,setShowPin:_,pinLoading:v,authPolling:b,setAuthPolling:x,authLoading:S,setAuthLoading:C,disconnecting:w,cacheKey:E,setCacheKey:D,claudeInfo:O,setClaudeInfo:k,businessName:A,role:M,userName:P,userAvatar:I,sessionId:ee,setSessionId:R,accounts:z,accountPickerLoading:V,expandAll:U,setExpandAll:W,expandAllDefaultRef:G,remoteAuthEnabled:K,pinInputRef:J,setPinFormRef:Y,handleSetPin:Q,handleLogin:te,handleAccountSelect:(0,m.useCallback)(async e=>{H(!0),h(``),await Z(i,e)},[i]),handleDisconnect:ne,handleLogout:$,handleChangePin:(0,m.useCallback)(async()=>{if(!i){h(`Enter your current PIN first.`);return}y(!0),h(``);try{let e=await fetch(`/api/onboarding/set-pin`,{method:`DELETE`,headers:{"Content-Type":`application/json`},body:JSON.stringify({currentPin:i})});if(!e.ok){h((await e.json().catch(()=>({error:`Incorrect PIN.`}))).error||`Incorrect PIN.`);return}a(``),h(``),r(`set-pin`)}catch(e){console.error(`[admin-auth] change pin failed:`,e),h(e instanceof Error?e.message:String(e))}finally{y(!1)}},[i])}}var g=r();function _({inputRef:e,value:t,onChange:n,onComplete:r,showPin:i,autoFocus:a}){let o=(0,m.useRef)([]);function s(e,r){r.key===`Backspace`?(r.preventDefault(),t[e]?n(t.slice(0,e)+t.slice(e+1)):e>0&&(n(t.slice(0,e-1)+t.slice(e)),o.current[e-1]?.focus())):r.key===`ArrowLeft`&&e>0?o.current[e-1]?.focus():r.key===`ArrowRight`&&e<5?o.current[e+1]?.focus():r.key===`Enter`&&(r.preventDefault(),r.currentTarget.form?.requestSubmit())}function c(e,i){let a=i.nativeEvent.data;if(!a||!/^\d$/.test(a))return;let s=t.split(``);for(s[e]=a;s.length<e;)s.push(``);let c=s.join(``).replace(/\D/g,``).slice(0,6);n(c),c.length===6?r?.(c):e<5&&o.current[e+1]?.focus()}function l(e){e.preventDefault();let t=e.clipboardData.getData(`text`).replace(/\D/g,``).slice(0,6);t&&(n(t),t.length===6?r?.(t):o.current[t.length]?.focus())}return(0,g.jsx)(`div`,{className:`pin-field`,children:Array.from({length:6}).map((n,r)=>(0,g.jsx)(`input`,{ref:t=>{o.current[r]=t,r===0&&e&&(e.current=t)},type:`text`,inputMode:`numeric`,className:`pin-box${t[r]?` pin-box-filled`:``}`,value:t[r]?i?t[r]:`•`:``,onKeyDown:e=>s(r,e),onInput:e=>c(r,e),onPaste:l,onFocus:e=>e.target.select(),autoFocus:a&&r===0,autoComplete:`off`,maxLength:1,"aria-label":`PIN digit ${r+1}`},r))})}function v(e){let{pin:t,setPin:n,showPin:r,setShowPin:a,pinLoading:s,pinError:c,pinInputRef:d,setPinFormRef:f,onSubmit:p,operatorName:m,setOperatorName:h}=e;return(0,g.jsx)(`div`,{className:`connect-page`,children:(0,g.jsxs)(`div`,{className:`connect-content`,children:[(0,g.jsx)(`img`,{src:i,alt:o.productName,className:`connect-logo connect-logo--maxy`}),!o.logoContainsName&&(0,g.jsxs)(`h1`,{className:`connect-title`,children:[`Welcome to `,o.productName]}),(0,g.jsxs)(`p`,{className:`connect-subtitle`,children:[`Tell `,o.productName,` who you are, then choose a PIN.`]}),(0,g.jsxs)(`form`,{ref:f,onSubmit:p,className:`connect-pin-form`,children:[(0,g.jsxs)(`div`,{className:`pin-input-row`,children:[(0,g.jsx)(`input`,{type:`text`,className:`connect-name-input`,placeholder:`Your full name`,value:m,onChange:e=>h(e.target.value),autoComplete:`name`,autoFocus:!0,required:!0,"aria-label":`Your full name`}),(0,g.jsx)(`div`,{style:{width:38,flexShrink:0},"aria-hidden":`true`})]}),(0,g.jsxs)(`div`,{className:`pin-input-row`,children:[(0,g.jsx)(_,{inputRef:d,value:t,onChange:n,onComplete:()=>{},showPin:r}),(0,g.jsx)(l,{variant:`send`,type:`submit`,disabled:!t||!m.trim(),loading:s,"aria-label":`Set PIN`,children:(0,g.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,g.jsx)(`line`,{x1:`5`,y1:`12`,x2:`19`,y2:`12`}),(0,g.jsx)(`polyline`,{points:`12 5 19 12 12 19`})]})})]}),(0,g.jsx)(u,{checked:r,onChange:()=>a(e=>!e),label:`Show PIN`})]}),c&&(0,g.jsx)(`p`,{className:`admin-pin-error`,children:c})]})})}function y(e){let{pin:t,setPin:n,showPin:r,setShowPin:a,pinLoading:s,pinError:c,pinInputRef:d,onSubmit:f,onChangePin:p,remoteAuthEnabled:m,onSignOutRemote:h}=e;return(0,g.jsxs)(`div`,{className:`connect-page`,children:[m&&h&&(0,g.jsx)(`button`,{type:`button`,className:`connect-signout`,onClick:h,children:`Sign out`}),(0,g.jsxs)(`div`,{className:`connect-content`,children:[(0,g.jsx)(`img`,{src:i,alt:o.productName,className:`connect-logo connect-logo--maxy`}),!o.logoContainsName&&(0,g.jsx)(`h1`,{className:`connect-title`,children:o.productName}),(0,g.jsxs)(`form`,{onSubmit:f,className:`connect-pin-form`,children:[(0,g.jsxs)(`div`,{className:`pin-input-row`,children:[(0,g.jsx)(_,{inputRef:d,value:t,onChange:n,onComplete:()=>{},showPin:r,autoFocus:!0}),(0,g.jsx)(l,{variant:`send`,type:`submit`,disabled:!t,loading:s,children:(0,g.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,children:[(0,g.jsx)(`line`,{x1:`5`,y1:`12`,x2:`19`,y2:`12`}),(0,g.jsx)(`polyline`,{points:`12 5 19 12 12 19`})]})})]}),(0,g.jsxs)(`div`,{className:`pin-options`,children:[(0,g.jsx)(u,{checked:r,onChange:()=>a(e=>!e),label:`Show PIN`}),(0,g.jsx)(l,{type:`button`,variant:`ghost`,onClick:p,children:`Change PIN`})]})]}),c&&(0,g.jsx)(`p`,{className:`admin-pin-error`,children:c})]})]})}function b(e){let{accounts:t,loading:n,error:r,onSelect:s}=e;return(0,g.jsx)(`div`,{className:`connect-page`,children:(0,g.jsxs)(`div`,{className:`connect-content`,children:[(0,g.jsx)(`img`,{src:i,alt:o.productName,className:`connect-logo connect-logo--maxy`}),!o.logoContainsName&&(0,g.jsx)(`h1`,{className:`connect-title`,children:o.productName}),(0,g.jsx)(`p`,{className:`connect-subtitle`,children:`Select an account`}),(0,g.jsx)(`div`,{className:`account-picker-list`,children:t.map(e=>(0,g.jsxs)(`button`,{className:`account-picker-card`,onClick:()=>s(e.accountId),disabled:n,type:`button`,children:[(0,g.jsx)(`span`,{className:`account-picker-name`,children:e.businessName||e.accountId}),(0,g.jsx)(`span`,{className:`account-picker-role`,children:e.role}),n&&(0,g.jsx)(a,{className:`account-picker-spinner`,size:16})]},e.accountId))}),r&&(0,g.jsx)(`p`,{className:`admin-pin-error`,children:r})]})})}function x(e){let{authPolling:t,setAuthPolling:n,authLoading:r,setAuthLoading:a,pinError:s,setPinError:c,setAppState:u}=e,[d,f]=(0,m.useState)(!1),[p,h]=(0,m.useState)(!1);async function _(){h(!0),c(``);try{let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`launch-browser`})})).json();e.launched?f(!0):e.error&&c(e.error)}catch(e){console.error(`[admin] browser launch error:`,e),c(`Could not launch browser.`)}h(!1)}async function v(){a(!0),c(``);try{let e=await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`})).json();if(e.started){n(!0),f(!0),a(!1);for(let e=0;e<120;e++)if(await new Promise(e=>setTimeout(e,2e3)),(await(await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`wait`})})).json()).authenticated){await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`stop`})}),u(`enter-pin`);return}c(`Timed out waiting for sign-in. Try again.`),n(!1)}else e.error&&c(e.error)}catch(e){console.error(`[admin] auth flow error:`,e),c(`Could not start auth flow.`)}a(!1)}async function y(){await fetch(`/api/onboarding/claude-auth`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({action:`stop`})}),n(!1),c(``)}return t||d?(0,g.jsxs)(`div`,{style:{display:`flex`,flexDirection:`column`,height:`100dvh`,overflow:`auto`},children:[(0,g.jsxs)(`header`,{className:`chat-header`,style:{paddingBottom:`12px`,flexShrink:0,position:`relative`,maxWidth:`680px`,width:`100%`,margin:`0 auto`,padding:`24px 20px 12px`},children:[t?(0,g.jsx)(`button`,{onClick:y,style:{position:`absolute`,top:`12px`,right:`12px`,background:`none`,border:`none`,color:`#999`,fontSize:`13px`,cursor:`pointer`,padding:`4px 8px`},"aria-label":`Cancel`,children:`✕`}):(0,g.jsx)(`button`,{onClick:()=>f(!1),style:{position:`absolute`,top:`12px`,right:`12px`,background:`none`,border:`none`,color:`#999`,fontSize:`13px`,cursor:`pointer`,padding:`4px 8px`},"aria-label":`Close browser`,children:`✕`}),(0,g.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`chat-logo`}),(0,g.jsx)(`h1`,{className:`chat-tagline`,children:`Connect Claude`}),(0,g.jsx)(`p`,{className:`chat-intro`,children:t?`Sign in and authorize in the browser below.`:`Open your email or prepare your accounts, then sign in.`}),!t&&(0,g.jsx)(`div`,{style:{marginTop:`12px`},children:(0,g.jsx)(l,{variant:`primary`,onClick:v,disabled:r,children:r?(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`span`,{className:`spin`,style:{display:`inline-block`},children:`✱`}),` Connecting…`]}):`Sign in to Claude`})})]}),(0,g.jsx)(`div`,{style:{flex:1,display:`flex`,flexDirection:`column`,minHeight:0,gap:`10px`,padding:`0 0 16px`},children:(0,g.jsx)(`iframe`,{src:`/vnc-viewer.html`,style:{flex:1,width:`100%`,minHeight:0,border:`none`,background:`#111`,display:`block`},title:`Claude Sign-in`})}),s&&(0,g.jsx)(`p`,{className:`admin-pin-error`,style:{textAlign:`center`,padding:`0 20px 16px`},children:s})]}):(0,g.jsx)(`div`,{className:`connect-page`,children:(0,g.jsxs)(`div`,{className:`connect-content`,children:[(0,g.jsxs)(`div`,{className:`connect-logos`,children:[(0,g.jsx)(`div`,{className:`connect-logo-wrap`,children:(0,g.jsx)(`img`,{src:`/brand/claude.png`,alt:`Claude`,className:`connect-logo`})}),(0,g.jsx)(`svg`,{className:`connect-arrow`,viewBox:`0 0 48 24`,fill:`none`,xmlns:`http://www.w3.org/2000/svg`,children:(0,g.jsx)(`path`,{d:`M0 12h44m0 0l-8-8m8 8l-8 8`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`})}),(0,g.jsxs)(`div`,{className:`connect-logo-wrap`,children:[(0,g.jsx)(`img`,{src:i,alt:o.productName,className:`connect-logo connect-logo--maxy`}),!o.logoContainsName&&(0,g.jsx)(`span`,{className:`connect-logo-label`,children:o.productName})]})]}),(0,g.jsxs)(`h1`,{className:`connect-title`,children:[`Connect Claude to power `,o.productName]}),(0,g.jsx)(`p`,{className:`connect-subtitle`,children:`Sign in with your Anthropic account to get started.`}),(0,g.jsx)(l,{variant:`primary`,onClick:v,disabled:r,children:r?(0,g.jsxs)(g.Fragment,{children:[(0,g.jsx)(`span`,{className:`spin`,style:{display:`inline-block`},children:`✱`}),` Connecting…`]}):`Sign in to Claude`}),(0,g.jsx)(`p`,{style:{marginTop:`6px`,fontSize:`11px`,color:`#999`,maxWidth:`300px`,textAlign:`center`,lineHeight:`1.4`},children:`First time? You may need to sign into your email and Anthropic account in the browser before connecting.`}),(0,g.jsx)(`button`,{onClick:_,disabled:p,style:{marginTop:`12px`,background:`none`,border:`none`,color:`var(--color-primary, #666)`,fontSize:`13px`,cursor:`pointer`,textDecoration:`underline`,textUnderlineOffset:`3px`},children:p?`Launching…`:`Open browser first`}),s&&(0,g.jsx)(`p`,{className:`admin-pin-error`,children:s})]})})}function S({auth:e}){return e.appState===`loading`?(0,g.jsx)(`div`,{className:`connect-page`}):e.appState===`set-pin`?(0,g.jsx)(v,{pin:e.pin,setPin:e.setPin,showPin:e.showPin,setShowPin:e.setShowPin,pinLoading:e.pinLoading,pinError:e.pinError,pinInputRef:e.pinInputRef,setPinFormRef:e.setPinFormRef,onSubmit:e.handleSetPin,operatorName:e.operatorName,setOperatorName:e.setOperatorName}):e.appState===`connect-claude`?(0,g.jsx)(x,{authPolling:e.authPolling,setAuthPolling:e.setAuthPolling,authLoading:e.authLoading,setAuthLoading:e.setAuthLoading,pinError:e.pinError,setPinError:e.setPinError,setAppState:e.setAppState}):e.appState===`enter-pin`?(0,g.jsx)(y,{pin:e.pin,setPin:e.setPin,showPin:e.showPin,setShowPin:e.setShowPin,pinLoading:e.pinLoading,pinError:e.pinError,pinInputRef:e.pinInputRef,onSubmit:e.handleLogin,onChangePin:e.handleChangePin,remoteAuthEnabled:e.remoteAuthEnabled,onSignOutRemote:()=>{console.info(`[admin-ui] remote-auth sign-out → /__remote-auth/logout`),window.location.href=`/__remote-auth/logout`}}):e.appState===`account-picker`?(0,g.jsx)(b,{accounts:e.accounts,loading:e.accountPickerLoading,error:e.pinError,onSelect:e.handleAccountSelect}):null}export{h as n,S as t};
|