@teamscala/service-boot 0.23.362 → 0.23.363

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
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "name": "@teamscala/service-boot",
8
8
  "type": "module",
9
- "version": "0.23.362",
9
+ "version": "0.23.363",
10
10
  "sideEffects": false,
11
11
  "imports": {
12
12
  "#service-boot/*": "./src/*",
@@ -51,6 +51,7 @@ 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";
54
55
  import {
55
56
  composeEscalationDescription,
56
57
  composeEscalationMessage,
@@ -116,6 +117,42 @@ async function getSystemIdentity(): Promise<{ userId: string | undefined; organi
116
117
  return cachedSystemIdentity;
117
118
  }
118
119
 
120
+ /**
121
+ * The system-authed in-process ORPC client (set at boot after the router is
122
+ * built — no-arg form carries the system super-admin context, correct for a
123
+ * platform system action). Per `no-direct-prisma-outside-orpc`, every WRITE in
124
+ * this handler routes through it; the single raw read (the event_log
125
+ * recurrence aggregate, a GROUP BY no generated CRUD expresses) uses the
126
+ * assigned-client form with $queryRawUnsafe — the lane-attention-sweep
127
+ * precedent in this same directory. Timestamps are ISO strings, never Date
128
+ * instances (the generated valibot schemas require it).
129
+ */
130
+ function orpc(): OrpcSurface {
131
+ return getServerClient() as unknown as OrpcSurface;
132
+ }
133
+
134
+ /** The router subset this handler uses — mirrors the generated procedure args. */
135
+ interface OrpcSurface {
136
+ platform_fault_escalations: {
137
+ upsert(args: {
138
+ where: { kind: string };
139
+ create: Record<string, unknown>;
140
+ update: Record<string, unknown>;
141
+ }): Promise<unknown>;
142
+ update(args: { where: { kind: string }; data: Record<string, unknown> }): Promise<unknown>;
143
+ findMany(args?: { where?: Record<string, unknown> }): Promise<Array<Record<string, unknown>>>;
144
+ };
145
+ messaging_bots: {
146
+ findFirst(args: Record<string, unknown>): Promise<Record<string, unknown> | null>;
147
+ };
148
+ work_items: {
149
+ create(args: { data: Record<string, unknown>; select?: Record<string, unknown> }): Promise<{ id: string }>;
150
+ };
151
+ comments: {
152
+ create(args: { data: Record<string, unknown> }): Promise<{ id: string }>;
153
+ };
154
+ }
155
+
119
156
  async function loadConfig(): Promise<FaultEscalationConfig> {
120
157
  return loadRegistryConfig("config", CONFIG_SLUG, FaultEscalationConfigSchema);
121
158
  }
@@ -150,15 +187,14 @@ async function fetchAggregates(patterns: string[], windowMinutes: number): Promi
150
187
  }
151
188
 
152
189
  async function resolveOperatorRelay(): Promise<OperatorRelayBot> {
153
- const prisma = getPrisma();
154
- const bot = await prisma.messaging_bots.findFirst({
190
+ const bot = (await orpc().messaging_bots.findFirst({
155
191
  where: {
156
192
  is_active: true,
157
193
  platform: "whatsapp",
158
194
  flow_config: { path: ["mode"], equals: "operator-relay" },
159
195
  },
160
196
  select: { id: true, organisation_id: true, external_id: true, flow_config: true },
161
- });
197
+ })) as unknown as OperatorRelayBot | null;
162
198
  if (!bot) throw new Error("platform-fault-escalation: no active operator-relay WhatsApp bot row");
163
199
  const phone = (bot.flow_config as { operator_phone?: string } | null)?.operator_phone;
164
200
  if (!phone) throw new Error("platform-fault-escalation: operator-relay bot has no operator_phone");
@@ -185,22 +221,21 @@ async function deliverEscalation(
185
221
  config: FaultEscalationConfig,
186
222
  ): Promise<{ messageId: string; workItemId: string }> {
187
223
  const log = getAppLogger();
188
- const prisma = getPrisma();
189
224
  const now = new Date();
190
225
 
191
226
  // 1. Claim the episode BEFORE sending, so a send failure leaves a durable
192
227
  // claim the next sweep retries instead of a lost escalation.
193
- await prisma.platform_fault_escalations.upsert({
228
+ await orpc().platform_fault_escalations.upsert({
194
229
  where: { kind },
195
230
  create: {
196
231
  kind,
197
- first_seen_at: aggregate.firstSeen,
198
- last_seen_at: aggregate.lastSeen,
232
+ first_seen_at: aggregate.firstSeen.toISOString(),
233
+ last_seen_at: aggregate.lastSeen.toISOString(),
199
234
  event_count: aggregate.count,
200
235
  },
201
236
  update: {
202
- first_seen_at: aggregate.firstSeen,
203
- last_seen_at: aggregate.lastSeen,
237
+ first_seen_at: aggregate.firstSeen.toISOString(),
238
+ last_seen_at: aggregate.lastSeen.toISOString(),
204
239
  event_count: aggregate.count,
205
240
  resolved_at: null,
206
241
  },
@@ -244,7 +279,7 @@ async function deliverEscalation(
244
279
  }
245
280
 
246
281
  // 4. Durable record: the work_items task the PA reads back / the operator answers on.
247
- const workItem = await prisma.work_items.create({
282
+ const workItem = await orpc().work_items.create({
248
283
  data: {
249
284
  kind: "task",
250
285
  parent_id: config.parent_project_id,
@@ -266,14 +301,14 @@ async function deliverEscalation(
266
301
  });
267
302
 
268
303
  // 5. Readback-correct the episode: delivered + linked in one update.
269
- await prisma.platform_fault_escalations.update({
304
+ await orpc().platform_fault_escalations.update({
270
305
  where: { kind },
271
306
  data: {
272
- escalated_at: now,
307
+ escalated_at: now.toISOString(),
273
308
  work_item_id: workItem.id,
274
309
  delivered_message_id: messageId,
275
- last_updated_at: now,
276
- updated_at: now,
310
+ last_updated_at: now.toISOString(),
311
+ updated_at: now.toISOString(),
277
312
  },
278
313
  });
279
314
 
@@ -290,19 +325,18 @@ async function deliverEscalation(
290
325
  }
291
326
 
292
327
  async function updateEpisode(aggregate: FaultAggregate, episode: EscalationEpisode): Promise<void> {
293
- const prisma = getPrisma();
294
328
  const now = new Date();
295
- await prisma.platform_fault_escalations.update({
329
+ await orpc().platform_fault_escalations.update({
296
330
  where: { kind: episode.kind },
297
331
  data: {
298
- last_seen_at: aggregate.lastSeen,
332
+ last_seen_at: aggregate.lastSeen.toISOString(),
299
333
  event_count: aggregate.count,
300
- last_updated_at: now,
301
- updated_at: now,
334
+ last_updated_at: now.toISOString(),
335
+ updated_at: now.toISOString(),
302
336
  },
303
337
  });
304
338
  if (episode.workItemId) {
305
- await prisma.comments.create({
339
+ await orpc().comments.create({
306
340
  data: {
307
341
  work_item_id: episode.workItemId,
308
342
  content: `Still recurring: ${aggregate.count} events, last at ${aggregate.lastSeen.toISOString()}. No new escalation (already escalated and unresolved).`,
@@ -318,14 +352,13 @@ async function updateEpisode(aggregate: FaultAggregate, episode: EscalationEpiso
318
352
  }
319
353
 
320
354
  async function resolveEpisode(episode: EscalationEpisode, quietMinutes: number): Promise<void> {
321
- const prisma = getPrisma();
322
355
  const now = new Date();
323
- await prisma.platform_fault_escalations.update({
356
+ await orpc().platform_fault_escalations.update({
324
357
  where: { kind: episode.kind },
325
- data: { resolved_at: now, updated_at: now },
358
+ data: { resolved_at: now.toISOString(), updated_at: now.toISOString() },
326
359
  });
327
360
  if (episode.workItemId) {
328
- await prisma.comments.create({
361
+ await orpc().comments.create({
329
362
  data: {
330
363
  work_item_id: episode.workItemId,
331
364
  content: `Fault stopped — no '${episode.kind}' events for ${Math.round(quietMinutes)} min. Episode closed; the fault will re-escalate if it recurs.`,
@@ -349,9 +382,18 @@ async function platformFaultEscalationSweep(ctx: { config: ScheduledJobConfig })
349
382
  // Only OPEN episodes matter: a resolved episode and no episode at all have
350
383
  // the same meaning to decideSweep (a recurring sustained fault opens a NEW
351
384
  // episode and escalates again).
352
- const openEpisodes = await getPrisma().platform_fault_escalations.findMany({
385
+ const openEpisodes = (await orpc().platform_fault_escalations.findMany({
353
386
  where: { resolved_at: null },
354
- });
387
+ })) as Array<{
388
+ kind: string;
389
+ first_seen_at: string | Date;
390
+ last_seen_at: string | Date;
391
+ event_count: number;
392
+ escalated_at: string | Date | null;
393
+ work_item_id: string | null;
394
+ resolved_at: string | Date | null;
395
+ last_updated_at: string | Date | null;
396
+ }>;
355
397
  const episodes: EscalationEpisode[] = openEpisodes.map((row) => ({
356
398
  kind: row.kind,
357
399
  firstSeenAt: new Date(row.first_seen_at),