@teamscala/service-boot 0.23.431 → 0.23.433
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 +12 -19
- package/src/agreement-gate/signing-shell.ts +19 -15
- package/src/attach-proposal-accept.ts +5 -2
- package/src/auth-gate/public-paths.ts +7 -16
- package/src/auth-gate.ts +45 -42
- package/src/bind-page-handler.ts +17 -32
- package/src/build-client-assets/build-lock.ts +15 -6
- package/src/build-client-assets/hashed-output.ts +3 -1
- package/src/build-client-assets/hydrate-bundle.ts +50 -57
- package/src/build-client-assets/input-fingerprint.ts +41 -57
- package/src/build-client-assets/input-hashing.ts +2 -4
- package/src/build-client-assets/manifest.ts +8 -11
- package/src/build-client-assets/source-candidates.ts +8 -16
- package/src/build-client-assets/sourcemaps-upload.ts +14 -10
- package/src/build-client-assets/styles-build.ts +20 -19
- package/src/build-client-assets.test.ts +2 -2
- package/src/build-client-assets.ts +13 -22
- package/src/content-gate/access.ts +5 -2
- package/src/content-gate/catalog.ts +20 -18
- package/src/content-gate/gate.ts +4 -3
- package/src/discover-blocks.ts +5 -7
- package/src/fn-routers/agreement-certificate.ts +1 -1
- package/src/fn-routers/agreement-send-router.ts +5 -4
- package/src/fn-routers/agreement-sign-side-effects.ts +24 -22
- package/src/fn-routers/assessment-report-email.ts +3 -2
- package/src/fn-routers/auth-router.ts +20 -21
- package/src/fn-routers/chat-session-router-shared.ts +12 -3
- package/src/fn-routers/chat-session-router.ts +8 -6
- package/src/fn-routers/conversation-ownership.ts +3 -2
- package/src/fn-routers/messaging-whatsapp-router.test.ts +3 -1
- package/src/fn-routers/messaging-whatsapp-router.ts +17 -3
- package/src/fn-routers/persist-chat-messages.ts +6 -4
- package/src/fn-routers/proposal-send-router.ts +4 -3
- package/src/fn-routers/public-agreement-router.ts +5 -4
- package/src/fn-routers/public-assessment-router.ts +5 -4
- package/src/fn-routers/public-gpt-session-router.ts +17 -12
- package/src/fn-routers/user-preferences-router.ts +6 -15
- package/src/fn-routers/work-items-router.ts +18 -17
- package/src/handlers/platform-fault-escalation/orpc-client.ts +35 -18
- package/src/handlers/projects-digest.ts +1 -1
- package/src/handlers/scala-tools-currency-watch.test.ts +1 -5
- package/src/handlers/scala-tools-currency-watch.ts +20 -32
- package/src/handlers/singleton-tools-currency-watch.test.ts +1 -5
- package/src/handlers/singleton-tools-currency-watch.ts +21 -19
- package/src/handlers/upstream-issue-sync.ts +11 -4
- package/src/handlers/whatsapp-liveness.ts +14 -4
- package/src/install-harness.ts +1 -1
- package/src/install-logging.ts +1 -1
- package/src/mount-orpc-router.test.ts +56 -10
- package/src/mount-orpc-router.ts +23 -9
- package/src/mount-server-packages.ts +14 -15
- package/src/pg-boss-mount.ts +5 -12
- package/src/proposal-gate.ts +7 -2
- package/src/read-html-template.test.ts +12 -15
- package/src/read-html-template.ts +2 -1
- package/src/readiness-gate.ts +2 -1
- package/src/register-crash-handlers.ts +4 -8
- package/src/register-page-server-client.ts +12 -19
- package/src/register-registry-page-route/ai-chat-prefetch.ts +13 -12
- package/src/register-registry-page-route/apply-page-branding.ts +59 -56
- package/src/register-registry-page-route/graph-cache.ts +19 -18
- package/src/register-registry-page-route/meta-title.ts +21 -20
- package/src/register-registry-page-route/prefetch-org-id.ts +3 -2
- package/src/register-registry-page-route/proposal-approval-email-token.ts +2 -3
- package/src/register-registry-page-route/run-content-gates.ts +2 -4
- package/src/register-registry-page-route.ts +1 -2
- package/src/register-static-assets.ts +6 -5
- package/src/resolve-app-base-url.ts +12 -5
- package/src/resolvers/portal-route-paths.ts +12 -11
- package/src/resource-chat-inject.ts +75 -72
- package/src/resource-not-found.ts +20 -19
- package/src/scala-tools-currency-watchdog.ts +41 -12
- package/src/two-phase-full-app.ts +52 -29
- package/src/two-phase-minimal-app.ts +33 -31
- package/src/two-phase-readiness.ts +41 -38
- package/src/ui-service.ts +41 -38
- package/src/handlers/pane-outbox-drain.ts +0 -39
- package/src/handlers/prune-stale-data.ts +0 -110
package/src/ui-service.ts
CHANGED
|
@@ -8,6 +8,47 @@
|
|
|
8
8
|
* served by @teamscala/service-runtime/serve (one Bun.serve in dev and prod).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Boots a UI service using two-phase boot for fast availability.
|
|
13
|
+
*
|
|
14
|
+
* Phase 1 (fast): Build and serve minimal tool-only app (ORPC, data routes,
|
|
15
|
+
* auth gates). This becomes available within ~10s, enabling the MCP tool
|
|
16
|
+
* surface to accept requests immediately after restart.
|
|
17
|
+
*
|
|
18
|
+
* Phase 2 (background): Build full app (pages, hydration) and atomically swap
|
|
19
|
+
* it in. The readiness gate flips to ready after Phase 1, not Phase 2.
|
|
20
|
+
*
|
|
21
|
+
* The interim mitigation (503 + gateway retry + circuit breaker) remains in
|
|
22
|
+
* place and is NOT undone.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Resolve the listen port through the SHARED resolver (`resolveServicePort`),
|
|
26
|
+
// the same one the backend branch uses. The registry wins wherever it has an
|
|
27
|
+
// opinion and a contradicting PORT env is reported, never silently obeyed —
|
|
28
|
+
// full reasoning in that file.
|
|
29
|
+
//
|
|
30
|
+
// This branch previously took `process.env.PORT` unconditionally, which is
|
|
31
|
+
// how scala-quiz (declared 5203) booted onto session-picker's 3021 from a
|
|
32
|
+
// leaked env and silently split that daemon's traffic (2026-07-31). On the
|
|
33
|
+
// dev host the registry wins a conflict (below). On Railway the injected PORT
|
|
34
|
+
// wins instead — Railway healthchecks exactly that port, and the row's port
|
|
35
|
+
// is the service's LOCAL dev port (e.g. portal 5200), so binding it on Railway
|
|
36
|
+
// makes the healthcheck probe a dead port and the deploy fails (`isRailway`
|
|
37
|
+
// path in the resolver).
|
|
38
|
+
|
|
39
|
+
// NO-SILENT-REVERTS — surface the failed client build to OBSERVABILITY
|
|
40
|
+
// (event-log → GlitchTip, per event-log-is-the-only-durable-event-log).
|
|
41
|
+
// The minimal-app fallback keeps serving SSR from the LAST-successful
|
|
42
|
+
// manifest (now a tombstone, see build-client-assets.ts), so the page
|
|
43
|
+
// looks "up" while the client bundle is stale. Without this signal the
|
|
44
|
+
// failure is buried in boot stderr and a published fix that reached
|
|
45
|
+
// node_modules but failed to bundle ships invisibly stale (the
|
|
46
|
+
// scala-agents 2026-07-18 incident: ui-foundation@3.4.5 installed but
|
|
47
|
+
// served pre-fix bytes across restarts while node_modules was
|
|
48
|
+
// mid-install). The boot.client-build-failed event routes into
|
|
49
|
+
// event-log's 7-day retention → surfaces in GlitchTip as an incident
|
|
50
|
+
// trail the operator actually watches.
|
|
51
|
+
|
|
11
52
|
import { recordEvent } from "@teamscala/event-log/record-event";
|
|
12
53
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
13
54
|
import { BootFatalError } from "@teamscala/os/errors/boot-error";
|
|
@@ -46,19 +87,6 @@ export interface PageServerDeps {
|
|
|
46
87
|
getSharedDb?: () => unknown;
|
|
47
88
|
}
|
|
48
89
|
|
|
49
|
-
/**
|
|
50
|
-
* Boots a UI service using two-phase boot for fast availability.
|
|
51
|
-
*
|
|
52
|
-
* Phase 1 (fast): Build and serve minimal tool-only app (ORPC, data routes,
|
|
53
|
-
* auth gates). This becomes available within ~10s, enabling the MCP tool
|
|
54
|
-
* surface to accept requests immediately after restart.
|
|
55
|
-
*
|
|
56
|
-
* Phase 2 (background): Build full app (pages, hydration) and atomically swap
|
|
57
|
-
* it in. The readiness gate flips to ready after Phase 1, not Phase 2.
|
|
58
|
-
*
|
|
59
|
-
* The interim mitigation (503 + gateway retry + circuit breaker) remains in
|
|
60
|
-
* place and is NOT undone.
|
|
61
|
-
*/
|
|
62
90
|
export async function bootUiService(
|
|
63
91
|
config: ServiceInstallConfig,
|
|
64
92
|
serviceSlug: string,
|
|
@@ -77,19 +105,6 @@ export async function bootUiService(
|
|
|
77
105
|
deps.getSharedDb,
|
|
78
106
|
);
|
|
79
107
|
|
|
80
|
-
// Resolve the listen port through the SHARED resolver (`resolveServicePort`),
|
|
81
|
-
// the same one the backend branch uses. The registry wins wherever it has an
|
|
82
|
-
// opinion and a contradicting PORT env is reported, never silently obeyed —
|
|
83
|
-
// full reasoning in that file.
|
|
84
|
-
//
|
|
85
|
-
// This branch previously took `process.env.PORT` unconditionally, which is
|
|
86
|
-
// how scala-quiz (declared 5203) booted onto session-picker's 3021 from a
|
|
87
|
-
// leaked env and silently split that daemon's traffic (2026-07-31). On the
|
|
88
|
-
// dev host the registry wins a conflict (below). On Railway the injected PORT
|
|
89
|
-
// wins instead — Railway healthchecks exactly that port, and the row's port
|
|
90
|
-
// is the service's LOCAL dev port (e.g. portal 5200), so binding it on Railway
|
|
91
|
-
// makes the healthcheck probe a dead port and the deploy fails (`isRailway`
|
|
92
|
-
// path in the resolver).
|
|
93
108
|
const isRailway = Boolean(process.env.RAILWAY_ENVIRONMENT);
|
|
94
109
|
const envPort = process.env.PORT ? Number(process.env.PORT) : undefined;
|
|
95
110
|
// The APP binds its PRIVATE port (app_port) when present — for scale-to-zero
|
|
@@ -160,18 +175,6 @@ export async function bootUiService(
|
|
|
160
175
|
log.error(
|
|
161
176
|
`[ui-service-boot:${serviceSlug}] Failed to build full app, continuing with minimal app: ${reason}`,
|
|
162
177
|
);
|
|
163
|
-
// NO-SILENT-REVERTS — surface the failed client build to OBSERVABILITY
|
|
164
|
-
// (event-log → GlitchTip, per event-log-is-the-only-durable-event-log).
|
|
165
|
-
// The minimal-app fallback keeps serving SSR from the LAST-successful
|
|
166
|
-
// manifest (now a tombstone, see build-client-assets.ts), so the page
|
|
167
|
-
// looks "up" while the client bundle is stale. Without this signal the
|
|
168
|
-
// failure is buried in boot stderr and a published fix that reached
|
|
169
|
-
// node_modules but failed to bundle ships invisibly stale (the
|
|
170
|
-
// scala-agents 2026-07-18 incident: ui-foundation@3.4.5 installed but
|
|
171
|
-
// served pre-fix bytes across restarts while node_modules was
|
|
172
|
-
// mid-install). The boot.client-build-failed event routes into
|
|
173
|
-
// event-log's 7-day retention → surfaces in GlitchTip as an incident
|
|
174
|
-
// trail the operator actually watches.
|
|
175
178
|
recordEvent({
|
|
176
179
|
kind: "boot.client-build-failed",
|
|
177
180
|
payload: {
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system server-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The `pane-outbox-drain` pg-boss handler — delivers messages the operator's
|
|
7
|
-
* WhatsApp PA queues into `pane_messages` to the addressed agent pane.
|
|
8
|
-
*
|
|
9
|
-
* WHY THE REGISTRATION LIVES HERE AND THE WORK LIVES IN @teamscala/channel.
|
|
10
|
-
* The delivery itself belongs to the channel capability, so `paneOutboxDrainTick`
|
|
11
|
-
* is owned by `@teamscala/channel` beside the dispatch handler it calls. But
|
|
12
|
-
* `pgBossHandlerRegistry` is SERVER tier and `@teamscala/channel` is ADAPTER
|
|
13
|
-
* tier, so the channel package cannot register a pg-boss handler without an
|
|
14
|
-
* upward dependency — forbidden by `vertical-dependency-only`, which has no
|
|
15
|
-
* carve-out. server → adapter is DOWNWARD, so the registration sits here and
|
|
16
|
-
* imports the work, exactly as the four sibling handlers in this directory do.
|
|
17
|
-
*
|
|
18
|
-
* The MountContext route (`ctx.registerScheduledJob`) is tier-legal but NOT
|
|
19
|
-
* usable: `start-scheduled-jobs.ts` offers only `long_running` (invoked once,
|
|
20
|
-
* runs forever) and `recurring` (whose handlers "manage their own setInterval").
|
|
21
|
-
* Both are the in-process timer that `pg-boss-for-background-work` forbids and
|
|
22
|
-
* that the timer half of `in-memory-state-is-not-durable` describes — a timer
|
|
23
|
-
* only fires if its host outlives it, so on a restart-prone service it silently
|
|
24
|
-
* never runs while logging that it started. pg-boss is the only durable option,
|
|
25
|
-
* because the job row outlives any worker.
|
|
26
|
-
*
|
|
27
|
-
* Scoped to scala-channel by its `scheduled_job` row's `service_slug`, so it
|
|
28
|
-
* runs on the service that owns the channel rather than on scala-agents, whose
|
|
29
|
-
* UI-asset build failures used to take PA delivery down with them.
|
|
30
|
-
*
|
|
31
|
-
* Registered under slug `@teamscala/service-boot:pane-outbox-drain`.
|
|
32
|
-
*/
|
|
33
|
-
import { paneOutboxDrainTick } from "@teamscala/channel/pane-outbox/drain-tick";
|
|
34
|
-
import { pgBossHandlerRegistry } from "../pg-boss-handler-registry.ts";
|
|
35
|
-
|
|
36
|
-
pgBossHandlerRegistry.register(
|
|
37
|
-
"@teamscala/service-boot:pane-outbox-drain",
|
|
38
|
-
paneOutboxDrainTick,
|
|
39
|
-
);
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system server-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The `db-prune-stale-data` pg-boss handler. Reads the `retention` map from the
|
|
7
|
-
* `scheduled_job` row config and prunes stale rows per entry.
|
|
8
|
-
*
|
|
9
|
-
* Writes use Prisma ORM methods (deleteMany/updateMany) via the prisma-registry
|
|
10
|
-
* client — NEVER raw SQL. The `@teamscala/db` client carries the `blockRawSqlWrites`
|
|
11
|
-
* audit extension: `$executeRaw`/`$executeRawUnsafe` are blocked so every write
|
|
12
|
-
* is audited; ORM methods are the canonical audited path.
|
|
13
|
-
*
|
|
14
|
-
* SAFETY — identifier allowlist: each retention key selects a FIXED, hardcoded
|
|
15
|
-
* Prisma model + field (table/field names are literal in code, never derived
|
|
16
|
-
* from the row); the row's value (a duration string) is parsed to a cutoff Date.
|
|
17
|
-
* A row can therefore never inject arbitrary SQL or address an unvetted model.
|
|
18
|
-
* Keys not in the allowlist are skipped + logged.
|
|
19
|
-
*
|
|
20
|
-
* `audit_trail.*` is INTENTIONALLY EXCLUDED: `audit-trail-permanent` (critical)
|
|
21
|
-
* forbids any DELETE FROM audit_trail. The legacy row's
|
|
22
|
-
* `audit_trail.sync_entities: "delete all"` entry is stale drift from before
|
|
23
|
-
* the 2026-06-19 hardening — surfaced (skipped + warned), never executed.
|
|
24
|
-
*
|
|
25
|
-
* Registered under slug `@teamscala/service-boot:prune-stale-data`.
|
|
26
|
-
*/
|
|
27
|
-
import type { ScheduledJobConfig } from "@teamscala/db-validation/registry-schemas/scheduled-job";
|
|
28
|
-
import { Prisma } from "@teamscala/db/client";
|
|
29
|
-
import { getPrisma } from "@teamscala/db/prisma-registry";
|
|
30
|
-
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
31
|
-
import { pgBossHandlerRegistry } from "../pg-boss-handler-registry.ts";
|
|
32
|
-
|
|
33
|
-
const DURATION_RE = /^(\d+)\s+(second|minute|hour|day|week|month|year)s?$/;
|
|
34
|
-
const UNIT_TO_MS: Record<string, number> = {
|
|
35
|
-
second: 1_000,
|
|
36
|
-
minute: 60_000,
|
|
37
|
-
hour: 3_600_000,
|
|
38
|
-
day: 86_400_000,
|
|
39
|
-
week: 604_800_000,
|
|
40
|
-
month: 2_592_000_000,
|
|
41
|
-
year: 31_536_000_000,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
type PrismaClient = ReturnType<typeof getPrisma>;
|
|
45
|
-
type PruneOp = (prisma: PrismaClient, cutoff: Date) => Promise<number>;
|
|
46
|
-
|
|
47
|
-
// Fixed ops — model + field names literal in code, cutoff Date is the only
|
|
48
|
-
// variable. Adding a prunable target = adding an entry here (and confirming the
|
|
49
|
-
// model/field + audit-trail-permanent scope).
|
|
50
|
-
const PRUNE_OPS: Record<string, PruneOp> = {
|
|
51
|
-
"fleet_run_events": async (prisma, cutoff) =>
|
|
52
|
-
(await prisma.fleet_run_events.deleteMany({ where: { created_at: { lt: cutoff } } })).count,
|
|
53
|
-
"fleet_run_transitions": async (prisma, cutoff) =>
|
|
54
|
-
(await prisma.fleet_run_transitions.deleteMany({ where: { created_at: { lt: cutoff } } })).count,
|
|
55
|
-
// Null out the bulky transcript JSON of old runs — keep the run row itself.
|
|
56
|
-
// A nullable-Json column requires Prisma's DbNull sentinel (bare `null` is
|
|
57
|
-
// ambiguous: JSON null vs SQL NULL); DbNull sets the column to SQL NULL.
|
|
58
|
-
// Prisma namespace is re-exported from @teamscala/db/client.
|
|
59
|
-
"fleet_agent_runs.transcript": async (prisma, cutoff) =>
|
|
60
|
-
(
|
|
61
|
-
await prisma.fleet_agent_runs.updateMany({
|
|
62
|
-
where: { created_at: { lt: cutoff } },
|
|
63
|
-
data: { transcript: Prisma.DbNull },
|
|
64
|
-
})
|
|
65
|
-
).count,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export async function pruneStaleData({
|
|
69
|
-
config,
|
|
70
|
-
}: {
|
|
71
|
-
config: ScheduledJobConfig;
|
|
72
|
-
}): Promise<void> {
|
|
73
|
-
const log = getAppLogger();
|
|
74
|
-
const retention = config.retention;
|
|
75
|
-
if (!retention || Object.keys(retention).length === 0) {
|
|
76
|
-
log.info("[db-prune-stale-data] no retention entries — nothing to prune");
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
const prisma = getPrisma();
|
|
80
|
-
let ran = 0;
|
|
81
|
-
let skipped = 0;
|
|
82
|
-
for (const [key, rawValue] of Object.entries(retention)) {
|
|
83
|
-
const op = PRUNE_OPS[key];
|
|
84
|
-
if (!op) {
|
|
85
|
-
log.warn(
|
|
86
|
-
`[db-prune-stale-data] skipping "${key}" — not in the prune allowlist ` +
|
|
87
|
-
`(audit_trail is excluded by audit-trail-permanent; add the op explicitly to enable)`,
|
|
88
|
-
);
|
|
89
|
-
skipped++;
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
const match = DURATION_RE.exec(rawValue.trim());
|
|
93
|
-
if (!match) {
|
|
94
|
-
log.warn(
|
|
95
|
-
`[db-prune-stale-data] skipping "${key}" — value "${rawValue}" is not a duration (expected e.g. "1 day")`,
|
|
96
|
-
);
|
|
97
|
-
skipped++;
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
const cutoff = new Date(Date.now() - Number(match[1]) * UNIT_TO_MS[match[2]!]);
|
|
101
|
-
const affected = await op(prisma, cutoff);
|
|
102
|
-
log.info(
|
|
103
|
-
`[db-prune-stale-data] pruned "${key}" older than ${rawValue} — ${affected} row(s) affected`,
|
|
104
|
-
);
|
|
105
|
-
ran++;
|
|
106
|
-
}
|
|
107
|
-
log.info(`[db-prune-stale-data] complete — ${ran} op(s) ran, ${skipped} skipped`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
pgBossHandlerRegistry.register("@teamscala/service-boot:prune-stale-data", pruneStaleData);
|