@teamscala/service-boot 0.23.479 → 0.23.492
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 -261
- package/src/agreement-gate/signing-session.ts +1 -1
- package/src/build-client-assets/styles-build.ts +11 -1
- package/src/mount-orpc-router.ts +1 -1
- package/src/register-page-server-client.ts +1 -15
- package/src/register-registry-page-route/region-slots-page-data.ts +2 -2
- package/src/register-ws-routes.ts +1 -1
- package/src/fn-routers/agreement-certificate.ts +0 -224
- package/src/fn-routers/agreement-completion-email.ts +0 -114
- package/src/fn-routers/agreement-send-router.ts +0 -303
- package/src/fn-routers/agreement-sign-side-effects.ts +0 -192
- package/src/fn-routers/agreement-signing-evidence.ts +0 -105
- package/src/fn-routers/assessment-report-data.test.ts +0 -404
- package/src/fn-routers/assessment-report-data.ts +0 -485
- package/src/fn-routers/assessment-report-email.ts +0 -256
- package/src/fn-routers/assessment-report-pair.test.ts +0 -107
- package/src/fn-routers/assessment-report-pair.ts +0 -155
- package/src/fn-routers/assessment-research-report-data.test.ts +0 -262
- package/src/fn-routers/assessment-research-report-data.ts +0 -424
- package/src/fn-routers/assessment-scoring.ts +0 -105
- package/src/fn-routers/auth-router.ts +0 -65
- package/src/fn-routers/chat-session-router-shared.ts +0 -122
- package/src/fn-routers/chat-session-router.ts +0 -356
- package/src/fn-routers/conversation-ownership.ts +0 -24
- package/src/fn-routers/email-defaults.ts +0 -28
- package/src/fn-routers/email-link-ttl.ts +0 -28
- package/src/fn-routers/index.ts +0 -76
- package/src/fn-routers/linkedin-identity.test.ts +0 -27
- package/src/fn-routers/linkedin-identity.ts +0 -36
- package/src/fn-routers/messaging-whatsapp-router.test.ts +0 -121
- package/src/fn-routers/messaging-whatsapp-router.ts +0 -200
- package/src/fn-routers/panel-data-router.ts +0 -104
- package/src/fn-routers/persist-chat-messages.test.ts +0 -86
- package/src/fn-routers/persist-chat-messages.ts +0 -98
- package/src/fn-routers/proposal-send-router.ts +0 -207
- package/src/fn-routers/public-agreement-router.ts +0 -407
- package/src/fn-routers/public-assessment-router.test.ts +0 -437
- package/src/fn-routers/public-assessment-router.ts +0 -268
- package/src/fn-routers/public-gpt-session-router.ts +0 -378
- package/src/fn-routers/resend-client.ts +0 -39
- package/src/fn-routers/sign-in-flow-router.ts +0 -196
- package/src/fn-routers/user-preferences-router.ts +0 -130
- package/src/fn-routers/work-items-router.ts +0 -342
- package/src/page-server-orpc/audit-revert-router.ts +0 -102
- package/src/page-server-orpc/puck-pages-router.ts +0 -170
- 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;
|