@teamscala/service-boot 0.23.417 → 0.23.419
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 +33 -1
- package/src/handlers/platform-fault-escalation/config.ts +54 -0
- package/src/handlers/platform-fault-escalation/delivery.ts +173 -0
- package/src/handlers/platform-fault-escalation/event-log-queries.ts +122 -0
- package/src/handlers/platform-fault-escalation/orpc-client.ts +97 -0
- package/src/handlers/platform-fault-escalation.ts +15 -387
- package/src/register-registry-page-route/page-data-shared.ts +124 -0
- package/src/register-registry-page-route/public-shell-page-data.ts +169 -0
- package/src/register-registry-page-route/region-slots-page-data.ts +175 -0
- package/src/register-registry-page-route/resolve-page-data.ts +16 -515
- package/src/register-registry-page-route/single-page-data.ts +171 -0
|
@@ -1,103 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @system ui-service-boot
|
|
3
|
-
* @status handwritten
|
|
3
|
+
* @status handwritten
|
|
4
4
|
* @edit edit directly
|
|
5
5
|
*
|
|
6
6
|
* Converts the resolved loader-graph into the pageData shape the renderer
|
|
7
7
|
* consumes — three branches: arbitrary region-slots (portal/utility), the
|
|
8
8
|
* header/body/footer public shell, and the single-page fallback. Extracted
|
|
9
|
-
* from register-registry-page-route.ts (single-purpose-file split)
|
|
9
|
+
* from register-registry-page-route.ts (single-purpose-file split); the three
|
|
10
|
+
* branches + shared helpers were split out of THIS file at 547 lines
|
|
11
|
+
* (region-slots-page-data.ts, public-shell-page-data.ts, single-page-data.ts,
|
|
12
|
+
* page-data-shared.ts).
|
|
10
13
|
*/
|
|
11
|
-
import { resolveThemeMode } from "../resolve-theme-mode.ts";
|
|
12
|
-
import { composePageTitle } from "../compose-page-title.ts";
|
|
13
|
-
import { resolveEntityTitle } from "./meta-title.ts";
|
|
14
|
-
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* excludes whole-statement type-only imports, so the bare specifier has no
|
|
20
|
-
* installed package to resolve against at publish typecheck time. The shape used
|
|
21
|
-
* here (root/content/zones) is all this module reads or constructs.
|
|
22
|
-
*/
|
|
23
|
-
type PuckData = {
|
|
24
|
-
root: { props?: Record<string, unknown> };
|
|
25
|
-
content: unknown[];
|
|
26
|
-
zones?: Record<string, unknown>;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const EMPTY_PUCK_DATA: PuckData = { root: { props: {} }, content: [] };
|
|
30
|
-
|
|
31
|
-
// Returns null on malformed JSON (observed via warn log) instead of a silent
|
|
32
|
-
// masking empty fallback — protective-layers-fail-explicitly. Callers default
|
|
33
|
-
// to EMPTY_PUCK_DATA where an empty render is the intended degradation.
|
|
34
|
-
function extractPuckData(source: Record<string, unknown>): PuckData | null {
|
|
35
|
-
const raw = source.puck_data ?? source.puckData;
|
|
36
|
-
if (!raw) return EMPTY_PUCK_DATA;
|
|
37
|
-
if (typeof raw === "string") {
|
|
38
|
-
try {
|
|
39
|
-
return JSON.parse(raw) as PuckData;
|
|
40
|
-
} catch (err) {
|
|
41
|
-
getAppLogger().warn(
|
|
42
|
-
"[extractPuckData] malformed puck_data JSON — rendering empty",
|
|
43
|
-
{ err },
|
|
44
|
-
);
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return raw as PuckData;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function applyLayoutDefaults(
|
|
52
|
-
puckData: PuckData,
|
|
53
|
-
defaults: Record<string, string>,
|
|
54
|
-
): void {
|
|
55
|
-
if (!defaults || Object.keys(defaults).length === 0) return;
|
|
56
|
-
const data = puckData as { root?: { props?: Record<string, unknown> } };
|
|
57
|
-
if (!data.root) data.root = { props: {} };
|
|
58
|
-
if (!data.root.props) data.root.props = {};
|
|
59
|
-
for (const [key, value] of Object.entries(defaults)) {
|
|
60
|
-
if (data.root.props[key] == null) data.root.props[key] = value;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function pickRootProp<T>(regions: RegionEntry[], key: string): T | undefined {
|
|
65
|
-
for (const region of regions) {
|
|
66
|
-
const props = (
|
|
67
|
-
region.puckData as { root?: { props?: Record<string, unknown> } } | null
|
|
68
|
-
)?.root?.props;
|
|
69
|
-
const value = props?.[key];
|
|
70
|
-
if (value != null && value !== "") return value as T;
|
|
71
|
-
}
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
interface RegionEntry {
|
|
76
|
-
slot: string;
|
|
77
|
-
puckData: PuckData;
|
|
78
|
-
pageId: string;
|
|
79
|
-
website?: { slug: string; name: string; organisation_id?: string | null };
|
|
80
|
-
metaTitle?: string | null;
|
|
81
|
-
metaDescription?: string | null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// A region slot the operator DECLARED (resource_loader.region_slots) whose
|
|
85
|
-
// source did not resolve used to vanish with no signal anywhere — the page
|
|
86
|
-
// rendered minus a region while the page, source, attributes, template and
|
|
87
|
-
// tree builder all looked innocent (measured 2026-08-18: a footer region
|
|
88
|
-
// added to a report produced no footer and no diagnostic; the cause was the
|
|
89
|
-
// boot-time config capture named in the message). One warn per slot per
|
|
90
|
-
// process, throttled like the block-coverage guard in the caller, so the hot
|
|
91
|
-
// path never spams it.
|
|
92
|
-
const unresolvedRegionSlotsLogged = new Set<string>();
|
|
93
|
-
|
|
94
|
-
function warnUnresolvedRegionSlot(slot: string): void {
|
|
95
|
-
if (unresolvedRegionSlotsLogged.has(slot)) return;
|
|
96
|
-
unresolvedRegionSlotsLogged.add(slot);
|
|
97
|
-
getAppLogger().warn(
|
|
98
|
-
`[resolve-page-data] region slot "${slot}" is declared in the resource_loader config but no source resolved for it — the region is omitted from this render. A resource_loader edit reaches a booted service only after a restart.`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
15
|
+
import { regionSlotsPageData } from "./region-slots-page-data.ts";
|
|
16
|
+
import { publicShellPageData } from "./public-shell-page-data.ts";
|
|
17
|
+
import { singlePageData } from "./single-page-data.ts";
|
|
101
18
|
|
|
102
19
|
export function resolvedToPageData(
|
|
103
20
|
resolved: Record<string, unknown>,
|
|
@@ -113,435 +30,19 @@ export function resolvedToPageData(
|
|
|
113
30
|
// prefixes carry the OWNING ORG's name instead of the website label.
|
|
114
31
|
orgPrefix?: boolean,
|
|
115
32
|
): Record<string, unknown> {
|
|
116
|
-
const brand = (resolved.brand as Record<string, unknown>) ?? {};
|
|
117
|
-
const primaryColor = (brand.primaryColor as string) ?? "";
|
|
118
|
-
const headingFont = (brand.headingFont as string) ?? null;
|
|
119
|
-
const bodyFont = (brand.bodyFont as string) ?? null;
|
|
120
|
-
const logo = (brand.logo as {
|
|
121
|
-
light: string | null;
|
|
122
|
-
dark: string | null;
|
|
123
|
-
brand: string | null;
|
|
124
|
-
}) ?? { light: null, dark: null, brand: null };
|
|
125
|
-
|
|
126
|
-
const brandTheme = typeof brand.theme === "string" ? brand.theme : undefined;
|
|
127
|
-
|
|
128
33
|
// Branch 1: arbitrary regions (portal/agents/docs) — region_slots from loader config
|
|
129
34
|
if (regionSlots && regionSlots.length > 0) {
|
|
130
|
-
|
|
131
|
-
const layoutDefaults =
|
|
132
|
-
(website.layout_defaults as Record<string, string>) ??
|
|
133
|
-
(website.layout_settings as Record<string, string>) ??
|
|
134
|
-
{};
|
|
135
|
-
const layoutSettings = website.layout_settings as
|
|
136
|
-
| { theme?: unknown }
|
|
137
|
-
| null
|
|
138
|
-
| undefined;
|
|
139
|
-
const websiteTheme =
|
|
140
|
-
layoutSettings && typeof layoutSettings.theme === "string"
|
|
141
|
-
? layoutSettings.theme
|
|
142
|
-
: undefined;
|
|
143
|
-
|
|
144
|
-
// Title derives from the primary resolved entity (e.g. the GPT agent),
|
|
145
|
-
// matching the flat-page branch — so a utility-shell GPT shows the agent
|
|
146
|
-
// name in <title>/OG, not the generic "Portal". Falls back to the website
|
|
147
|
-
// name, then "Portal". Without this, the region-slots branch returned
|
|
148
|
-
// early with a hardcoded "Portal" title (gpt-test: SSR/DOM/OG title FAIL).
|
|
149
|
-
const entityTitle = resolveEntityTitle(resolved);
|
|
150
|
-
|
|
151
|
-
const regions: RegionEntry[] = [];
|
|
152
|
-
for (const slot of regionSlots) {
|
|
153
|
-
const source = resolved[slot] as Record<string, unknown> | undefined;
|
|
154
|
-
if (!source) {
|
|
155
|
-
warnUnresolvedRegionSlot(slot);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
const puckData = extractPuckData(source) ?? EMPTY_PUCK_DATA;
|
|
159
|
-
applyLayoutDefaults(puckData, layoutDefaults);
|
|
160
|
-
regions.push({
|
|
161
|
-
slot,
|
|
162
|
-
puckData,
|
|
163
|
-
pageId: (source.id as string) ?? "",
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (regions.length === 0) {
|
|
168
|
-
return {
|
|
169
|
-
puckData: { root: { props: {} }, content: [] } as PuckData,
|
|
170
|
-
theme: "dark" as const,
|
|
171
|
-
brand: {
|
|
172
|
-
primaryColor,
|
|
173
|
-
backgroundColor: (brand.backgroundColor as string | null) ?? null,
|
|
174
|
-
lightSurface: (brand.lightSurface as string | null) ?? null,
|
|
175
|
-
darkSurface: (brand.darkSurface as string | null) ?? null,
|
|
176
|
-
headingFont,
|
|
177
|
-
bodyFont,
|
|
178
|
-
logo,
|
|
179
|
-
companyName: (brand.companyName as string) ?? null,
|
|
180
|
-
companyFriendlyName: (brand.companyFriendlyName as string) ?? null,
|
|
181
|
-
contactEmail: (brand.contactEmail as string) ?? null,
|
|
182
|
-
favicon: (brand.favicon as string) ?? null,
|
|
183
|
-
},
|
|
184
|
-
meta: {
|
|
185
|
-
title: composePageTitle(
|
|
186
|
-
{
|
|
187
|
-
templateTitle: (meta.title as string) || undefined,
|
|
188
|
-
websiteName: (website.name as string) || undefined,
|
|
189
|
-
organisationName: (brand.companyName as string) || undefined,
|
|
190
|
-
entityTitle: entityTitle || undefined,
|
|
191
|
-
},
|
|
192
|
-
{ orgPrefix },
|
|
193
|
-
),
|
|
194
|
-
},
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const rootTheme = pickRootProp<string>(regions, "theme");
|
|
199
|
-
const { theme } = resolveThemeMode({
|
|
200
|
-
override: themeOverride,
|
|
201
|
-
candidates: [rootTheme, brandTheme, websiteTheme],
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const overrideColor = pickRootProp<string>(regions, "primaryColor");
|
|
205
|
-
const overrideHeading = pickRootProp<string>(regions, "headingFont");
|
|
206
|
-
const overrideBody = pickRootProp<string>(regions, "bodyFont");
|
|
207
|
-
// The page the user is viewing is the main content region (center for
|
|
208
|
-
// portal, body for utility). Its name ("Dashboard") is the title subject.
|
|
209
|
-
const contentSlot = regionSlots.find((s) => s === "center" || s === "body");
|
|
210
|
-
const contentPageName = contentSlot
|
|
211
|
-
? ((resolved[contentSlot] as Record<string, unknown> | undefined)?.name as
|
|
212
|
-
| string
|
|
213
|
-
| undefined)
|
|
214
|
-
: undefined;
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
puckData: { root: { props: {} }, content: [] } as PuckData,
|
|
218
|
-
regions,
|
|
219
|
-
// Region-shell blocks (e.g. the GPT chat in the center region) read
|
|
220
|
-
// their resolved entities from pageData.loaderData — same surface the
|
|
221
|
-
// flat single-page branch exposes. Without it the chat block has no
|
|
222
|
-
// agent and renders "Custom GPT not found".
|
|
223
|
-
loaderData: resolved,
|
|
224
|
-
website: {
|
|
225
|
-
slug: website.slug as string,
|
|
226
|
-
name: website.name as string,
|
|
227
|
-
organisation_id: website.organisation_id as string | null,
|
|
228
|
-
// header_settings carries the DB-driven header config (e.g.
|
|
229
|
-
// backdropBlur) the public shell applies — PublicTemplate reads
|
|
230
|
-
// pageData.website.header_settings.backdropBlur. Dropping it here
|
|
231
|
-
// silently kills the header blur even though the DB switch is set
|
|
232
|
-
// (the 2026-06-17 regression). Pass it through verbatim.
|
|
233
|
-
header_settings:
|
|
234
|
-
(website.header_settings as Record<string, unknown> | null) ?? null,
|
|
235
|
-
},
|
|
236
|
-
orgId:
|
|
237
|
-
((resolved.center as Record<string, unknown> | undefined)
|
|
238
|
-
?.organisation_id as string | null) ??
|
|
239
|
-
((resolved.agent as Record<string, unknown> | undefined)
|
|
240
|
-
?.organisation_id as string | null) ??
|
|
241
|
-
null,
|
|
242
|
-
brand: {
|
|
243
|
-
primaryColor: overrideColor ?? primaryColor,
|
|
244
|
-
backgroundColor: (brand.backgroundColor as string | null) ?? null,
|
|
245
|
-
lightSurface: (brand.lightSurface as string | null) ?? null,
|
|
246
|
-
darkSurface: (brand.darkSurface as string | null) ?? null,
|
|
247
|
-
headingFont: overrideHeading ?? headingFont,
|
|
248
|
-
bodyFont: overrideBody ?? bodyFont,
|
|
249
|
-
logo,
|
|
250
|
-
companyName: (brand.companyName as string) ?? null,
|
|
251
|
-
companyFriendlyName: (brand.companyFriendlyName as string) ?? null,
|
|
252
|
-
contactEmail: (brand.contactEmail as string) ?? null,
|
|
253
|
-
favicon: (brand.favicon as string) ?? null,
|
|
254
|
-
},
|
|
255
|
-
theme,
|
|
256
|
-
meta: {
|
|
257
|
-
title: composePageTitle(
|
|
258
|
-
{
|
|
259
|
-
templateTitle: (meta.title as string) || undefined,
|
|
260
|
-
websiteName: (website.name as string) || undefined,
|
|
261
|
-
organisationName: (brand.companyName as string) || undefined,
|
|
262
|
-
pageName: contentPageName,
|
|
263
|
-
entityTitle: entityTitle || undefined,
|
|
264
|
-
},
|
|
265
|
-
{ orgPrefix },
|
|
266
|
-
),
|
|
267
|
-
},
|
|
268
|
-
};
|
|
35
|
+
return regionSlotsPageData(resolved, meta, regionSlots, themeOverride, orgPrefix);
|
|
269
36
|
}
|
|
270
37
|
|
|
271
38
|
// Branch 2: public shell (header/body/footer regions from header/footer sources)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const website =
|
|
278
|
-
(resolved.website_by_host as Record<string, unknown>) ??
|
|
279
|
-
(resolved.website as Record<string, unknown>) ??
|
|
280
|
-
{};
|
|
281
|
-
const layoutDefaults =
|
|
282
|
-
(website.layout_defaults as Record<string, string>) ??
|
|
283
|
-
(website.layout_settings as Record<string, string>) ??
|
|
284
|
-
{};
|
|
285
|
-
const layoutSettings = website.layout_settings as
|
|
286
|
-
| { theme?: unknown }
|
|
287
|
-
| null
|
|
288
|
-
| undefined;
|
|
289
|
-
const websiteTheme =
|
|
290
|
-
layoutSettings && typeof layoutSettings.theme === "string"
|
|
291
|
-
? layoutSettings.theme
|
|
292
|
-
: undefined;
|
|
293
|
-
|
|
294
|
-
const regions: RegionEntry[] = [];
|
|
295
|
-
|
|
296
|
-
if (headerSource) {
|
|
297
|
-
const headerPuck = extractPuckData(headerSource) ?? EMPTY_PUCK_DATA;
|
|
298
|
-
applyLayoutDefaults(headerPuck, layoutDefaults);
|
|
299
|
-
regions.push({
|
|
300
|
-
slot: "header",
|
|
301
|
-
puckData: headerPuck,
|
|
302
|
-
pageId: (headerSource.id as string) ?? "",
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const bodyPuck = extractPuckData(page) ?? EMPTY_PUCK_DATA;
|
|
307
|
-
applyLayoutDefaults(bodyPuck, layoutDefaults);
|
|
308
|
-
regions.push({
|
|
309
|
-
slot: "body",
|
|
310
|
-
puckData: bodyPuck,
|
|
311
|
-
pageId: (page.id as string) ?? "",
|
|
312
|
-
metaTitle: (page.meta_title as string) ?? null,
|
|
313
|
-
metaDescription: (page.meta_description as string) ?? null,
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
if (footerSource) {
|
|
317
|
-
const footerPuck = extractPuckData(footerSource) ?? EMPTY_PUCK_DATA;
|
|
318
|
-
applyLayoutDefaults(footerPuck, layoutDefaults);
|
|
319
|
-
regions.push({
|
|
320
|
-
slot: "footer",
|
|
321
|
-
puckData: footerPuck,
|
|
322
|
-
pageId: (footerSource.id as string) ?? "",
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const rootTheme = pickRootProp<string>(regions, "theme");
|
|
327
|
-
const { theme } = resolveThemeMode({
|
|
328
|
-
override: themeOverride,
|
|
329
|
-
candidates: [rootTheme, brandTheme, websiteTheme],
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
const overrideColor = pickRootProp<string>(regions, "primaryColor");
|
|
333
|
-
const overrideBg = pickRootProp<string>(regions, "backgroundColor");
|
|
334
|
-
const overrideHeading = pickRootProp<string>(regions, "headingFont");
|
|
335
|
-
const overrideBody = pickRootProp<string>(regions, "bodyFont");
|
|
336
|
-
|
|
337
|
-
const pageName = (page.name as string) || null;
|
|
338
|
-
const entityTitle = resolveEntityTitle(resolved);
|
|
339
|
-
const bodyMetaTitle = regions.find((r) => r.slot === "body")?.metaTitle;
|
|
340
|
-
const pageRootTitle = pickRootProp<string>(regions, "title");
|
|
341
|
-
const pageDescription =
|
|
342
|
-
regions.find((r) => r.slot === "body")?.metaDescription ??
|
|
343
|
-
pickRootProp<string>(regions, "description") ??
|
|
344
|
-
null;
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
puckData: { root: { props: {} }, content: [] } as PuckData,
|
|
348
|
-
regions,
|
|
349
|
-
// Region-shell blocks (e.g. the header Navigation deriving the body's
|
|
350
|
-
// document sections) read the resolved entity graph from
|
|
351
|
-
// pageData.loaderData — the same surface Branch 1 exposes. Without it a
|
|
352
|
-
// header/footer block serverLoader gets ctx.loaderData=undefined and
|
|
353
|
-
// cannot see the body page, so a server-derived nav produced no items
|
|
354
|
-
// and the menu was client-only (vanished on stale tabs / narrow views).
|
|
355
|
-
loaderData: resolved,
|
|
356
|
-
website: {
|
|
357
|
-
slug: website.slug as string,
|
|
358
|
-
name: website.name as string,
|
|
359
|
-
organisation_id: website.organisation_id as string | null,
|
|
360
|
-
// header_settings carries the DB-driven header config (e.g.
|
|
361
|
-
// backdropBlur) the public shell applies — PublicTemplate reads
|
|
362
|
-
// pageData.website.header_settings.backdropBlur. Dropping it here
|
|
363
|
-
// silently kills the header blur even though the DB switch is set
|
|
364
|
-
// (the 2026-06-17 regression). Pass it through verbatim.
|
|
365
|
-
header_settings:
|
|
366
|
-
(website.header_settings as Record<string, unknown> | null) ?? null,
|
|
367
|
-
},
|
|
368
|
-
orgId:
|
|
369
|
-
(page.organisation_id as string) ??
|
|
370
|
-
null,
|
|
371
|
-
brand: {
|
|
372
|
-
primaryColor: overrideColor ?? primaryColor,
|
|
373
|
-
// Thread the org brand's dark surface (brand_background_color /
|
|
374
|
-
// brand_dark_color) when the page sets no explicit override — so
|
|
375
|
-
// dark-mode pages use the brand's real dark colour, not a
|
|
376
|
-
// primary-derived tint. Uses the org brand's real dark colour.
|
|
377
|
-
backgroundColor:
|
|
378
|
-
overrideBg ?? (brand.backgroundColor as string | null) ?? null,
|
|
379
|
-
lightSurface: (brand.lightSurface as string | null) ?? null,
|
|
380
|
-
darkSurface: (brand.darkSurface as string | null) ?? null,
|
|
381
|
-
accentColor: (brand.accentColor as string | null) ?? null,
|
|
382
|
-
secondaryColor: (brand.secondaryColor as string | null) ?? null,
|
|
383
|
-
mutedColor: (brand.mutedColor as string | null) ?? null,
|
|
384
|
-
headingFont: overrideHeading ?? headingFont,
|
|
385
|
-
bodyFont: overrideBody ?? bodyFont,
|
|
386
|
-
logo,
|
|
387
|
-
companyName: (brand.companyName as string) ?? null,
|
|
388
|
-
companyFriendlyName: (brand.companyFriendlyName as string) ?? null,
|
|
389
|
-
contactEmail: (brand.contactEmail as string) ?? null,
|
|
390
|
-
favicon: (brand.favicon as string) ?? null,
|
|
391
|
-
},
|
|
392
|
-
theme,
|
|
393
|
-
meta: {
|
|
394
|
-
title: composePageTitle(
|
|
395
|
-
{
|
|
396
|
-
pageMetaTitle: bodyMetaTitle,
|
|
397
|
-
templateTitle: (meta.title as string) || undefined,
|
|
398
|
-
websiteName: (website.name as string) || undefined,
|
|
399
|
-
organisationName: (brand.companyName as string) || undefined,
|
|
400
|
-
pageName: pageName ?? pageRootTitle,
|
|
401
|
-
entityTitle: entityTitle || undefined,
|
|
402
|
-
},
|
|
403
|
-
{ orgPrefix },
|
|
404
|
-
),
|
|
405
|
-
...(pageDescription ? { description: pageDescription } : {}),
|
|
406
|
-
},
|
|
407
|
-
};
|
|
39
|
+
if (
|
|
40
|
+
(resolved.header as Record<string, unknown> | undefined) ||
|
|
41
|
+
(resolved.footer as Record<string, unknown> | undefined)
|
|
42
|
+
) {
|
|
43
|
+
return publicShellPageData(resolved, meta, themeOverride, orgPrefix);
|
|
408
44
|
}
|
|
409
45
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
? extractPuckData(page) ?? EMPTY_PUCK_DATA
|
|
413
|
-
: ({ root: { props: {} }, content: [] } as PuckData);
|
|
414
|
-
|
|
415
|
-
const website = (resolved.website as Record<string, unknown>) ?? {};
|
|
416
|
-
const layoutSettings = (website.layout_settings ??
|
|
417
|
-
resolved.websiteDefaults) as { theme?: unknown } | null | undefined;
|
|
418
|
-
const websiteTheme =
|
|
419
|
-
layoutSettings && typeof layoutSettings.theme === "string"
|
|
420
|
-
? layoutSettings.theme
|
|
421
|
-
: undefined;
|
|
422
|
-
|
|
423
|
-
const pageRoot = (
|
|
424
|
-
puckData as { root?: { props?: Record<string, unknown> } } | null
|
|
425
|
-
)?.root?.props;
|
|
426
|
-
const rootTheme =
|
|
427
|
-
pageRoot && typeof pageRoot.theme === "string" ? pageRoot.theme : undefined;
|
|
428
|
-
const agent = (resolved.agent as Record<string, unknown>) ?? null;
|
|
429
|
-
// A GPT page's mode is the agent's dark_theme flag (the DB switch). It's the
|
|
430
|
-
// top candidate so the SSR :root matches the GPT shell's own scope (both
|
|
431
|
-
// derive from the same ai_agents.dark_theme) — flip the flag, the whole GPT
|
|
432
|
-
// + everything it frames flips. themeOverride (?theme=) still wins for an
|
|
433
|
-
// embedded render.
|
|
434
|
-
const agentTheme =
|
|
435
|
-
typeof agent?.dark_theme === "boolean"
|
|
436
|
-
? agent.dark_theme
|
|
437
|
-
? "dark"
|
|
438
|
-
: "light"
|
|
439
|
-
: undefined;
|
|
440
|
-
// themeOverride (?theme=) wins so a form/quiz framed in a GPT panel matches
|
|
441
|
-
// the host shell's light/dark scheme — this single-page branch serves
|
|
442
|
-
// form/quiz/proposal/gpt pages.
|
|
443
|
-
const { theme } = resolveThemeMode({
|
|
444
|
-
override: themeOverride,
|
|
445
|
-
candidates: [agentTheme, rootTheme, brandTheme, websiteTheme],
|
|
446
|
-
});
|
|
447
|
-
const orgId =
|
|
448
|
-
(page?.organisation_id as string) ??
|
|
449
|
-
(agent?.organisation_id as string) ??
|
|
450
|
-
null;
|
|
451
|
-
|
|
452
|
-
const overrideColor =
|
|
453
|
-
pageRoot &&
|
|
454
|
-
typeof pageRoot.primaryColor === "string" &&
|
|
455
|
-
pageRoot.primaryColor
|
|
456
|
-
? pageRoot.primaryColor
|
|
457
|
-
: null;
|
|
458
|
-
const overrideHeading =
|
|
459
|
-
pageRoot && typeof pageRoot.headingFont === "string" && pageRoot.headingFont
|
|
460
|
-
? pageRoot.headingFont
|
|
461
|
-
: null;
|
|
462
|
-
const overrideBody =
|
|
463
|
-
pageRoot && typeof pageRoot.bodyFont === "string" && pageRoot.bodyFont
|
|
464
|
-
? pageRoot.bodyFont
|
|
465
|
-
: null;
|
|
466
|
-
|
|
467
|
-
const screenMode = pageRoot?.screenMode as string | undefined;
|
|
468
|
-
|
|
469
|
-
const pageName = (page?.name as string) || null;
|
|
470
|
-
const entityTitle = resolveEntityTitle(resolved);
|
|
471
|
-
const entityDescription =
|
|
472
|
-
((resolved.form as Record<string, unknown>)?.description as string) ||
|
|
473
|
-
((resolved.quiz as Record<string, unknown>)?.description as string) ||
|
|
474
|
-
((resolved.proposal as Record<string, unknown>)?.description as string) ||
|
|
475
|
-
((resolved.agent as Record<string, unknown>)?.description as string);
|
|
476
|
-
const pageDescription =
|
|
477
|
-
(page?.meta_description as string) ||
|
|
478
|
-
entityDescription ||
|
|
479
|
-
(pageRoot?.description as string) ||
|
|
480
|
-
null;
|
|
481
|
-
const loaderDataPageMetaTitle = (
|
|
482
|
-
resolved.loaderData as { page?: { meta_title?: unknown } } | undefined
|
|
483
|
-
)?.page?.meta_title;
|
|
484
|
-
const pageMetaTitle =
|
|
485
|
-
(typeof loaderDataPageMetaTitle === "string" &&
|
|
486
|
-
loaderDataPageMetaTitle.trim()
|
|
487
|
-
? loaderDataPageMetaTitle.trim()
|
|
488
|
-
: typeof page?.meta_title === "string" && page.meta_title.trim()
|
|
489
|
-
? page.meta_title.trim()
|
|
490
|
-
: null) || null;
|
|
491
|
-
|
|
492
|
-
// Title is DERIVED via the single composer: meta_title is an explicit
|
|
493
|
-
// per-page OVERRIDE (priority 1), a curated resource template wins for
|
|
494
|
-
// resource surfaces (priority 2), otherwise <website>: <subject>. The
|
|
495
|
-
// /sign-in special-case is gone — pageMetaTitle now wins uniformly, so a
|
|
496
|
-
// sign-in page with a meta_title is honoured on every path, not just /sign-in.
|
|
497
|
-
const mergedMeta: Record<string, unknown> = {
|
|
498
|
-
...(pageDescription ? { description: pageDescription } : {}),
|
|
499
|
-
...meta,
|
|
500
|
-
};
|
|
501
|
-
mergedMeta.title = composePageTitle(
|
|
502
|
-
{
|
|
503
|
-
pageMetaTitle,
|
|
504
|
-
templateTitle: (meta.title as string) || undefined,
|
|
505
|
-
websiteName: (website.name as string) || undefined,
|
|
506
|
-
organisationName: (brand.companyName as string) || undefined,
|
|
507
|
-
pageName: pageName ?? ((pageRoot?.title as string) || undefined),
|
|
508
|
-
entityTitle: entityTitle || undefined,
|
|
509
|
-
},
|
|
510
|
-
{ orgPrefix },
|
|
511
|
-
);
|
|
512
|
-
|
|
513
|
-
return {
|
|
514
|
-
puckData,
|
|
515
|
-
website: {
|
|
516
|
-
slug: website.slug as string,
|
|
517
|
-
name: website.name as string,
|
|
518
|
-
organisation_id: website.organisation_id as string | null,
|
|
519
|
-
// Pass header_settings through (DB-driven header blur etc.) — see the
|
|
520
|
-
// other two website assignments above. Dropping it kills the blur.
|
|
521
|
-
header_settings:
|
|
522
|
-
(website.header_settings as Record<string, unknown> | null) ?? null,
|
|
523
|
-
},
|
|
524
|
-
brand: {
|
|
525
|
-
primaryColor: overrideColor ?? primaryColor,
|
|
526
|
-
backgroundColor: (brand.backgroundColor as string | null) ?? null,
|
|
527
|
-
lightSurface: (brand.lightSurface as string | null) ?? null,
|
|
528
|
-
darkSurface: (brand.darkSurface as string | null) ?? null,
|
|
529
|
-
headingFont: overrideHeading ?? headingFont,
|
|
530
|
-
bodyFont: overrideBody ?? bodyFont,
|
|
531
|
-
logo,
|
|
532
|
-
companyName: (brand.companyName as string) ?? null,
|
|
533
|
-
companyFriendlyName: (brand.companyFriendlyName as string) ?? null,
|
|
534
|
-
contactEmail: (brand.contactEmail as string) ?? null,
|
|
535
|
-
favicon: (brand.favicon as string) ?? null,
|
|
536
|
-
},
|
|
537
|
-
theme,
|
|
538
|
-
orgId,
|
|
539
|
-
loaderData: resolved,
|
|
540
|
-
...(screenMode ? { fullScreen: screenMode === "full-screen" } : {}),
|
|
541
|
-
...(pageRoot?.standalone != null
|
|
542
|
-
? { standalone: pageRoot.standalone === false ? false : undefined }
|
|
543
|
-
: {}),
|
|
544
|
-
...(providers ? { providerKeys: providers } : {}),
|
|
545
|
-
meta: mergedMeta,
|
|
546
|
-
};
|
|
46
|
+
// Branch 3: single-page fallback.
|
|
47
|
+
return singlePageData(resolved, meta, providers, themeOverride, orgPrefix);
|
|
547
48
|
}
|