@rubytech/create-maxy-code 0.1.487 → 0.1.489
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 +1 -1
- package/payload/platform/docs/superpowers/plans/2026-07-22-task-1899-reconcile-report-only.md +748 -0
- package/payload/platform/docs/superpowers/specs/2026-07-22-task-1899-reconcile-report-only-design.md +121 -0
- package/payload/platform/lib/active-rules/package-lock.json +12 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.js +51 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/schema-cypher-parser.test.js.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.d.ts +2 -2
- package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.d.ts.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.js +20 -4
- package/payload/platform/lib/graph-mcp/dist/schema-cypher-parser.js.map +1 -1
- package/payload/platform/lib/graph-mcp/src/__tests__/schema-cypher-parser.test.ts +55 -0
- package/payload/platform/lib/graph-mcp/src/schema-cypher-parser.ts +19 -4
- package/payload/platform/lib/graph-style/dist/index.d.ts +24 -0
- package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-style/dist/index.js +25 -1
- package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-style/src/__tests__/additional-base-labels.test.ts +22 -0
- package/payload/platform/lib/graph-style/src/index.ts +27 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js +15 -0
- package/payload/platform/lib/storage-broker/dist/__tests__/audit.test.js.map +1 -1
- package/payload/platform/lib/storage-broker/dist/audit.d.ts +5 -0
- package/payload/platform/lib/storage-broker/dist/audit.d.ts.map +1 -1
- package/payload/platform/lib/storage-broker/dist/audit.js +3 -2
- package/payload/platform/lib/storage-broker/dist/audit.js.map +1 -1
- package/payload/platform/lib/storage-broker/src/__tests__/audit.test.ts +21 -0
- package/payload/platform/lib/storage-broker/src/audit.ts +8 -2
- package/payload/platform/neo4j/schema.cypher +105 -0
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +8 -2
- package/payload/platform/plugins/cloudflare/PLUGIN.md +22 -2
- package/payload/platform/plugins/cloudflare/bin/portal-index-push.mjs +30 -8
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-directory-listing.test.ts +75 -13
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-index-push.test.ts +77 -9
- package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-indexed-download.test.ts +38 -2
- package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +72 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +43 -7
- package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +22 -7
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +14 -2
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +23 -5
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/index.html +3 -0
- package/payload/platform/plugins/cloudflare/skills/data-portal/template/portal.js +46 -5
- package/payload/platform/plugins/docs/references/calendar-booking.md +5 -1
- package/payload/platform/plugins/docs/references/internals.md +2 -0
- package/payload/platform/plugins/memory/PLUGIN.md +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js +61 -31
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-construction.md +13 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +2 -2
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js +70 -5
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/booking-reconcile.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/getUserTimezone.test.js +31 -6
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/getUserTimezone.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts +31 -4
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js +18 -7
- package/payload/platform/plugins/scheduling/mcp/dist/lib/booking-reconcile.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.d.ts +15 -6
- package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.js +24 -11
- package/payload/platform/plugins/scheduling/mcp/dist/lib/neo4j.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js +84 -5
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/reconcile-bookings.js.map +1 -1
- package/payload/platform/plugins/storage-broker/PLUGIN.md +1 -0
- package/payload/platform/scripts/__tests__/account-schema-owned-dirs.test.sh +22 -2
- package/payload/platform/scripts/__tests__/quarantine-restore.test.sh +143 -0
- package/payload/platform/scripts/quarantine-restore.mjs +175 -0
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts +24 -7
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js +55 -72
- package/payload/platform/services/claude-session-manager/dist/account-dir-schema-reconcile.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +11 -8
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +32 -0
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +32 -2
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts +6 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/targets.js +6 -0
- package/payload/platform/services/whatsapp-channel/dist/targets.js.map +1 -1
- package/payload/platform/templates/account-schema/SCHEMA.md +7 -4
- package/payload/server/{chunk-REWXLAPC.js → chunk-UFXRUKXO.js} +4 -2
- package/payload/server/public/activity.html +6 -6
- package/payload/server/public/assets/{AdminLoginScreens-DF9S5Rca.js → AdminLoginScreens-Dic_fPt6.js} +1 -1
- package/payload/server/public/assets/{AdminShell-B0PNDCAb.js → AdminShell-JN7rZ8aG.js} +1 -1
- package/payload/server/public/assets/{Checkbox-s5X6dso4.js → Checkbox-CB74kINA.js} +1 -1
- package/payload/server/public/assets/{activity-BRBsaalV.js → activity-DbfpHg3i.js} +1 -1
- package/payload/server/public/assets/{admin-BoVCP4Mw.js → admin-DvQPTxqe.js} +1 -1
- package/payload/server/public/assets/{bot-BVO9Uqr_.js → bot-DnuyRsLS.js} +1 -1
- package/payload/server/public/assets/{browser-BVW4FFii.js → browser-q4EFqKvR.js} +1 -1
- package/payload/server/public/assets/{calendar-CWsD3uLk.js → calendar-C-q9kWmA.js} +1 -1
- package/payload/server/public/assets/chat-DcefIV9q.js +1 -0
- package/payload/server/public/assets/chevron-down-Blma_CKl.js +1 -0
- package/payload/server/public/assets/chevron-left-xeYDxFiC.js +1 -0
- package/payload/server/public/assets/chevron-right-BonbtjqJ.js +1 -0
- package/payload/server/public/assets/data-Bu-5SUmR.js +1 -0
- package/payload/server/public/assets/{graph-mJ9ky5dc.js → graph-Cyh6FWaV.js} +1 -1
- package/payload/server/public/assets/graph-labels-CUQtXFKW.js +1 -0
- package/payload/server/public/assets/{maximize-2-yucCepu_.js → maximize-2-k3Fu5hdP.js} +1 -1
- package/payload/server/public/assets/{operator-911z_Fy_.js → operator-8NVpNLeD.js} +1 -1
- package/payload/server/public/assets/{page-Cv0qUwMG.js → page-Cno8NNMk.js} +1 -1
- package/payload/server/public/assets/{page-CgSgj8Cy.js → page-DhwPG5Zf.js} +1 -1
- package/payload/server/public/assets/{public-BzwveFwz.js → public-BJ5Oo2Fw.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-BH51cf2Q.js → rotate-ccw-D-ov5jM4.js} +1 -1
- package/payload/server/public/assets/tasks-DHMxTZVD.js +1 -0
- package/payload/server/public/assets/{time-entry-format-DPdGkp1C.js → time-entry-format-EmV5lz6o.js} +1 -1
- package/payload/server/public/assets/{triangle-alert-DO2rjjfV.js → triangle-alert-CivyPfAL.js} +1 -1
- package/payload/server/public/assets/{useCopyFeedback-HMSM7-Ir.js → useCopyFeedback-hbnPS-r9.js} +1 -1
- package/payload/server/public/assets/{useSubAccountSwitcher-2ojOOYNo.css → useSubAccountSwitcher-kwhG-aJj.css} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-BcukQIza.js → useVoiceRecorder-QN9_GqB_.js} +1 -1
- package/payload/server/public/browser.html +4 -4
- package/payload/server/public/calendar.html +6 -6
- package/payload/server/public/chat.html +13 -13
- package/payload/server/public/data.html +10 -10
- package/payload/server/public/graph.html +11 -11
- package/payload/server/public/index.html +16 -16
- package/payload/server/public/operator.html +15 -15
- package/payload/server/public/public.html +13 -13
- package/payload/server/public/tasks.html +6 -6
- package/payload/server/server.js +1036 -784
- package/payload/server/{src-JJ6PHO5I.js → src-YJF3UJNR.js} +1 -1
- package/payload/server/public/assets/chat-DKGREuIO.js +0 -1
- package/payload/server/public/assets/chevron-down-m581tl1J.js +0 -1
- package/payload/server/public/assets/chevron-left-BAY0xfmR.js +0 -1
- package/payload/server/public/assets/chevron-right-Dy_REdYZ.js +0 -1
- package/payload/server/public/assets/data-CtOxa94P.js +0 -1
- package/payload/server/public/assets/graph-labels-C8-sIKuv.js +0 -1
- package/payload/server/public/assets/tasks-21egY7A6.js +0 -1
- /package/payload/server/public/assets/{useSubAccountSwitcher-DLvqvfoc.js → useSubAccountSwitcher-DotTrvX3.js} +0 -0
|
@@ -114,4 +114,19 @@ const row = (objectKey, ingested, uploadedAt) => ({
|
|
|
114
114
|
const mixed = (0, audit_js_1.reconcileDataPortal)([], [row("a", 0, "not-a-date"), row("b", 0, "2026-07-20T11:00:00Z")], now);
|
|
115
115
|
strict_1.default.equal(mixed.oldestUningestedHrs, 1);
|
|
116
116
|
});
|
|
117
|
+
(0, node_test_1.default)("uningestedRows counts every pending row, including one the age sample drops", () => {
|
|
118
|
+
const now = Date.parse("2026-07-20T12:00:00Z");
|
|
119
|
+
const r = (0, audit_js_1.reconcileDataPortal)([], [
|
|
120
|
+
row("a", 0, "2026-07-20T11:00:00Z"),
|
|
121
|
+
row("b", 0, "not-a-date"),
|
|
122
|
+
row("c", 1, "2026-07-20T11:00:00Z"),
|
|
123
|
+
], now);
|
|
124
|
+
strict_1.default.equal(r.uningestedRows, 2);
|
|
125
|
+
strict_1.default.equal(r.oldestUningestedHrs, 1);
|
|
126
|
+
});
|
|
127
|
+
(0, node_test_1.default)("a backlog of only unparseable rows reports a count beside a null age", () => {
|
|
128
|
+
const r = (0, audit_js_1.reconcileDataPortal)([], [row("a", 0, "not-a-date")], 0);
|
|
129
|
+
strict_1.default.equal(r.uningestedRows, 1);
|
|
130
|
+
strict_1.default.equal(r.oldestUningestedHrs, null);
|
|
131
|
+
});
|
|
117
132
|
//# sourceMappingURL=audit.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.test.js","sourceRoot":"","sources":["../../src/__tests__/audit.test.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"audit.test.js","sourceRoot":"","sources":["../../src/__tests__/audit.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -75,6 +75,11 @@ export interface DataPortalReconcile {
|
|
|
75
75
|
/** Null when nothing is uningested, or when no uningested row carries a
|
|
76
76
|
* parseable timestamp. Rendered `na` by the caller. */
|
|
77
77
|
oldestUningestedHrs: number | null;
|
|
78
|
+
/** Every row at `ingested = 0`, including rows whose `uploadedAt` does not
|
|
79
|
+
* parse. Deliberately NOT the size of the age sample below: that sample
|
|
80
|
+
* excludes unparseable timestamps, so a backlog made entirely of them would
|
|
81
|
+
* report a null age beside a zero count and read as an empty queue. */
|
|
82
|
+
uningestedRows: number;
|
|
78
83
|
}
|
|
79
84
|
/** Pure over two lists and an injected clock, so the age arithmetic is
|
|
80
85
|
* deterministic under test rather than dependent on when the suite runs.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,SAAS,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAErF,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD;;;;;;;;;;;;;;;;;uDAiBuD;AACvD,MAAM,WAAW,SAAS;IACxB,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1B,yEAAyE;IACzE,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACtD,mBAAmB,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CAC9F;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,KAAK,EAAE,CAAA;CAAE,CAAC,CA8BpF;AAmBD;;;;;;2CAM2C;AAC3C,MAAM,WAAW,cAAc;IAC7B,UAAU,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAChC,kBAAkB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAclF;AAoBD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;;uEAKmE;IACnE,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB;4DACwD;IACxD,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,SAAS,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAErF,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD;;;;;;;;;;;;;;;;;uDAiBuD;AACvD,MAAM,WAAW,SAAS;IACxB,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1B,IAAI,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1B,yEAAyE;IACzE,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACtD,mBAAmB,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;CAC9F;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,KAAK,EAAE,CAAA;CAAE,CAAC,CA8BpF;AAmBD;;;;;;2CAM2C;AAC3C,MAAM,WAAW,cAAc;IAC7B,UAAU,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAChC,kBAAkB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAclF;AAoBD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;;uEAKmE;IACnE,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB;4DACwD;IACxD,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC;;;4EAGwE;IACxE,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;;gEAKgE;AAChE,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,IAAI,EAAE,aAAa,EAAE,EACrB,KAAK,EAAE,MAAM,GACZ,mBAAmB,CA8BrB"}
|
|
@@ -68,8 +68,8 @@ function reconcileDataPortal(objects, rows, nowMs) {
|
|
|
68
68
|
// and renders as `oldestUningestedHrs=NaN` — the defect the availability
|
|
69
69
|
// loop's snapshotAgeMs already had to fix. Unparseable rows are excluded from
|
|
70
70
|
// the minimum; if that leaves none, the answer is null, never NaN.
|
|
71
|
-
const
|
|
72
|
-
|
|
71
|
+
const uningested = rows.filter((r) => r.ingested === 0);
|
|
72
|
+
const uningestedMs = uningested
|
|
73
73
|
.map((r) => Date.parse(r.uploadedAt))
|
|
74
74
|
.filter((ms) => !Number.isNaN(ms));
|
|
75
75
|
// Reduced, never `Math.min(...uningestedMs)`. A spread passes one argument per
|
|
@@ -86,6 +86,7 @@ function reconcileDataPortal(objects, rows, nowMs) {
|
|
|
86
86
|
orphanObjects: [...objectKeys].filter((k) => !rowKeys.has(k)),
|
|
87
87
|
phantomRows: [...rowKeys].filter((k) => !objectKeys.has(k)),
|
|
88
88
|
oldestUningestedHrs: uningestedMs.length === 0 ? null : (nowMs - oldestMs) / 3_600_000,
|
|
89
|
+
uningestedRows: uningested.length,
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
//# sourceMappingURL=audit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":";AAAA,+EAA+E;AAC/E,+EAA+E;AAC/E,8EAA8E;AAC9E,gFAAgF;AAChF,0DAA0D;;AAuC1D,4CA8BC;AAsCD,wCAcC;
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -147,3 +147,24 @@ test("an unparseable uploadedAt never yields NaN", () => {
|
|
|
147
147
|
);
|
|
148
148
|
assert.equal(mixed.oldestUningestedHrs, 1);
|
|
149
149
|
});
|
|
150
|
+
|
|
151
|
+
test("uningestedRows counts every pending row, including one the age sample drops", () => {
|
|
152
|
+
const now = Date.parse("2026-07-20T12:00:00Z");
|
|
153
|
+
const r = reconcileDataPortal(
|
|
154
|
+
[],
|
|
155
|
+
[
|
|
156
|
+
row("a", 0, "2026-07-20T11:00:00Z"),
|
|
157
|
+
row("b", 0, "not-a-date"),
|
|
158
|
+
row("c", 1, "2026-07-20T11:00:00Z"),
|
|
159
|
+
],
|
|
160
|
+
now,
|
|
161
|
+
);
|
|
162
|
+
assert.equal(r.uningestedRows, 2);
|
|
163
|
+
assert.equal(r.oldestUningestedHrs, 1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("a backlog of only unparseable rows reports a count beside a null age", () => {
|
|
167
|
+
const r = reconcileDataPortal([], [row("a", 0, "not-a-date")], 0);
|
|
168
|
+
assert.equal(r.uningestedRows, 1);
|
|
169
|
+
assert.equal(r.oldestUningestedHrs, null);
|
|
170
|
+
});
|
|
@@ -167,6 +167,11 @@ export interface DataPortalReconcile {
|
|
|
167
167
|
/** Null when nothing is uningested, or when no uningested row carries a
|
|
168
168
|
* parseable timestamp. Rendered `na` by the caller. */
|
|
169
169
|
oldestUningestedHrs: number | null;
|
|
170
|
+
/** Every row at `ingested = 0`, including rows whose `uploadedAt` does not
|
|
171
|
+
* parse. Deliberately NOT the size of the age sample below: that sample
|
|
172
|
+
* excludes unparseable timestamps, so a backlog made entirely of them would
|
|
173
|
+
* report a null age beside a zero count and read as an empty queue. */
|
|
174
|
+
uningestedRows: number;
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
/** Pure over two lists and an injected clock, so the age arithmetic is
|
|
@@ -187,8 +192,8 @@ export function reconcileDataPortal(
|
|
|
187
192
|
// and renders as `oldestUningestedHrs=NaN` — the defect the availability
|
|
188
193
|
// loop's snapshotAgeMs already had to fix. Unparseable rows are excluded from
|
|
189
194
|
// the minimum; if that leaves none, the answer is null, never NaN.
|
|
190
|
-
const
|
|
191
|
-
|
|
195
|
+
const uningested = rows.filter((r) => r.ingested === 0);
|
|
196
|
+
const uningestedMs = uningested
|
|
192
197
|
.map((r) => Date.parse(r.uploadedAt))
|
|
193
198
|
.filter((ms) => !Number.isNaN(ms));
|
|
194
199
|
|
|
@@ -207,5 +212,6 @@ export function reconcileDataPortal(
|
|
|
207
212
|
orphanObjects: [...objectKeys].filter((k) => !rowKeys.has(k)),
|
|
208
213
|
phantomRows: [...rowKeys].filter((k) => !objectKeys.has(k)),
|
|
209
214
|
oldestUningestedHrs: uningestedMs.length === 0 ? null : (nowMs - oldestMs) / 3_600_000,
|
|
215
|
+
uningestedRows: uningested.length,
|
|
210
216
|
};
|
|
211
217
|
}
|
|
@@ -2228,3 +2228,108 @@ OPTIONS {
|
|
|
2228
2228
|
`vector.similarity_function`: 'cosine'
|
|
2229
2229
|
}
|
|
2230
2230
|
};
|
|
2231
|
+
|
|
2232
|
+
// ---------------------------------------------------------------------------
|
|
2233
|
+
// Vertical node-type declarations (Task 1893).
|
|
2234
|
+
//
|
|
2235
|
+
// Every label below is declared in a `## Node Types` / `## Additional Node
|
|
2236
|
+
// Types` table of a schema-*.md ontology doc but had no line here, so the
|
|
2237
|
+
// schema-doc drift guard could not see it and the first memory-write against a
|
|
2238
|
+
// fresh graph would hit an unregistered label. The guard was blind to them
|
|
2239
|
+
// because it matched only the backticked `:Label` prose form.
|
|
2240
|
+
//
|
|
2241
|
+
// An accountId index, not a uniqueness constraint. Only schema-construction.md
|
|
2242
|
+
// and schema-knowledge-work.md carry an `## Idempotency contracts` section, so
|
|
2243
|
+
// no natural key is agreed for any label here, and several have none that would
|
|
2244
|
+
// be correct: two :StockMovement rows on one day are legitimately distinct, as
|
|
2245
|
+
// are two :Stop rows on one route. accountId is the one property every one of
|
|
2246
|
+
// them requires, and an index can never reject a legitimate write.
|
|
2247
|
+
// ---------------------------------------------------------------------------
|
|
2248
|
+
|
|
2249
|
+
// schema-base.md
|
|
2250
|
+
//
|
|
2251
|
+
// :`Order` and :`Case` are backtick-escaped because ORDER and CASE are Cypher
|
|
2252
|
+
// reserved words. Escaping a label is always legal whether or not the word is
|
|
2253
|
+
// reserved, and seed-neo4j.sh pipes this whole file through cypher-shell under
|
|
2254
|
+
// `set -euo pipefail`, so one unparseable statement aborts the entire install
|
|
2255
|
+
// seed. They are the only two of the 130 declared labels that collide.
|
|
2256
|
+
CREATE INDEX order_account IF NOT EXISTS
|
|
2257
|
+
FOR (n:`Order`) ON (n.accountId);
|
|
2258
|
+
|
|
2259
|
+
CREATE INDEX lead_account IF NOT EXISTS
|
|
2260
|
+
FOR (n:Lead) ON (n.accountId);
|
|
2261
|
+
|
|
2262
|
+
CREATE INDEX case_account IF NOT EXISTS
|
|
2263
|
+
FOR (n:`Case`) ON (n.accountId);
|
|
2264
|
+
|
|
2265
|
+
CREATE INDEX whatsapp_conversation_account IF NOT EXISTS
|
|
2266
|
+
FOR (n:WhatsAppConversation) ON (n.accountId);
|
|
2267
|
+
|
|
2268
|
+
CREATE INDEX whatsapp_message_account IF NOT EXISTS
|
|
2269
|
+
FOR (n:WhatsAppMessage) ON (n.accountId);
|
|
2270
|
+
|
|
2271
|
+
// schema-creator.md
|
|
2272
|
+
CREATE INDEX content_account IF NOT EXISTS
|
|
2273
|
+
FOR (n:Content) ON (n.accountId);
|
|
2274
|
+
|
|
2275
|
+
CREATE INDEX campaign_account IF NOT EXISTS
|
|
2276
|
+
FOR (n:Campaign) ON (n.accountId);
|
|
2277
|
+
|
|
2278
|
+
CREATE INDEX audience_account IF NOT EXISTS
|
|
2279
|
+
FOR (n:Audience) ON (n.accountId);
|
|
2280
|
+
|
|
2281
|
+
CREATE INDEX brand_deal_account IF NOT EXISTS
|
|
2282
|
+
FOR (n:BrandDeal) ON (n.accountId);
|
|
2283
|
+
|
|
2284
|
+
// schema-food-beverage.md
|
|
2285
|
+
CREATE INDEX table_reservation_account IF NOT EXISTS
|
|
2286
|
+
FOR (n:TableReservation) ON (n.accountId);
|
|
2287
|
+
|
|
2288
|
+
CREATE INDEX menu_item_account IF NOT EXISTS
|
|
2289
|
+
FOR (n:MenuItem) ON (n.accountId);
|
|
2290
|
+
|
|
2291
|
+
CREATE INDEX menu_account IF NOT EXISTS
|
|
2292
|
+
FOR (n:Menu) ON (n.accountId);
|
|
2293
|
+
|
|
2294
|
+
CREATE INDEX dining_order_account IF NOT EXISTS
|
|
2295
|
+
FOR (n:DiningOrder) ON (n.accountId);
|
|
2296
|
+
|
|
2297
|
+
// schema-hospitality.md
|
|
2298
|
+
CREATE INDEX reservation_account IF NOT EXISTS
|
|
2299
|
+
FOR (n:Reservation) ON (n.accountId);
|
|
2300
|
+
|
|
2301
|
+
CREATE INDEX room_account IF NOT EXISTS
|
|
2302
|
+
FOR (n:Room) ON (n.accountId);
|
|
2303
|
+
|
|
2304
|
+
// schema-logistics.md
|
|
2305
|
+
CREATE INDEX shipment_account IF NOT EXISTS
|
|
2306
|
+
FOR (n:Shipment) ON (n.accountId);
|
|
2307
|
+
|
|
2308
|
+
CREATE INDEX vehicle_account IF NOT EXISTS
|
|
2309
|
+
FOR (n:Vehicle) ON (n.accountId);
|
|
2310
|
+
|
|
2311
|
+
CREATE INDEX route_account IF NOT EXISTS
|
|
2312
|
+
FOR (n:Route) ON (n.accountId);
|
|
2313
|
+
|
|
2314
|
+
CREATE INDEX stop_account IF NOT EXISTS
|
|
2315
|
+
FOR (n:Stop) ON (n.accountId);
|
|
2316
|
+
|
|
2317
|
+
// schema-professional-services.md
|
|
2318
|
+
CREATE INDEX engagement_account IF NOT EXISTS
|
|
2319
|
+
FOR (n:Engagement) ON (n.accountId);
|
|
2320
|
+
|
|
2321
|
+
CREATE INDEX retainer_account IF NOT EXISTS
|
|
2322
|
+
FOR (n:Retainer) ON (n.accountId);
|
|
2323
|
+
|
|
2324
|
+
CREATE INDEX deliverable_account IF NOT EXISTS
|
|
2325
|
+
FOR (n:Deliverable) ON (n.accountId);
|
|
2326
|
+
|
|
2327
|
+
// schema-retail.md
|
|
2328
|
+
CREATE INDEX product_account IF NOT EXISTS
|
|
2329
|
+
FOR (n:Product) ON (n.accountId);
|
|
2330
|
+
|
|
2331
|
+
CREATE INDEX stock_movement_account IF NOT EXISTS
|
|
2332
|
+
FOR (n:StockMovement) ON (n.accountId);
|
|
2333
|
+
|
|
2334
|
+
CREATE INDEX store_location_account IF NOT EXISTS
|
|
2335
|
+
FOR (n:StoreLocation) ON (n.accountId);
|
|
@@ -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:643a947eb8968517ad598f7f8b4e433d8545877d7c21f857d4867ac914e34cc9
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -2482,10 +2482,14 @@ This usually completes within a couple of minutes of the booking.
|
|
|
2482
2482
|
Bookings are reconciled on a cycle and the system audits itself each run. In the device logs:
|
|
2483
2483
|
|
|
2484
2484
|
- `[calendar-booking]` lines trace one booking through submit, write, notify, and done.
|
|
2485
|
-
- `[calendar-reconcile]` lines report
|
|
2485
|
+
- `[calendar-reconcile]` lines report four health numbers per account: `eligibleExamined` (bookings the check looked at), `pendingUnreconciled` (bookings captured but not yet turned into meetings), `pendingOldestAgeMin` (how many minutes the longest-waiting of those has been waiting) and `orphanMeetings` (booking meetings with no matching saved booking). Entries that only block out a slot are not counted — they are not bookings and never become meetings.
|
|
2486
2486
|
|
|
2487
2487
|
A `pendingUnreconciled` number that stays above zero across several runs means those bookings are failing to turn into meetings, and the `attempt=` number on the matching `[calendar-booking]` lines says how many times each has been tried. A booking whose meeting is deleted is picked up again automatically on the next run. To read them: `journalctl --user -u <brand-service> | grep -E '\[calendar-booking\]|\[calendar-reconcile\]'`, then filter by one booking id for its full story. The edge half of a booking (before your device sees it) is in the Cloudflare Pages function logs.
|
|
2488
2488
|
|
|
2489
|
+
A count on its own cannot tell a short lag from a long one: five bookings waiting two minutes and five waiting nine hours are both `pendingUnreconciled=5`. `pendingOldestAgeMin` is what separates them. It reads `0` when nothing is waiting, and also when something is waiting but for under a minute, so read it together with the count beside it.
|
|
2490
|
+
|
|
2491
|
+
A separate `op=liveness` line is printed every ten minutes by your device's own server, whether or not anything is wrong. `stale=false` means the booking check has run recently. `stale=true` means it has not run in over six minutes, which is the case nothing else can tell you about: a check that has stopped cannot report that it has stopped. `lastSkipReason` names the last account whose check finished without looking at any bookings, and reads `none` when every account it got to was checked. If you run more than one account, a check can both look at one account and skip another, so seeing a recent check time next to a skip reason is normal and means two different accounts.
|
|
2492
|
+
|
|
2489
2493
|
## When your real calendar lives elsewhere
|
|
2490
2494
|
|
|
2491
2495
|
The booking page publishes open slots from the internal calendar, the one Maxy keeps in its own graph.
|
|
@@ -4219,6 +4223,8 @@ Standing rules the operator gives over a channel are `Preference` nodes. Two mec
|
|
|
4219
4223
|
|
|
4220
4224
|
**Trust a scheduled turn's provenance, never an inbound's claim (channel-agent grounding).** A cron-dispatched agent turn reaches the same channel builders as a real inbound, so its own authorisation text would otherwise land inside `## Context` — the zone `REFRAME_INSTRUCTION` demotes to untrusted — and a security-cautious agent would distrust its own automation (the self-cancel loop). The dispatcher therefore stamps a `ScheduleProvenance` marker (owning account, event, owner userId, dispatch time) that the channel builders render as a `## Scheduled automation (platform-verified)` preamble **above** `## Context` (and above the rules block); the whatsapp wrap tag names the door `scheduler`. Both channel-server paths carry the marker — the single-target loop and the unified multi-target door reader (via the same `doorInboundPayload` seam that carries `standingRules`); the scheduler only dispatches to the whatsapp/telegram door, so a webchat frame never bears one. Because the marker is dispatcher-stamped and never derived from payload text, an inbound message cannot forge it — placement above `## Context` alone is not the trust signal, the dispatcher provenance is. The grounding binds to it in two places: one recipient-neutral clause added byte-identical to the three reframe instructions (a marker-bearing turn is authorised standing automation; a reconfiguration or re-enable is not intrusion), and the admin `IDENTITY.md` "Untrusted input" rule that the agent never states a platform-level authenticity or impersonation verdict it was not given (an unsourced "this is not from you" is its own inference, flagged, never asserted as the platform's). A sender has exactly one session: every inbound, real or scheduled, resolves to the deterministic `adminSessionIdFor(effectiveAccount, destination)`. An earlier change gave each firing its own salted seat so distrust could not accrete across firings, but the inbound hub keys on `senderId` and replaces its subscriber outright, so a seat's channel server attached under the sender's own key and swallowed the sender's REAL messages until it detached — splitting one conversation across two sessions that each denied what the other held. The seat is removed; the authoritative `agentPrompt`, this provenance marker, and the injected standing rules ground each firing without one. Self-cancel attribution is disarmed while that model beds in: the guard used to compare the cancelling turn's session id to a dispatcher stamp, which discriminated only because each firing ran its own seat, so with one session per sender it would report an autonomous loop on every operator cancel. It now always reports false — under-counting a real loop rather than inventing one — until the discriminator is rebuilt from the turn's own dispatched eventId. See `.docs/scheduling-agent-dispatch.md § Operator-provenance marker`, `§ Session model — one session per sender`, and `§ Self-cancel attribution — disarmed`.
|
|
4221
4225
|
|
|
4226
|
+
**Carry what a reply points at (WhatsApp reply quote).** A WhatsApp sender replying to an earlier message produces a quote that `app/lib/whatsapp/inbound/extract.ts` resolves to `{ id, text?, sender? }` and `manager.ts` assigns to the route payload's `replyContext`. That field had no reader: the `server/index.ts` gateway argument omitted it, `AdminInbound` and the hub frame had no such field, and the channel service's `composePayload` emitted the caption and media notes only, so a message saying "prepare an email confirmation for this booking" reached the agent with nothing to resolve "this booking" against. It now rides both WhatsApp admin doors. On the persistent door it travels `server/index.ts` -> `AdminInbound` -> the hub frame the SSE route stringifies -> the channel service's `InboundPayload`, where `composeReplyNote` renders it as a bracketed note at the top of the payload, inside `## Context` and above the caption and any woven media notes: the quoted body is sender-controlled and belongs in the reframed untrusted zone, never in the platform preamble. Both channel-server inbound paths carry it, through the same `doorInboundPayload` seam that carries `standingRules` and the schedule provenance marker: the single-target SSE loop parses the whole frame, and the unified multi-target door reader rebuilds the payload field by field, so a field that seam omits is dropped on multi-target sessions alone. That seam is guarded by a test which reads the `InboundPayload` declaration and the rebuild function rather than a fixture, because the pre-existing byte-identity guard compared one hardcoded payload and so stayed green for every field added after it was written. On the passive account-manager door there is no channel server and no `## Context` frame, so the one-shot's whole initial prompt is read at face value; `composePassivePrompt` therefore puts the BARE sentence above the requirement and never the quoted body. The body arrives from the sender's own client and is never checked against the message store, so quoting it in a platform-voiced bracket on the door with no untrusted zone would let a modified client put words the agent never said into a `work-create` filing. The empty-text drop gate still tests the requirement alone, so a quote never rescues an inbound carrying no requirement of its own. The quoted body is capped at `QUOTED_TEXT_CAP` (500 characters, `…` appended when cut) because it is unbounded sender-controlled input, and the cut lands on whole characters so a body whose 500th boundary falls inside a surrogate pair cannot emit a lone surrogate at a position the sender chose. Only the channel service renders a body, so the cap lives there alone; platform/ui's `app/lib/whatsapp/reply-quote.ts` holds just the bare sentence for the passive door, and both packages hardcode that sentence in their own tests so the two doors cannot drift apart on the wording (the packages never cross-import, the same arrangement `REFRAME_INSTRUCTION` and `MediaRef` use). Telegram and webchat extract no quote at all, so neither carries one. `payloadBytes` measures the whole inner payload, so a reply carrying a quote produces a visibly larger `[reframe] op=applied … payloadBytes=<n>` than the same text without one, and a bare-text-length `payloadBytes` on an inbound whose store row has a non-null `quotedId` is the signature of this defect returning. Because the field is optional at every hop, dropping it from either `server/index.ts` argument would be silent (tsc stays green, and no unit test executes that wiring file), so the `check-reply-context-threaded` build gate asserts both argument sites.
|
|
4227
|
+
|
|
4222
4228
|
**Observability.** `[preference-write] op=reconcile … mode=<reinforce|update|contradict|new> similarity=…` — a stream of `mode=new` for subjects that already exist is the duplicate-minting signature. `[preference-inject] op=inject channel=<wa|tg|web> injected=<N> owner=<8> source=<owner|no-owner|owner-error|fetch-error>` — injection selects only the account owner's admin preferences, so `source=owner injected=0` is a genuinely empty owner, `source=no-owner` is an account with no seeded owner (injects nothing, a missed owner seeding), and `injected=0 source=owner` on an account whose owner holds rules is the write-only regression re-appearing. `[preference-audit] op=drift accountId=… storedActive=… injectedActive=… duplicateClusters=…` — the standing reconciliation audit (claude-session-manager, 5-minute tick) flags any account whose store holds more active rows than injection would surface, the no-event detector for drift.
|
|
4223
4229
|
|
|
4224
4230
|
---
|
|
@@ -73,11 +73,11 @@ The setup-done claim only fires when `curl -I https://<hostname>` issued from ou
|
|
|
73
73
|
|
|
74
74
|
The portal shows a sub-account's deliverable folders alongside what the client uploaded. Which folders those are is never a list in the code: it is derived per account from that account's own `SCHEMA.md` — the buckets in its `<!-- ontology-buckets -->` region plus `output`, intersected with its `allowed-top-level` block. A brand on a different vertical exposes different folders with no code change. `bin/schema-exposed-dirs.mjs` is the only place that decision is made. An operator can add non-ontology folders (a workflow folder such as `inbound-invoices`) by listing them in `exposeFolders` in the account's `data-portal.json`; each must match `[A-Za-z0-9_-]+` and is never a system folder (`agents`, `secrets`, `specialists`, `state`, `memory`, `cache`, `logs`, `tmp`) or dot-prefixed. Those extras union on top of the schema-derived set and, unlike ontology buckets, are not intersected with `allowed-top-level`; empty or absent behaves as before.
|
|
75
75
|
|
|
76
|
-
`bin/portal-index-push.mjs` walks those folders and full-replaces the account's rows in the portal's D1 `directory` table each cycle. Metadata only — names, sizes, times — so listing survives the device being offline, and a deleted or renamed file converges on the next cycle without a tombstone. The scheduling heartbeat runs it hourly for every account carrying a `data-portal.json`; it is also runnable standalone, which is how it stays operable while the heartbeat spawn path is unproven on a device.
|
|
76
|
+
`bin/portal-index-push.mjs` walks those folders and full-replaces the account's rows in the portal's D1 `directory` table each cycle. Metadata only — names, sizes, times — so listing survives the device being offline, and a deleted or renamed file converges on the next cycle without a tombstone. Every walked folder gets a row of its own marked `isDir`, not only the files inside it, so an exposed folder holding nothing at any depth still renders and opens; a folder whose `readdir` fails publishes nothing, so the tree never offers one the device does not have. `isDir` is a later column and `IF NOT EXISTS` adds nothing to an existing table, so a portal that predates it needs one hand-run `ALTER TABLE directory ADD COLUMN isDir INTEGER NOT NULL DEFAULT 0`. The scheduling heartbeat runs it hourly for every account carrying a `data-portal.json`; it is also runnable standalone, which is how it stays operable while the heartbeat spawn path is unproven on a device.
|
|
77
77
|
|
|
78
78
|
Opening a file reaches the install: the portal probes it, then mints an `/api/portal/fetch` link signed with the account's `PORTAL_FETCH_SECRET` and valid for five minutes. The install verifies the signature **and independently re-derives the exposed folder set**, so a correctly signed link for a non-exposed path is still refused. That second check is the security boundary, not the signature.
|
|
79
79
|
|
|
80
|
-
Exposure is narrowed twice. The install re-derives the account's exposed set from `SCHEMA.md` on every request and refuses anything outside it, which is the account-isolation boundary. On top of that, a person's `people.folders` grant (written at enrolment, empty means the full set) narrows them to named top-level folders inside that set. The grant lives only in the portal's D1, so a portal compromise widens a person back to the account's exposed set and never beyond it. `op=list` carries `granted=<n|full>` and an out-of-grant download logs `op=fetch-through result=not-granted`, distinct from `result=absent`.
|
|
80
|
+
Exposure is narrowed twice. The install re-derives the account's exposed set from `SCHEMA.md` on every request and refuses anything outside it, which is the account-isolation boundary. On top of that, a person's `people.folders` grant (written at enrolment, empty means the full set) narrows them to named top-level folders inside that set. The grant lives only in the portal's D1, so a portal compromise widens a person back to the account's exposed set and never beyond it. `op=list` carries `granted=<n|full>` and an out-of-grant download logs `op=fetch-through result=not-granted`, distinct from `result=absent`. A download naming a folder logs `result=is-directory` and mints no link, distinct from both.
|
|
81
81
|
|
|
82
82
|
What to watch, on the device:
|
|
83
83
|
|
|
@@ -94,6 +94,26 @@ What to watch, on the device:
|
|
|
94
94
|
|
|
95
95
|
`op=audit` is the standing check, and it is the only one that speaks when the push does **not**. It runs in-process inside the heartbeat rather than as a spawn, and reads only local state, so it cannot be blinded by the same failure it is watching for. It fires hourly even when everything is healthy — an audit that goes quiet on success is indistinguishable from an audit that stopped running. `result=never-ran` on an account with a `data-portal.json` means no cycle has ever succeeded for it; `result=stale` with `ageMin` means cycles have stopped. Its input is written only after `op=verify`, so a stale entry means the push stopped succeeding rather than merely stopped logging.
|
|
96
96
|
|
|
97
|
+
## Data-portal upload pull
|
|
98
|
+
|
|
99
|
+
The index push publishes what the device holds; the pull is the other direction. An upload lands in R2 under the person's prefix with one `manifest` row at `ingested = 0`, and the `portal-uploads-pull` loop in the admin server carries it onto the device every minute. It does not live in this plugin: it sits beside `runDataPortalAudit` in `platform/ui/server/portal-uploads-pull.ts`, because that audit already resolves the house Cloudflare credential and each account's portal database and bucket, and a second enumeration or credential path is how the two would drift.
|
|
100
|
+
|
|
101
|
+
Per pending row the order is fetch, write, verify, then claim. The object is fetched by `objectKey` through the storage broker, written to a dot-prefixed `.uploads-tmp/` partial (invisible to the index walk, so a partial can never be published as a deliverable), renamed into `uploads/<ownerName>/<filename>`, and only then is `ingested` set to 1 — and only if the written size equals `manifest.size`. A row claimed before the bytes land is a lost file carrying a flag that says it arrived. Anything that fails leaves the row at 0 and it is retried next cycle. `ownerName` and `filename` must each be a single path segment, checked on the device rather than trusted from the portal's own gate, and `fileId` must be a UUID before it reaches a statement, because `d1Query` takes raw SQL and binds nothing.
|
|
102
|
+
|
|
103
|
+
What to watch, on the device:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
[portal-pull] op=claim account=<id> fileId=<uuid> bytes=<n> result=written|fetch-failed|write-failed|verify-failed|claim-failed|bad-owner|bad-filename|bad-fileid
|
|
107
|
+
[portal-pull] op=done account=<id> pulled=<n> failed=<n>
|
|
108
|
+
[portal-pull] op=malformed account=<id> rows=<n>
|
|
109
|
+
[portal-pull] op=failed account=<id> err="<message>"
|
|
110
|
+
[data-portal-audit] account=<id> … oldestUningestedHrs=<n|na> pendingRows=<n|na>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`[data-portal-audit]` is the standing signal, printed every cycle including at `pendingRows=0`, so a quiet line reads as healthy rather than as a stopped pull. When a client says a file never arrived: a `pendingRows` that stays non-zero with a rising `oldestUningestedHrs` means the pull runs and cannot finish, so read `result=` on `op=claim`. `pendingRows=na` means the manifest read itself failed, a different fault. No `[data-portal-audit]` line at all means the audit loop is not running, a third. Both counts are computed from the manifest alone, so they survive R2 being unreachable — which is exactly when a stalled pull matters most. `result=claim-failed` is the one outcome that leaves a file on disk with its row unclaimed; the next cycle re-fetches and overwrites the same path, which is deliberate.
|
|
114
|
+
|
|
115
|
+
Log lines carry counts and ids only. An object key is `<ownerId>/<filename>`, a real person's filename, and the brand journal is not where those belong.
|
|
116
|
+
|
|
97
117
|
On the portal side `[data-portal] op=list` carries `account=` and `op=fetch-through` carries the outcome; on the install `[portal-fetch]` logs `op=request` then exactly one of `op=served`, `op=denied`, `op=miss`. A portal `fetch-through` with no matching install `op=request` means the request never arrived, rather than having been refused. `[portal-fetch] op=no-secret account=<id>` is an unconfigured portal, which the `op=denied reason=secret` line alone would not distinguish from a bad signature.
|
|
98
118
|
|
|
99
119
|
## Discipline
|
|
@@ -57,14 +57,23 @@ export async function readExposeFolders(accountDir) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* Files under `exposed` dirs, at any depth
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* Files AND directories under `exposed` dirs, at any depth, each row carrying
|
|
61
|
+
* `isDir`. Directories are published rather than left to be derived from file
|
|
62
|
+
* prefixes, because a folder holding no files at any depth has no prefix to be
|
|
63
|
+
* derived from and would not exist to the client at all.
|
|
64
|
+
*
|
|
65
|
+
* A directory row is emitted only after its own `readdir` succeeds, so an
|
|
66
|
+
* exposed name that is absent or unreadable on disk still contributes nothing
|
|
67
|
+
* and the portal never renders a folder the device does not have.
|
|
68
|
+
*
|
|
69
|
+
* Dot-prefixed entries are skipped at every level: `.uploads-tmp/` holds
|
|
70
|
+
* in-flight upload partials (files.ts:25) and must never be published as a
|
|
71
|
+
* deliverable, as a file row or as a folder.
|
|
63
72
|
* @param {string} accountDir @param {string[]} exposed
|
|
64
|
-
* @returns {Promise<{relPath: string, sizeBytes: number, modifiedAt: string}[]>}
|
|
73
|
+
* @returns {Promise<{relPath: string, sizeBytes: number, modifiedAt: string, isDir: boolean}[]>}
|
|
65
74
|
*/
|
|
66
75
|
export async function walkExposed(accountDir, exposed) {
|
|
67
|
-
/** @type {{relPath: string, sizeBytes: number, modifiedAt: string}[]} */
|
|
76
|
+
/** @type {{relPath: string, sizeBytes: number, modifiedAt: string, isDir: boolean}[]} */
|
|
68
77
|
const out = []
|
|
69
78
|
|
|
70
79
|
/** @param {string} rel */
|
|
@@ -75,6 +84,14 @@ export async function walkExposed(accountDir, exposed) {
|
|
|
75
84
|
} catch {
|
|
76
85
|
return // absent or unreadable dir contributes nothing
|
|
77
86
|
}
|
|
87
|
+
try {
|
|
88
|
+
const st = await stat(join(accountDir, rel))
|
|
89
|
+
// sizeBytes 0 rather than the directory inode's own size: the column
|
|
90
|
+
// means "bytes a client can fetch", and a folder has none.
|
|
91
|
+
out.push({ relPath: rel, sizeBytes: 0, modifiedAt: st.mtime.toISOString(), isDir: true })
|
|
92
|
+
} catch {
|
|
93
|
+
// vanished between the readdir and the stat; the next cycle settles it
|
|
94
|
+
}
|
|
78
95
|
for (const e of entries) {
|
|
79
96
|
if (e.name.startsWith('.')) continue
|
|
80
97
|
const childRel = `${rel}/${e.name}`
|
|
@@ -83,7 +100,12 @@ export async function walkExposed(accountDir, exposed) {
|
|
|
83
100
|
} else if (e.isFile()) {
|
|
84
101
|
try {
|
|
85
102
|
const st = await stat(join(accountDir, childRel))
|
|
86
|
-
out.push({
|
|
103
|
+
out.push({
|
|
104
|
+
relPath: childRel,
|
|
105
|
+
sizeBytes: st.size,
|
|
106
|
+
modifiedAt: st.mtime.toISOString(),
|
|
107
|
+
isDir: false,
|
|
108
|
+
})
|
|
87
109
|
} catch {
|
|
88
110
|
// vanished mid-walk; the next cycle settles it
|
|
89
111
|
}
|
|
@@ -159,8 +181,8 @@ export async function pushAccount(opts) {
|
|
|
159
181
|
await client.query('DELETE FROM directory WHERE accountId = ?', [accountId])
|
|
160
182
|
for (const row of rows) {
|
|
161
183
|
await client.query(
|
|
162
|
-
'INSERT INTO directory (accountId, relPath, sizeBytes, modifiedAt, indexedAt) VALUES (?, ?, ?, ?, ?)',
|
|
163
|
-
[accountId, row.relPath, row.sizeBytes, row.modifiedAt, nowIso],
|
|
184
|
+
'INSERT INTO directory (accountId, relPath, sizeBytes, modifiedAt, indexedAt, isDir) VALUES (?, ?, ?, ?, ?, ?)',
|
|
185
|
+
[accountId, row.relPath, row.sizeBytes, row.modifiedAt, nowIso, row.isDir ? 1 : 0],
|
|
164
186
|
)
|
|
165
187
|
}
|
|
166
188
|
log(`${TAG} op=replace account=${accountId} deleted=all inserted=${rows.length}`)
|
|
@@ -2,19 +2,28 @@ import { describe, it, expect } from 'vitest'
|
|
|
2
2
|
import { processFiles, entriesUnder } from '../../skills/data-portal/template/functions/api/files'
|
|
3
3
|
import type { PortalEnv } from '../../skills/data-portal/template/functions/api/_lib/types'
|
|
4
4
|
|
|
5
|
+
// Mirrors what the push writes: a row per folder as well as per file. `jobs` is
|
|
6
|
+
// exposed and holds nothing, which is the case that had no row at all before.
|
|
5
7
|
const DIRECTORY = [
|
|
6
|
-
{ accountId: 'acc-a', relPath: 'output
|
|
7
|
-
{ accountId: 'acc-a', relPath: '
|
|
8
|
-
{ accountId: 'acc-a', relPath: 'quotes
|
|
9
|
-
{ accountId: 'acc-
|
|
8
|
+
{ accountId: 'acc-a', relPath: 'output', sizeBytes: 0, modifiedAt: '2026-07-20T10:00:00.000Z', isDir: 1 },
|
|
9
|
+
{ accountId: 'acc-a', relPath: 'output/report.pdf', sizeBytes: 10, modifiedAt: '2026-07-20T10:00:00.000Z', isDir: 0 },
|
|
10
|
+
{ accountId: 'acc-a', relPath: 'quotes', sizeBytes: 0, modifiedAt: '2026-07-20T12:00:00.000Z', isDir: 1 },
|
|
11
|
+
{ accountId: 'acc-a', relPath: 'quotes/CR2969', sizeBytes: 0, modifiedAt: '2026-07-20T11:00:00.000Z', isDir: 1 },
|
|
12
|
+
{ accountId: 'acc-a', relPath: 'quotes/CR2969/quote.pdf', sizeBytes: 20, modifiedAt: '2026-07-20T11:00:00.000Z', isDir: 0 },
|
|
13
|
+
{ accountId: 'acc-a', relPath: 'quotes/CR2974', sizeBytes: 0, modifiedAt: '2026-07-20T12:00:00.000Z', isDir: 1 },
|
|
14
|
+
{ accountId: 'acc-a', relPath: 'quotes/CR2974/quote.pdf', sizeBytes: 30, modifiedAt: '2026-07-20T12:00:00.000Z', isDir: 0 },
|
|
15
|
+
{ accountId: 'acc-a', relPath: 'jobs', sizeBytes: 0, modifiedAt: '2026-07-20T14:00:00.000Z', isDir: 1 },
|
|
16
|
+
{ accountId: 'acc-b', relPath: 'output/secret.pdf', sizeBytes: 40, modifiedAt: '2026-07-20T13:00:00.000Z', isDir: 0 },
|
|
10
17
|
]
|
|
11
18
|
|
|
12
19
|
function makeEnv(
|
|
13
20
|
sessions: Record<string, { ownerId: string; accountId: string; folders?: string }>,
|
|
21
|
+
onQuery: (query: string) => void = () => {},
|
|
14
22
|
) {
|
|
15
23
|
return {
|
|
16
24
|
DB: {
|
|
17
25
|
prepare(query: string) {
|
|
26
|
+
onQuery(query)
|
|
18
27
|
let bound: unknown[] = []
|
|
19
28
|
const stmt = {
|
|
20
29
|
bind(...v: unknown[]) {
|
|
@@ -47,9 +56,9 @@ type Entry = { name: string; kind: string; relPath: string; sizeBytes: number |
|
|
|
47
56
|
describe('entriesUnder', () => {
|
|
48
57
|
const rows = DIRECTORY.filter((d) => d.accountId === 'acc-a')
|
|
49
58
|
|
|
50
|
-
it('
|
|
59
|
+
it('lists top-level folders, including one holding no files', () => {
|
|
51
60
|
const e = entriesUnder(rows, '')
|
|
52
|
-
expect(e.map((x) => x.name)).toEqual(['output', 'quotes'])
|
|
61
|
+
expect(e.map((x) => x.name)).toEqual(['jobs', 'output', 'quotes'])
|
|
53
62
|
expect(e.every((x) => x.kind === 'directory')).toBe(true)
|
|
54
63
|
})
|
|
55
64
|
|
|
@@ -57,6 +66,42 @@ describe('entriesUnder', () => {
|
|
|
57
66
|
expect(entriesUnder(rows, 'quotes').map((x) => x.name)).toEqual(['CR2969', 'CR2974'])
|
|
58
67
|
})
|
|
59
68
|
|
|
69
|
+
// An empty folder must open, not read as missing. Its own row is what puts it
|
|
70
|
+
// in the parent listing; inside it there is nothing to show.
|
|
71
|
+
it('opens an empty folder to an empty listing', () => {
|
|
72
|
+
expect(entriesUnder(rows, 'jobs')).toEqual([])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// The explicit row and the row implied by a nested file name the same folder.
|
|
76
|
+
// Two entries for one folder would render it twice.
|
|
77
|
+
it('collapses an explicit folder row and one implied by a nested file', () => {
|
|
78
|
+
const both = [
|
|
79
|
+
{ relPath: 'quotes', sizeBytes: 0, modifiedAt: '2026-07-20T12:00:00.000Z', isDir: 1 },
|
|
80
|
+
{ relPath: 'quotes/CR2969/quote.pdf', sizeBytes: 20, modifiedAt: '2026-07-20T11:00:00.000Z', isDir: 0 },
|
|
81
|
+
]
|
|
82
|
+
const e = entriesUnder(both, '')
|
|
83
|
+
expect(e.length).toBe(1)
|
|
84
|
+
expect(e[0]).toEqual({
|
|
85
|
+
name: 'quotes',
|
|
86
|
+
kind: 'directory',
|
|
87
|
+
relPath: 'quotes',
|
|
88
|
+
sizeBytes: null,
|
|
89
|
+
modifiedAt: null,
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Without the isDir check a folder row has no slash left after the prefix and
|
|
94
|
+
// lands in the file list, where clicking it would try to download a folder.
|
|
95
|
+
it('never renders a folder row as a file', () => {
|
|
96
|
+
const e = entriesUnder(
|
|
97
|
+
[{ relPath: 'jobs', sizeBytes: 0, modifiedAt: '2026-07-20T14:00:00.000Z', isDir: 1 }],
|
|
98
|
+
'',
|
|
99
|
+
)
|
|
100
|
+
expect(e).toEqual([
|
|
101
|
+
{ name: 'jobs', kind: 'directory', relPath: 'jobs', sizeBytes: null, modifiedAt: null },
|
|
102
|
+
])
|
|
103
|
+
})
|
|
104
|
+
|
|
60
105
|
it('lists files inside a leaf folder with size and time', () => {
|
|
61
106
|
expect(entriesUnder(rows, 'quotes/CR2969')).toEqual([
|
|
62
107
|
{
|
|
@@ -71,9 +116,9 @@ describe('entriesUnder', () => {
|
|
|
71
116
|
|
|
72
117
|
it('puts folders before files and files newest first', () => {
|
|
73
118
|
const mixed = [
|
|
74
|
-
{ relPath: 'output/old.pdf', sizeBytes: 1, modifiedAt: '2026-01-01T00:00:00.000Z' },
|
|
75
|
-
{ relPath: 'output/new.pdf', sizeBytes: 1, modifiedAt: '2026-09-01T00:00:00.000Z' },
|
|
76
|
-
{ relPath: 'output/sub/x.pdf', sizeBytes: 1, modifiedAt: '2026-05-01T00:00:00.000Z' },
|
|
119
|
+
{ relPath: 'output/old.pdf', sizeBytes: 1, modifiedAt: '2026-01-01T00:00:00.000Z', isDir: 0 },
|
|
120
|
+
{ relPath: 'output/new.pdf', sizeBytes: 1, modifiedAt: '2026-09-01T00:00:00.000Z', isDir: 0 },
|
|
121
|
+
{ relPath: 'output/sub/x.pdf', sizeBytes: 1, modifiedAt: '2026-05-01T00:00:00.000Z', isDir: 0 },
|
|
77
122
|
]
|
|
78
123
|
expect(entriesUnder(mixed, 'output').map((x) => x.name)).toEqual(['sub', 'new.pdf', 'old.pdf'])
|
|
79
124
|
})
|
|
@@ -85,9 +130,18 @@ describe('entriesUnder', () => {
|
|
|
85
130
|
})
|
|
86
131
|
|
|
87
132
|
describe('processFiles directory listing', () => {
|
|
88
|
-
it('returns
|
|
133
|
+
it('returns the account folders, including one holding no files', async () => {
|
|
89
134
|
const r = await processFiles('sess-a', makeEnv(SESSIONS), () => {}, 0, '')
|
|
90
|
-
expect((r.payload.entries as Entry[]).map((e) => e.name)).toEqual(['output', 'quotes'])
|
|
135
|
+
expect((r.payload.entries as Entry[]).map((e) => e.name)).toEqual(['jobs', 'output', 'quotes'])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// The column is what tells a folder row from a file row; a SELECT that drops
|
|
139
|
+
// it renders every folder as an unopenable file.
|
|
140
|
+
it('reads isDir in the directory select', async () => {
|
|
141
|
+
const queries: string[] = []
|
|
142
|
+
const env = makeEnv(SESSIONS, (q) => queries.push(q))
|
|
143
|
+
await processFiles('sess-a', env, () => {}, 0, '')
|
|
144
|
+
expect(queries.some((q) => /FROM directory/i.test(q) && /isDir/.test(q))).toBe(true)
|
|
91
145
|
})
|
|
92
146
|
|
|
93
147
|
it('never leaks another account rows', async () => {
|
|
@@ -129,15 +183,23 @@ describe('processFiles directory listing', () => {
|
|
|
129
183
|
it('an empty grant shows the full set', async () => {
|
|
130
184
|
const env = makeEnv({ 'sess-a': { ownerId: 'alice', accountId: 'acc-a', folders: '' } })
|
|
131
185
|
const r = await processFiles('sess-a', env, () => {}, 0, '')
|
|
132
|
-
expect((r.payload.entries as Entry[]).map((e) => e.name)).toEqual(['output', 'quotes'])
|
|
186
|
+
expect((r.payload.entries as Entry[]).map((e) => e.name)).toEqual(['jobs', 'output', 'quotes'])
|
|
133
187
|
})
|
|
134
188
|
|
|
135
189
|
it('a grant naming an absent folder shows nothing', async () => {
|
|
136
|
-
const env = makeEnv({ 'sess-a': { ownerId: 'alice', accountId: 'acc-a', folders: '
|
|
190
|
+
const env = makeEnv({ 'sess-a': { ownerId: 'alice', accountId: 'acc-a', folders: 'parts' } })
|
|
137
191
|
const r = await processFiles('sess-a', env, () => {}, 0, '')
|
|
138
192
|
expect(r.payload.entries).toEqual([])
|
|
139
193
|
})
|
|
140
194
|
|
|
195
|
+
// The grant narrows the account's exposed set, and an empty folder is inside
|
|
196
|
+
// that set like any other: outside the grant it stays hidden.
|
|
197
|
+
it('an empty folder outside the grant stays hidden', async () => {
|
|
198
|
+
const env = makeEnv({ 'sess-a': { ownerId: 'alice', accountId: 'acc-a', folders: 'quotes' } })
|
|
199
|
+
const r = await processFiles('sess-a', env, () => {}, 0, '')
|
|
200
|
+
expect((r.payload.entries as Entry[]).map((e) => e.name)).not.toContain('jobs')
|
|
201
|
+
})
|
|
202
|
+
|
|
141
203
|
it('the grant holds when browsing into a granted folder', async () => {
|
|
142
204
|
const env = makeEnv({ 'sess-a': { ownerId: 'alice', accountId: 'acc-a', folders: 'quotes' } })
|
|
143
205
|
const r = await processFiles('sess-a', env, () => {}, 0, 'quotes')
|