@rubytech/create-maxy-code 0.1.305 → 0.1.306
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/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 Maxy 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:9d8f9a898ee1cb0726e604e78f5fd1bf2c9660b76604d523ba1c4e3598092a98
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -41,6 +41,19 @@ Because Maxy runs locally, your data stays in your home. It never passes through
|
|
|
41
41
|
|
|
42
42
|
**Public (visitors)** — anyone who reaches your public URL gets the public agent. It handles product enquiries, collects prospect details, and answers questions about your business. It cannot read or write your private data.
|
|
43
43
|
|
|
44
|
+
## Install the Dashboard as an App
|
|
45
|
+
|
|
46
|
+
The admin dashboard and the chat dashboard can be installed as standalone apps, so they open in their own window with their own icon instead of a browser tab. Installing changes nothing about how Maxy works — it is the same dashboard, just launched like any other app.
|
|
47
|
+
|
|
48
|
+
**On a phone or tablet:**
|
|
49
|
+
|
|
50
|
+
- **iPhone or iPad (Safari):** open the dashboard, tap the Share button, then **Add to Home Screen**. A Maxy icon appears on your home screen; tapping it opens the dashboard full-screen.
|
|
51
|
+
- **Android (Chrome):** open the dashboard, then either tap the **Install** prompt Chrome offers, or open the browser menu and choose **Install app** / **Add to Home screen**.
|
|
52
|
+
|
|
53
|
+
**On a computer (Chrome or Edge):** open the dashboard, then click the **Install** icon at the right of the address bar (or open the browser menu and choose **Install**). The dashboard opens in its own window from then on.
|
|
54
|
+
|
|
55
|
+
The admin dashboard and the chat dashboard install as two separate apps, so you can keep both on your home screen or dock. Each one opens straight to its own screen.
|
|
56
|
+
|
|
44
57
|
## First Power-On (New Device)
|
|
45
58
|
|
|
46
59
|
If your device has no WiFi configured and no ethernet cable connected, it creates a temporary WiFi network for setup:
|
|
@@ -14,6 +14,19 @@ Because {{productName}} runs locally, your data stays in your home. It never pas
|
|
|
14
14
|
|
|
15
15
|
**Public (visitors)** — anyone who reaches your public URL gets the public agent. It handles product enquiries, collects prospect details, and answers questions about your business. It cannot read or write your private data.
|
|
16
16
|
|
|
17
|
+
## Install the Dashboard as an App
|
|
18
|
+
|
|
19
|
+
The admin dashboard and the chat dashboard can be installed as standalone apps, so they open in their own window with their own icon instead of a browser tab. Installing changes nothing about how {{productName}} works — it is the same dashboard, just launched like any other app.
|
|
20
|
+
|
|
21
|
+
**On a phone or tablet:**
|
|
22
|
+
|
|
23
|
+
- **iPhone or iPad (Safari):** open the dashboard, tap the Share button, then **Add to Home Screen**. A {{productName}} icon appears on your home screen; tapping it opens the dashboard full-screen.
|
|
24
|
+
- **Android (Chrome):** open the dashboard, then either tap the **Install** prompt Chrome offers, or open the browser menu and choose **Install app** / **Add to Home screen**.
|
|
25
|
+
|
|
26
|
+
**On a computer (Chrome or Edge):** open the dashboard, then click the **Install** icon at the right of the address bar (or open the browser menu and choose **Install**). The dashboard opens in its own window from then on.
|
|
27
|
+
|
|
28
|
+
The admin dashboard and the chat dashboard install as two separate apps, so you can keep both on your home screen or dock. Each one opens straight to its own screen.
|
|
29
|
+
|
|
17
30
|
## First Power-On (New Device)
|
|
18
31
|
|
|
19
32
|
If your device has no WiFi configured and no ethernet cable connected, it creates a temporary WiFi network for setup:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Minimal installable-PWA service worker for the admin, operator, and chat
|
|
2
|
+
// shells. Its only job is to exist: browsers require a registered service
|
|
3
|
+
// worker with a fetch handler before they offer "Install" / "Add to Home
|
|
4
|
+
// Screen". It does NOT cache — no precaching, no offline app-shell, no runtime
|
|
5
|
+
// cache. Offline support is a separate, out-of-scope capability.
|
|
6
|
+
//
|
|
7
|
+
// Served at the origin root (/sw.js) so its default scope is '/', covering
|
|
8
|
+
// every surface. See app/lib/pwa.ts and .docs/pwa-installable-shells.md.
|
|
9
|
+
|
|
10
|
+
self.addEventListener('install', () => {
|
|
11
|
+
// Activate this worker immediately instead of waiting for existing tabs to
|
|
12
|
+
// close, so a freshly-installed app controls its pages on first launch.
|
|
13
|
+
self.skipWaiting()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
self.addEventListener('activate', (event) => {
|
|
17
|
+
event.waitUntil(self.clients.claim())
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
self.addEventListener('fetch', () => {
|
|
21
|
+
// Network passthrough: no respondWith, so the browser handles every request
|
|
22
|
+
// exactly as it would without a service worker. Present only to satisfy the
|
|
23
|
+
// installability requirement.
|
|
24
|
+
})
|
package/payload/server/server.js
CHANGED
|
@@ -1227,6 +1227,89 @@ import { resolve as resolve29, join as join22, basename as basename8 } from "pat
|
|
|
1227
1227
|
import { homedir as homedir3 } from "os";
|
|
1228
1228
|
import { monitorEventLoopDelay } from "perf_hooks";
|
|
1229
1229
|
|
|
1230
|
+
// app/lib/pwa.ts
|
|
1231
|
+
var PWA_SURFACES = [
|
|
1232
|
+
{
|
|
1233
|
+
surface: "admin",
|
|
1234
|
+
shellFile: "index.html",
|
|
1235
|
+
manifestPath: "/manifest-admin.webmanifest",
|
|
1236
|
+
nameSuffix: "",
|
|
1237
|
+
startUrl: "/",
|
|
1238
|
+
scope: "/",
|
|
1239
|
+
id: "/?pwa=admin"
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
surface: "operator",
|
|
1243
|
+
shellFile: "operator.html",
|
|
1244
|
+
manifestPath: "/manifest-operator.webmanifest",
|
|
1245
|
+
nameSuffix: " Operator",
|
|
1246
|
+
startUrl: "/",
|
|
1247
|
+
scope: "/",
|
|
1248
|
+
id: "/?pwa=operator"
|
|
1249
|
+
},
|
|
1250
|
+
{
|
|
1251
|
+
surface: "chat",
|
|
1252
|
+
shellFile: "chat.html",
|
|
1253
|
+
manifestPath: "/manifest-chat.webmanifest",
|
|
1254
|
+
nameSuffix: " Chat",
|
|
1255
|
+
startUrl: "/chat",
|
|
1256
|
+
scope: "/chat",
|
|
1257
|
+
id: "/chat?pwa=chat"
|
|
1258
|
+
}
|
|
1259
|
+
];
|
|
1260
|
+
var MANIFEST_CONTENT_TYPE = "application/manifest+json";
|
|
1261
|
+
var SW_CONTENT_TYPE = "application/javascript";
|
|
1262
|
+
function pwaSurfaceForShell(file) {
|
|
1263
|
+
return PWA_SURFACES.find((s) => s.shellFile === file);
|
|
1264
|
+
}
|
|
1265
|
+
function buildManifest(s, b) {
|
|
1266
|
+
const name = `${b.productName}${s.nameSuffix}`;
|
|
1267
|
+
return {
|
|
1268
|
+
id: s.id,
|
|
1269
|
+
name,
|
|
1270
|
+
short_name: name,
|
|
1271
|
+
start_url: s.startUrl,
|
|
1272
|
+
scope: s.scope,
|
|
1273
|
+
display: "standalone",
|
|
1274
|
+
theme_color: b.themeColor,
|
|
1275
|
+
background_color: b.backgroundColor,
|
|
1276
|
+
icons: [{ src: b.iconPath, sizes: "any", purpose: "any" }]
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
function escAttr(s) {
|
|
1280
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1281
|
+
}
|
|
1282
|
+
function swRegisterScript(s) {
|
|
1283
|
+
return [
|
|
1284
|
+
"<script>",
|
|
1285
|
+
"if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js').catch(function(e){",
|
|
1286
|
+
`try{fetch('/api/_client-error',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({kind:'event',source:'pwa',op:'sw-register-fail',surface:'${s.surface}',scope:'${s.scope}',err:String((e&&e.message)||e)}),keepalive:true}).catch(function(){});}catch(_){}`,
|
|
1287
|
+
"});}",
|
|
1288
|
+
"</script>"
|
|
1289
|
+
].join("\n");
|
|
1290
|
+
}
|
|
1291
|
+
function pwaHeadTags(s, b) {
|
|
1292
|
+
return [
|
|
1293
|
+
`<link rel="manifest" href="${escAttr(s.manifestPath)}">`,
|
|
1294
|
+
`<link rel="apple-touch-icon" href="${escAttr(b.iconPath)}">`,
|
|
1295
|
+
// Both the standards-based and the legacy Apple capable meta: Chromium
|
|
1296
|
+
// deprecates `apple-mobile-web-app-capable` in favour of
|
|
1297
|
+
// `mobile-web-app-capable`, while iOS Safari still reads the apple one.
|
|
1298
|
+
'<meta name="mobile-web-app-capable" content="yes">',
|
|
1299
|
+
'<meta name="apple-mobile-web-app-capable" content="yes">',
|
|
1300
|
+
`<meta name="theme-color" content="${escAttr(b.themeColor)}">`,
|
|
1301
|
+
swRegisterScript(s)
|
|
1302
|
+
].join("\n");
|
|
1303
|
+
}
|
|
1304
|
+
function pwaCensus(d) {
|
|
1305
|
+
const manifestLinked = PWA_SURFACES.filter((s) => {
|
|
1306
|
+
const html = d.shellHtml(s.shellFile);
|
|
1307
|
+
return html != null && html.includes(`"${s.manifestPath}"`);
|
|
1308
|
+
}).length;
|
|
1309
|
+
const iconsPresent = d.iconExists() ? PWA_SURFACES.length : 0;
|
|
1310
|
+
return { surfaces: PWA_SURFACES.length, manifestLinked, iconsPresent };
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1230
1313
|
// server/webchat-descriptor.ts
|
|
1231
1314
|
import { resolve, join as join2 } from "path";
|
|
1232
1315
|
function webchatGatewayUrl() {
|
|
@@ -9153,7 +9236,7 @@ app9.post("/", async (c) => {
|
|
|
9153
9236
|
const safe = typeof v === "string" ? truncate(v, 200) : typeof v === "number" || typeof v === "boolean" ? String(v) : JSON.stringify(v).slice(0, 200);
|
|
9154
9237
|
return `${k}=${safe}`;
|
|
9155
9238
|
}).join(" ");
|
|
9156
|
-
const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set(["stream-log-click"]);
|
|
9239
|
+
const TAGGED_PREFIX_SOURCES = /* @__PURE__ */ new Set(["stream-log-click", "pwa"]);
|
|
9157
9240
|
if (TAGGED_PREFIX_SOURCES.has(source)) {
|
|
9158
9241
|
console.log(
|
|
9159
9242
|
`[${source}] ts=${ts} ip=${ip} version=${version || "unknown"}${extra ? " " + extra : ""}`
|
|
@@ -11804,6 +11887,32 @@ function sortDirEntries(entries) {
|
|
|
11804
11887
|
return aKey.localeCompare(bKey);
|
|
11805
11888
|
});
|
|
11806
11889
|
}
|
|
11890
|
+
var PROTECTED_DELETE_SEGMENTS = {
|
|
11891
|
+
".git": "git",
|
|
11892
|
+
".claude": "claude",
|
|
11893
|
+
secrets: "secrets",
|
|
11894
|
+
agents: "agents",
|
|
11895
|
+
specialists: "specialists",
|
|
11896
|
+
state: "state"
|
|
11897
|
+
};
|
|
11898
|
+
var PROTECTED_DELETE_BASENAMES = {
|
|
11899
|
+
"account.json": "account-json",
|
|
11900
|
+
"wa-channel-bindings.json": "channel-bindings",
|
|
11901
|
+
"webchat-channel-bindings.json": "channel-bindings"
|
|
11902
|
+
};
|
|
11903
|
+
function isProtectedFromDeletion(relPath) {
|
|
11904
|
+
const segments = relPath.split("/").filter(Boolean);
|
|
11905
|
+
for (const seg of segments) {
|
|
11906
|
+
if (Object.hasOwn(PROTECTED_DELETE_SEGMENTS, seg)) {
|
|
11907
|
+
return { protected: true, rule: PROTECTED_DELETE_SEGMENTS[seg] };
|
|
11908
|
+
}
|
|
11909
|
+
}
|
|
11910
|
+
const base = segments[segments.length - 1];
|
|
11911
|
+
if (base && Object.hasOwn(PROTECTED_DELETE_BASENAMES, base)) {
|
|
11912
|
+
return { protected: true, rule: PROTECTED_DELETE_BASENAMES[base] };
|
|
11913
|
+
}
|
|
11914
|
+
return { protected: false };
|
|
11915
|
+
}
|
|
11807
11916
|
async function knowledgeDocOwnedByAccount(accountId, attachmentId) {
|
|
11808
11917
|
if (!attachmentId) return false;
|
|
11809
11918
|
const session = getSession();
|
|
@@ -12035,9 +12144,9 @@ app19.delete("/", requireAdminSession, async (c) => {
|
|
|
12035
12144
|
return c.json({ error: "Not found" }, 404);
|
|
12036
12145
|
}
|
|
12037
12146
|
const base = basename6(absolute);
|
|
12038
|
-
const
|
|
12039
|
-
if (
|
|
12040
|
-
console.error(`[data] file-delete blocked path="${relPath}" reason="protected"`);
|
|
12147
|
+
const protection = isProtectedFromDeletion(relPath);
|
|
12148
|
+
if (protection.protected) {
|
|
12149
|
+
console.error(`[data] file-delete blocked path="${relPath}" reason="protected" rule="${protection.rule}"`);
|
|
12041
12150
|
return c.json({ error: "Protected file \u2014 refusing to delete" }, 403);
|
|
12042
12151
|
}
|
|
12043
12152
|
try {
|
|
@@ -18188,10 +18297,11 @@ app49.post("/api/remote-auth/set-password", async (c) => {
|
|
|
18188
18297
|
});
|
|
18189
18298
|
app49.route("/api/_client-error", client_error_default);
|
|
18190
18299
|
console.log("[client-error-route] mounted");
|
|
18300
|
+
var PWA_PUBLIC_PATHS = /* @__PURE__ */ new Set(["/sw.js", ...PWA_SURFACES.map((s) => s.manifestPath)]);
|
|
18191
18301
|
app49.use("*", async (c, next) => {
|
|
18192
18302
|
const host = (c.req.header("host") ?? "").split(":")[0];
|
|
18193
18303
|
const path2 = c.req.path;
|
|
18194
|
-
if (path2 === "/favicon.ico" || path2.startsWith("/assets/") || path2.startsWith("/brand/")) {
|
|
18304
|
+
if (path2 === "/favicon.ico" || path2.startsWith("/assets/") || path2.startsWith("/brand/") || PWA_PUBLIC_PATHS.has(path2)) {
|
|
18195
18305
|
await next();
|
|
18196
18306
|
return;
|
|
18197
18307
|
}
|
|
@@ -18338,6 +18448,17 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
|
|
|
18338
18448
|
icon: brandIconPath,
|
|
18339
18449
|
logoContainsName: !!BRAND.logoContainsName
|
|
18340
18450
|
})}</script>`;
|
|
18451
|
+
var brandThemeColor = BRAND.defaultColors?.primary ?? "#000000";
|
|
18452
|
+
var brandBackgroundColor = BRAND.defaultColors?.background ?? "#ffffff";
|
|
18453
|
+
var brandIconFsPath = resolve29(process.cwd(), "public", brandIconPath.replace(/^\//, ""));
|
|
18454
|
+
var brandIconExists = existsSync27(brandIconFsPath);
|
|
18455
|
+
var SW_SOURCE = (() => {
|
|
18456
|
+
try {
|
|
18457
|
+
return readFileSync26(resolve29(process.cwd(), "public", "sw.js"), "utf-8");
|
|
18458
|
+
} catch {
|
|
18459
|
+
return null;
|
|
18460
|
+
}
|
|
18461
|
+
})();
|
|
18341
18462
|
function readInstalledVersion() {
|
|
18342
18463
|
try {
|
|
18343
18464
|
if (!PLATFORM_ROOT9) return "unknown";
|
|
@@ -18388,11 +18509,13 @@ function cachedHtml(file) {
|
|
|
18388
18509
|
const productNameEsc = escapeHtml(BRAND.productName);
|
|
18389
18510
|
html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
|
|
18390
18511
|
html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
|
|
18391
|
-
const
|
|
18512
|
+
const pwaSurface = pwaSurfaceForShell(file);
|
|
18513
|
+
const pwaHead = pwaSurface ? "\n" + pwaHeadTags(pwaSurface, { iconPath: brandIconPath, themeColor: brandThemeColor }) + "\n" : "";
|
|
18514
|
+
const headInjection = (file === "index.html" ? `${brandScript}
|
|
18392
18515
|
${versionScript}
|
|
18393
18516
|
${clientErrorReporterScript}
|
|
18394
18517
|
` : `${brandScript}
|
|
18395
|
-
|
|
18518
|
+
`) + pwaHead;
|
|
18396
18519
|
html = html.replace("</head>", `${headInjection}</head>`);
|
|
18397
18520
|
htmlCache.set(file, html);
|
|
18398
18521
|
}
|
|
@@ -18534,6 +18657,30 @@ app49.post("/api/vnc/client-event", async (c) => {
|
|
|
18534
18657
|
app49.get("/g/:slug", (c) => {
|
|
18535
18658
|
return c.html(brandedPublicHtml());
|
|
18536
18659
|
});
|
|
18660
|
+
for (const pwa of PWA_SURFACES) {
|
|
18661
|
+
app49.get(pwa.manifestPath, (c) => {
|
|
18662
|
+
const manifest = buildManifest(pwa, {
|
|
18663
|
+
productName: BRAND.productName,
|
|
18664
|
+
iconPath: brandIconPath,
|
|
18665
|
+
themeColor: brandThemeColor,
|
|
18666
|
+
backgroundColor: brandBackgroundColor
|
|
18667
|
+
});
|
|
18668
|
+
c.header("Content-Type", MANIFEST_CONTENT_TYPE);
|
|
18669
|
+
console.log(
|
|
18670
|
+
`[pwa] op=manifest surface=${pwa.surface} brand=${BRAND.productName} status=200 ct=${MANIFEST_CONTENT_TYPE} icon=${brandIconPath} iconExists=${brandIconExists}`
|
|
18671
|
+
);
|
|
18672
|
+
return c.body(JSON.stringify(manifest));
|
|
18673
|
+
});
|
|
18674
|
+
}
|
|
18675
|
+
app49.get("/sw.js", (c) => {
|
|
18676
|
+
if (SW_SOURCE == null) {
|
|
18677
|
+
console.error("[pwa] op=sw status=500 reason=sw-source-missing");
|
|
18678
|
+
return c.text("Service worker unavailable", 500);
|
|
18679
|
+
}
|
|
18680
|
+
c.header("Content-Type", SW_CONTENT_TYPE);
|
|
18681
|
+
console.log(`[pwa] op=sw status=200 ct=${SW_CONTENT_TYPE}`);
|
|
18682
|
+
return c.body(SW_SOURCE);
|
|
18683
|
+
});
|
|
18537
18684
|
app49.get("/graph", (c) => {
|
|
18538
18685
|
const host = (c.req.header("host") ?? "").split(":")[0];
|
|
18539
18686
|
if (isPublicHost(host) || isOperatorHost(host, getOperatorDomains())) return c.text("Not found", 404);
|
|
@@ -18591,6 +18738,21 @@ var port = requirePortEnv("MAXY_UI_INTERNAL_PORT", { tag: "ui-server" });
|
|
|
18591
18738
|
var hostname = process.env.HOSTNAME ?? "127.0.0.1";
|
|
18592
18739
|
var httpServer = serve({ fetch: app49.fetch, port, hostname });
|
|
18593
18740
|
console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
|
|
18741
|
+
{
|
|
18742
|
+
const census = pwaCensus({
|
|
18743
|
+
shellHtml: (file) => {
|
|
18744
|
+
try {
|
|
18745
|
+
return cachedHtml(file);
|
|
18746
|
+
} catch {
|
|
18747
|
+
return null;
|
|
18748
|
+
}
|
|
18749
|
+
},
|
|
18750
|
+
iconExists: () => brandIconExists
|
|
18751
|
+
});
|
|
18752
|
+
console.log(
|
|
18753
|
+
`[pwa-census] surfaces=${census.surfaces} manifest-linked=${census.manifestLinked} icons-present=${census.iconsPresent}`
|
|
18754
|
+
);
|
|
18755
|
+
}
|
|
18594
18756
|
startAuthHealthHeartbeat();
|
|
18595
18757
|
{
|
|
18596
18758
|
const loopHist = monitorEventLoopDelay({ resolution: 50 });
|