@teamscala/service-boot 0.23.481 → 0.23.493

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.
Files changed (45) hide show
  1. package/package.json +1 -261
  2. package/src/agreement-gate/signing-session.ts +1 -1
  3. package/src/build-client-assets/styles-build.ts +11 -1
  4. package/src/mount-orpc-router.ts +1 -1
  5. package/src/register-page-server-client.ts +1 -15
  6. package/src/register-ws-routes.ts +1 -1
  7. package/src/fn-routers/agreement-certificate.ts +0 -224
  8. package/src/fn-routers/agreement-completion-email.ts +0 -114
  9. package/src/fn-routers/agreement-send-router.ts +0 -303
  10. package/src/fn-routers/agreement-sign-side-effects.ts +0 -192
  11. package/src/fn-routers/agreement-signing-evidence.ts +0 -105
  12. package/src/fn-routers/assessment-report-data.test.ts +0 -404
  13. package/src/fn-routers/assessment-report-data.ts +0 -485
  14. package/src/fn-routers/assessment-report-email.ts +0 -256
  15. package/src/fn-routers/assessment-report-pair.test.ts +0 -136
  16. package/src/fn-routers/assessment-report-pair.ts +0 -155
  17. package/src/fn-routers/assessment-research-report-data.test.ts +0 -233
  18. package/src/fn-routers/assessment-research-report-data.ts +0 -424
  19. package/src/fn-routers/assessment-scoring.ts +0 -105
  20. package/src/fn-routers/auth-router.ts +0 -65
  21. package/src/fn-routers/chat-session-router-shared.ts +0 -122
  22. package/src/fn-routers/chat-session-router.ts +0 -356
  23. package/src/fn-routers/conversation-ownership.ts +0 -24
  24. package/src/fn-routers/email-defaults.ts +0 -28
  25. package/src/fn-routers/email-link-ttl.ts +0 -28
  26. package/src/fn-routers/index.ts +0 -76
  27. package/src/fn-routers/linkedin-identity.test.ts +0 -27
  28. package/src/fn-routers/linkedin-identity.ts +0 -36
  29. package/src/fn-routers/messaging-whatsapp-router.test.ts +0 -121
  30. package/src/fn-routers/messaging-whatsapp-router.ts +0 -200
  31. package/src/fn-routers/panel-data-router.ts +0 -104
  32. package/src/fn-routers/persist-chat-messages.test.ts +0 -86
  33. package/src/fn-routers/persist-chat-messages.ts +0 -98
  34. package/src/fn-routers/proposal-send-router.ts +0 -207
  35. package/src/fn-routers/public-agreement-router.ts +0 -407
  36. package/src/fn-routers/public-assessment-router.test.ts +0 -437
  37. package/src/fn-routers/public-assessment-router.ts +0 -268
  38. package/src/fn-routers/public-gpt-session-router.ts +0 -378
  39. package/src/fn-routers/resend-client.ts +0 -39
  40. package/src/fn-routers/sign-in-flow-router.ts +0 -196
  41. package/src/fn-routers/user-preferences-router.ts +0 -130
  42. package/src/fn-routers/work-items-router.ts +0 -342
  43. package/src/page-server-orpc/audit-revert-router.ts +0 -102
  44. package/src/page-server-orpc/puck-pages-router.ts +0 -170
  45. package/src/page-server-orpc/user-preferences-router.ts +0 -132
@@ -1,170 +0,0 @@
1
- /**
2
- * @system page-server
3
- * @status handwritten
4
- * @edit edit directly
5
- *
6
- * ORPC procedures for loading Puck pages. These replace the direct Prisma calls
7
- * in load-puck-page-by-slug.ts with proper ORPC procedures that receive
8
- * context.prisma from the router.
9
- */
10
-
11
- import { os } from "@orpc/server";
12
- import * as v from "valibot";
13
- import { createCache } from "@teamscala/cache/create-cache";
14
-
15
- const procedure = os;
16
-
17
- const _WebsiteShellSettingsSchema = v.object({
18
- website_type: v.nullable(v.string()),
19
- layout_settings: v.nullable(v.record(v.string(), v.unknown())),
20
- header_settings: v.nullable(v.record(v.string(), v.unknown())),
21
- footer_settings: v.nullable(v.record(v.string(), v.unknown())),
22
- });
23
-
24
- const PuckPageRowSchema = v.object({
25
- id: v.string(),
26
- path: v.nullable(v.string()),
27
- puckData: v.unknown(),
28
- mode: v.nullable(v.string()),
29
- render_mode: v.nullable(v.string()),
30
- website_slug: v.string(),
31
- organisation_id: v.nullable(v.string()),
32
- meta_title: v.nullable(v.string()),
33
- meta_description: v.nullable(v.string()),
34
- website: v.object({
35
- slug: v.string(),
36
- name: v.string(),
37
- organisation_id: v.nullable(v.string()),
38
- website_type: v.nullable(v.string()),
39
- layout_settings: v.nullable(v.record(v.string(), v.unknown())),
40
- header_settings: v.nullable(v.record(v.string(), v.unknown())),
41
- footer_settings: v.nullable(v.record(v.string(), v.unknown())),
42
- }),
43
- });
44
-
45
- type PuckPageRow = v.InferOutput<typeof PuckPageRowSchema>;
46
-
47
- /**
48
- * Right-panel / region pages (AI chat, audit, comments, …) are STATIC website
49
- * config — one page row + its website row, no per-user data — so they are safe
50
- * to cache across users keyed by website + path. Each cold open otherwise pays
51
- * TWO dependent cross-internet DB round-trips (~230ms each against the dev host's
52
- * proxied Railway Postgres); caching makes a repeat panel open near-instant.
53
- * SWR: fresh 60s, stale-served up to 5min while ONE background refresh runs
54
- * (stampede-safe), so a page edit appears within the window without every open
55
- * re-querying. No user identity in the key or value — no cross-user leak.
56
- */
57
- const pageCache = createCache<PuckPageRow | null>(
58
- "page-server:load-from-website",
59
- { ttlMs: 60_000, staleTtlMs: 5 * 60_000, maxSize: 200, stampedeProtection: true },
60
- );
61
-
62
- export const puckPagesRouter = {
63
- loadFromWebsite: procedure
64
- .input(v.object({ websiteSlug: v.string(), path: v.string() }))
65
- .output(v.nullable(PuckPageRowSchema))
66
- .handler(async ({ input, context }) => {
67
- const db = (context as { prisma?: unknown }).prisma;
68
- if (!db)
69
- throw new Error("puckPages.loadFromWebsite requires context.prisma");
70
-
71
- // Normalize the path: builda_pages stores paths WITH a leading slash
72
- // ('/chat', '/audit', …), but a panel_view.page_slug + a region
73
- // `?right=`/`?panel=` slug conventionally OMIT it ('chat', 'audit').
74
- // The SSR panel resolver (resolve-page-puck-data.ts) already prepends
75
- // '/' (`/${activePanel}`); match that contract here so a client-side
76
- // panel/region load by slug resolves the SAME row a deep-link SSR does.
77
- // Without this, regions.loadPage({ path: "chat" }) queried
78
- // `WHERE path = 'chat'` → no match → null → the panel dock opened blank
79
- // (portal Chat/Audit/Notifications/Utility + the fleet-dashboard region
80
- // swap — any page-kind panel opened by click rather than deep link).
81
- const path = input.path.startsWith("/") ? input.path : `/${input.path}`;
82
-
83
- return pageCache.fetchOrCompute(
84
- `${input.websiteSlug}::${path}`,
85
- async () => {
86
- const site = (await (
87
- db as {
88
- builda_websites: { findFirst: (args: unknown) => Promise<unknown> };
89
- }
90
- ).builda_websites.findFirst({
91
- // websiteSlug may be a website SLUG or a website ID. The Utility shell
92
- // carries the serving website's id (loaderData.*.website_id) but not its
93
- // slug, so its region panels pass the id; UUIDs and kebab slugs cannot
94
- // collide, so OR-matching both is unambiguous.
95
- where: {
96
- OR: [{ slug: input.websiteSlug }, { id: input.websiteSlug }],
97
- status: { not: "archived" },
98
- },
99
- select: {
100
- id: true,
101
- name: true,
102
- slug: true,
103
- organisation_id: true,
104
- website_type: true,
105
- layout_settings: true,
106
- header_settings: true,
107
- footer_settings: true,
108
- },
109
- })) as {
110
- id: string;
111
- name: string;
112
- slug: string;
113
- organisation_id: string | null;
114
- website_type: string | null;
115
- layout_settings: Record<string, unknown> | null;
116
- header_settings: Record<string, unknown> | null;
117
- footer_settings: Record<string, unknown> | null;
118
- } | null;
119
-
120
- if (!site) return null;
121
-
122
- const row = (await (
123
- db as {
124
- builda_pages: { findFirst: (args: unknown) => Promise<unknown> };
125
- }
126
- ).builda_pages.findFirst({
127
- where: { path, is_active: true, website_id: site.id },
128
- select: {
129
- id: true,
130
- path: true,
131
- puck_data: true,
132
- meta_title: true,
133
- meta_description: true,
134
- },
135
- })) as {
136
- id: string;
137
- path: string | null;
138
- puck_data: unknown;
139
- meta_title: string | null;
140
- meta_description: string | null;
141
- } | null;
142
-
143
- if (!row) return null;
144
-
145
- return {
146
- id: row.id,
147
- path: row.path,
148
- puckData: row.puck_data,
149
- mode: null,
150
- render_mode: null,
151
- website_slug: site.slug,
152
- organisation_id: site.organisation_id,
153
- meta_title: row.meta_title,
154
- meta_description: row.meta_description,
155
- website: {
156
- slug: site.slug,
157
- name: site.name,
158
- organisation_id: site.organisation_id,
159
- website_type: site.website_type,
160
- layout_settings: site.layout_settings,
161
- header_settings: site.header_settings,
162
- footer_settings: site.footer_settings,
163
- },
164
- };
165
- });
166
- }),
167
-
168
- };
169
-
170
- export type PuckPagesRouter = typeof puckPagesRouter;
@@ -1,132 +0,0 @@
1
- /**
2
- * @system page-server
3
- * @status handwritten
4
- * @edit edit directly
5
- *
6
- * ORPC procedures for per-user portal UI preferences (sidebar collapse,
7
- * panel sizes, active right panel, conversation ID, brand cache).
8
- * Reads and writes the `portal` key inside `user.preferences` JSONB.
9
- *
10
- * The SSR page loads initial state via getPortalPreferences; mutations
11
- * from usePortalActionHelpers call updatePortalPreferences. This replaces
12
- * the injected localStorage pattern with DB-backed persistence.
13
- */
14
-
15
- import { os } from "@orpc/server";
16
- import type { PrismaClient } from "@teamscala/db/client";
17
- import * as v from "valibot";
18
-
19
- const procedure = os;
20
-
21
- // Typed portal UI preferences shape (the `portal` key inside user.preferences).
22
- // Owned here because the page-server is the sole reader/writer of this key, and
23
- // db-validation's generated user-preferences schema is a loose `v.record` that
24
- // can't express this typed all-optional ("partial") shape. Used for both the
25
- // mutation input and the merged-result re-validation below.
26
- const portalPreferencesPartialSchema = v.object({
27
- // Per-user dark/light choice. Read server-side into the shell's initialTheme
28
- // (so a refresh keeps the user's choice instead of resetting to the org brand
29
- // default); written on the toolbar theme toggle.
30
- theme: v.optional(v.nullable(v.picklist(["light", "dark"]))),
31
- paneLeftWidth: v.optional(v.number()),
32
- paneRightWidth: v.optional(v.number()),
33
- sidebarCollapsed: v.optional(v.boolean()),
34
- activePanel: v.optional(v.nullable(v.string())),
35
- panelSizes: v.optional(
36
- v.object({ main: v.optional(v.number()), right: v.optional(v.number()) }),
37
- ),
38
- conversationId: v.optional(v.nullable(v.string())),
39
- brandCache: v.optional(
40
- v.nullable(
41
- v.object({
42
- logo: v.object({
43
- light: v.optional(v.nullable(v.string())),
44
- dark: v.optional(v.nullable(v.string())),
45
- brand: v.optional(v.nullable(v.string())),
46
- }),
47
- primaryColor: v.optional(v.string()),
48
- }),
49
- ),
50
- ),
51
- });
52
-
53
- type C = { prisma?: unknown; user?: { id?: string } | null };
54
-
55
- function getPrisma(c: unknown) {
56
- const db = (c as C).prisma;
57
- if (!db) throw new Error("userPreferences: context.prisma is required");
58
- return db as PrismaClient;
59
- }
60
-
61
- export const userPreferencesRouter = {
62
- /**
63
- * Read the current user's portal preferences from user.preferences.portal.
64
- * Returns null portal if no preferences exist yet.
65
- */
66
- getPortalPreferences: procedure.handler(async ({ context }) => {
67
- const prisma = getPrisma(context);
68
- const userId = (context as C).user?.id;
69
- if (!userId) return { portal: null };
70
-
71
- const user = await prisma.user.findUnique({
72
- where: { id: userId },
73
- select: { preferences: true },
74
- });
75
-
76
- const prefs = (user?.preferences as Record<string, unknown> | null) ?? {};
77
- const portal = (prefs.portal as Record<string, unknown> | undefined) ?? null;
78
- if (!portal) return { portal: null };
79
-
80
- return { portal };
81
- }),
82
-
83
- /**
84
- * Merge and persist a patch to the user's portal preferences.
85
- * The patch is a deep partial of the portal object; missing keys
86
- * are preserved from the existing value.
87
- */
88
- updatePortalPreferences: procedure
89
- .input(v.object({ patch: portalPreferencesPartialSchema }))
90
- .handler(async ({ context, input }) => {
91
- const prisma = getPrisma(context);
92
- const userId = (context as C).user?.id;
93
- if (!userId) throw new Error("updatePortalPreferences: no user id in context");
94
-
95
- const user = await prisma.user.findUnique({
96
- where: { id: userId },
97
- select: { preferences: true },
98
- });
99
-
100
- const current = ((user?.preferences as Record<string, unknown>) ?? {}) as Record<
101
- string,
102
- unknown
103
- >;
104
- const existingPortal = ((current.portal as Record<string, unknown>) ?? {}) as Record<
105
- string,
106
- unknown
107
- >;
108
-
109
- const mergedPortal = {
110
- ...existingPortal,
111
- ...input.patch,
112
- panelSizes: input.patch.panelSizes
113
- ? { ...(existingPortal.panelSizes as Record<string, unknown>), ...input.patch.panelSizes }
114
- : existingPortal.panelSizes,
115
- brandCache:
116
- input.patch.brandCache !== undefined
117
- ? input.patch.brandCache
118
- : existingPortal.brandCache,
119
- };
120
-
121
- v.parse(portalPreferencesPartialSchema, mergedPortal);
122
-
123
- await prisma.user.update({
124
- where: { id: userId },
125
- data: { preferences: JSON.parse(JSON.stringify({ ...current, portal: mergedPortal })) },
126
- });
127
-
128
- return { portal: mergedPortal };
129
- }),
130
- };
131
-
132
- export type UserPreferencesRouter = typeof userPreferencesRouter;