@teamscala/service-boot 0.23.366 → 0.23.367
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
|
@@ -51,7 +51,6 @@ import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
|
51
51
|
import { recordEvent } from "@teamscala/event-log/record-event";
|
|
52
52
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
53
53
|
import { pgBossHandlerRegistry } from "../pg-boss-handler-registry.ts";
|
|
54
|
-
import { getServerClient } from "../page-server-orpc/server-client.ts";
|
|
55
54
|
import {
|
|
56
55
|
composeEscalationDescription,
|
|
57
56
|
composeEscalationMessage,
|
|
@@ -118,17 +117,55 @@ async function getSystemIdentity(): Promise<{ userId: string | undefined; organi
|
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
/**
|
|
121
|
-
* The system-authed in-process ORPC client
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
120
|
+
* The system-authed in-process ORPC client, built LAZILY on first use from the
|
|
121
|
+
* SAME composed appRouter the /api/rpc mount uses (getAppRouter — never a raw
|
|
122
|
+
* re-import of the generated routers, per register-page-server-client's
|
|
123
|
+
* documented module-identity trap). Built here rather than read from
|
|
124
|
+
* page-server-orpc/server-client because a pg-boss handler can fire on a
|
|
125
|
+
* BACKEND service, where the page-server client is never registered — the
|
|
126
|
+
* empty-router fallback returns a client with no model namespaces (the
|
|
127
|
+
* TypeError this replaced). Per `no-direct-prisma-outside-orpc`, every WRITE
|
|
128
|
+
* in this handler routes through this client; the single raw read (the
|
|
129
|
+
* event_log recurrence aggregate, a GROUP BY no generated CRUD expresses) uses
|
|
130
|
+
* the assigned-client form with $queryRawUnsafe — the lane-attention-sweep
|
|
127
131
|
* precedent in this same directory. Timestamps are ISO strings, never Date
|
|
128
132
|
* instances (the generated valibot schemas require it).
|
|
129
133
|
*/
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
let cachedOrpc: OrpcSurface | null = null;
|
|
135
|
+
async function orpc(): Promise<OrpcSurface> {
|
|
136
|
+
if (cachedOrpc) return cachedOrpc;
|
|
137
|
+
const { createRouterClient } = await import("@orpc/server");
|
|
138
|
+
const { getAppRouter } = await import("@teamscala/orpc/app-router");
|
|
139
|
+
const appRouter = (await getAppRouter()) as Record<string, unknown> | null;
|
|
140
|
+
if (!appRouter) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"platform-fault-escalation: no ORPC appRouter — the service has no generated routers to write through",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const { getPrisma } = await import("@teamscala/db/prisma-registry");
|
|
146
|
+
const { loadRegistryConfig } = await import("@teamscala/db/registry/load-config");
|
|
147
|
+
const { PlatformIdentitySchema } = await import(
|
|
148
|
+
"@teamscala/db-validation/registry-schemas/platform-identity"
|
|
149
|
+
);
|
|
150
|
+
const platformIdentity = await loadRegistryConfig(
|
|
151
|
+
"config",
|
|
152
|
+
"platform-identity",
|
|
153
|
+
PlatformIdentitySchema,
|
|
154
|
+
);
|
|
155
|
+
cachedOrpc = createRouterClient(
|
|
156
|
+
{ ...appRouter } as never,
|
|
157
|
+
{
|
|
158
|
+
context: {
|
|
159
|
+
prisma: getPrisma(),
|
|
160
|
+
user: {
|
|
161
|
+
id: "system",
|
|
162
|
+
isSuperAdmin: true,
|
|
163
|
+
organisationId: platformIdentity.platformOrganisationId,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
} as never,
|
|
167
|
+
) as unknown as OrpcSurface;
|
|
168
|
+
return cachedOrpc;
|
|
132
169
|
}
|
|
133
170
|
|
|
134
171
|
/** The router subset this handler uses — mirrors the generated procedure args. */
|
|
@@ -187,7 +224,7 @@ async function fetchAggregates(patterns: string[], windowMinutes: number): Promi
|
|
|
187
224
|
}
|
|
188
225
|
|
|
189
226
|
async function resolveOperatorRelay(): Promise<OperatorRelayBot> {
|
|
190
|
-
const bot = (await orpc().messaging_bots.findFirst({
|
|
227
|
+
const bot = (await (await orpc()).messaging_bots.findFirst({
|
|
191
228
|
where: {
|
|
192
229
|
is_active: true,
|
|
193
230
|
platform: "whatsapp",
|
|
@@ -225,7 +262,7 @@ async function deliverEscalation(
|
|
|
225
262
|
|
|
226
263
|
// 1. Claim the episode BEFORE sending, so a send failure leaves a durable
|
|
227
264
|
// claim the next sweep retries instead of a lost escalation.
|
|
228
|
-
await orpc().platform_fault_escalations.upsert({
|
|
265
|
+
await (await orpc()).platform_fault_escalations.upsert({
|
|
229
266
|
where: { kind },
|
|
230
267
|
create: {
|
|
231
268
|
kind,
|
|
@@ -280,7 +317,7 @@ async function deliverEscalation(
|
|
|
280
317
|
}
|
|
281
318
|
|
|
282
319
|
// 4. Durable record: the work_items task the PA reads back / the operator answers on.
|
|
283
|
-
const workItem = await orpc().work_items.create({
|
|
320
|
+
const workItem = await (await orpc()).work_items.create({
|
|
284
321
|
data: {
|
|
285
322
|
kind: "task",
|
|
286
323
|
parent_id: config.parent_project_id,
|
|
@@ -302,7 +339,7 @@ async function deliverEscalation(
|
|
|
302
339
|
});
|
|
303
340
|
|
|
304
341
|
// 5. Readback-correct the episode: delivered + linked in one update.
|
|
305
|
-
await orpc().platform_fault_escalations.update({
|
|
342
|
+
await (await orpc()).platform_fault_escalations.update({
|
|
306
343
|
where: { kind },
|
|
307
344
|
data: {
|
|
308
345
|
escalated_at: now.toISOString(),
|
|
@@ -327,7 +364,7 @@ async function deliverEscalation(
|
|
|
327
364
|
|
|
328
365
|
async function updateEpisode(aggregate: FaultAggregate, episode: EscalationEpisode): Promise<void> {
|
|
329
366
|
const now = new Date();
|
|
330
|
-
await orpc().platform_fault_escalations.update({
|
|
367
|
+
await (await orpc()).platform_fault_escalations.update({
|
|
331
368
|
where: { kind: episode.kind },
|
|
332
369
|
data: {
|
|
333
370
|
last_seen_at: aggregate.lastSeen.toISOString(),
|
|
@@ -337,7 +374,7 @@ async function updateEpisode(aggregate: FaultAggregate, episode: EscalationEpiso
|
|
|
337
374
|
},
|
|
338
375
|
});
|
|
339
376
|
if (episode.workItemId) {
|
|
340
|
-
await orpc().comments.create({
|
|
377
|
+
await (await orpc()).comments.create({
|
|
341
378
|
data: {
|
|
342
379
|
work_item_id: episode.workItemId,
|
|
343
380
|
content: `Still recurring: ${aggregate.count} events, last at ${aggregate.lastSeen.toISOString()}. No new escalation (already escalated and unresolved).`,
|
|
@@ -354,12 +391,12 @@ async function updateEpisode(aggregate: FaultAggregate, episode: EscalationEpiso
|
|
|
354
391
|
|
|
355
392
|
async function resolveEpisode(episode: EscalationEpisode, quietMinutes: number): Promise<void> {
|
|
356
393
|
const now = new Date();
|
|
357
|
-
await orpc().platform_fault_escalations.update({
|
|
394
|
+
await (await orpc()).platform_fault_escalations.update({
|
|
358
395
|
where: { kind: episode.kind },
|
|
359
396
|
data: { resolved_at: now.toISOString(), updated_at: now.toISOString() },
|
|
360
397
|
});
|
|
361
398
|
if (episode.workItemId) {
|
|
362
|
-
await orpc().comments.create({
|
|
399
|
+
await (await orpc()).comments.create({
|
|
363
400
|
data: {
|
|
364
401
|
work_item_id: episode.workItemId,
|
|
365
402
|
content: `Fault stopped — no '${episode.kind}' events for ${Math.round(quietMinutes)} min. Episode closed; the fault will re-escalate if it recurs.`,
|
|
@@ -383,7 +420,7 @@ async function platformFaultEscalationSweep(ctx: { config: ScheduledJobConfig })
|
|
|
383
420
|
// Only OPEN episodes matter: a resolved episode and no episode at all have
|
|
384
421
|
// the same meaning to decideSweep (a recurring sustained fault opens a NEW
|
|
385
422
|
// episode and escalates again).
|
|
386
|
-
const openEpisodes = (await orpc().platform_fault_escalations.findMany({
|
|
423
|
+
const openEpisodes = (await (await orpc()).platform_fault_escalations.findMany({
|
|
387
424
|
where: { resolved_at: null },
|
|
388
425
|
})) as Array<{
|
|
389
426
|
kind: string;
|