@camstack/server 1.2.88 → 1.2.89

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 (2) hide show
  1. package/dist/main.js +149 -197
  2. package/package.json +4 -4
package/dist/main.js CHANGED
@@ -39,7 +39,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  /* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument -- pre-existing lint debt across this 800+ line bootstrap module. The flagged sites cross typed boundaries (Fastify request typing, AddonRouteRegistry, AuthService inherited methods) where the projectService context can't trace inheritance chains. Tracked separately; do not amend in unrelated edits. */
40
40
  const fastify_1 = require("@trpc/server/adapters/fastify");
41
41
  const ws_1 = require("@trpc/server/adapters/ws");
42
- const static_1 = __importDefault(require("@fastify/static"));
43
42
  const compress_1 = __importDefault(require("@fastify/compress"));
44
43
  const cookie_1 = __importDefault(require("@fastify/cookie"));
45
44
  const sse_no_compression_js_1 = require("./api/sse-no-compression.js");
@@ -1074,216 +1073,166 @@ async function bootstrap() {
1074
1073
  await addonRegistry.setAppRouter(appRouter);
1075
1074
  console.log('[bootstrap] AddonRegistry wired with tRPC direct caller');
1076
1075
  }
1077
- // ScopedTokenManager and admin user creation handled by local-auth addon.
1078
- // Serve admin UI static files from the admin-ui singleton capability.
1079
- // Always enabled — in dev mode Vite runs on its own port and doesn't interfere.
1080
- //
1081
- // The admin-ui addon runs in its own dedicated runner subprocess and
1082
- // finishes registering 1-3s after bootstrap; poll briefly for it
1083
- // before giving up to avoid the 'admin-ui capability not registered —
1084
- // no static file serving' warn that left the SPA unserved until next
1085
- // restart.
1086
- try {
1087
- const bootAddonRegistry = app.get(addon_registry_service_1.AddonRegistryService);
1088
- const capRegistry = bootAddonRegistry.getCapabilityRegistry();
1089
- // HUB-NODE-SCOPED resolution — never the cluster-global singleton.
1090
- // Agent nodes register the same `admin-ui` cap through their own
1091
- // agent-ui addon (placement: agent-only), and the cluster-elected
1092
- // active provider can land on the AGENT's registration depending on
1093
- // node boot order. Its `getStaticDir()` then answers with a path on
1094
- // the agent's filesystem nonexistent here — and the SPA catch-all
1095
- // silently never registers (every `GET /` 404s until the next
1096
- // restart re-rolls the election). `getSingletonForNode(cap, 'hub')`
1097
- // only ever resolves providers hosted on THIS node.
1098
- let adminUI = capRegistry?.getSingletonForNode('admin-ui', 'hub');
1099
- // CAMSTACK_SKIP_ADMIN_UI_WAIT — bypass the 60s poll. Used by the
1100
- // e2e harness, which doesn't need the SPA served and spawns hubs
1101
- // with strict boot timeouts. Production keeps the poll so cold
1102
- // boots wait for the forked admin-ui group to register.
1103
- const skipAdminUIWait = process.env['CAMSTACK_SKIP_ADMIN_UI_WAIT'] === '1';
1104
- if (!adminUI && capRegistry && !skipAdminUIWait) {
1105
- // Forked-addon spawn + Moleculer registration + tRPC hydration
1106
- // can take ~15-20s on cold boot, especially when several runners
1107
- // are spawning in parallel. The previous 10s window was racing
1108
- // the admin-ui runner's boot — fastify-static didn't register and
1109
- // every `GET /` returned 404. Bump to 60s; we only pay this
1110
- // wait once at boot.
1111
- const ADMIN_UI_WAIT_MS = 60_000;
1112
- const POLL_MS = 200;
1113
- const deadline = Date.now() + ADMIN_UI_WAIT_MS;
1114
- while (!adminUI && Date.now() < deadline) {
1115
- await new Promise((r) => setTimeout(r, POLL_MS));
1116
- adminUI = capRegistry.getSingletonForNode('admin-ui', 'hub');
1076
+ const adminUiState = { staticDir: null, indexPath: null };
1077
+ const viewerUiState = { staticDir: null, indexPath: null };
1078
+ const VIEWER_MOUNT = '/viewer/camstack';
1079
+ // Register UI routes before Fastify starts, but resolve their forked
1080
+ // providers after listen. During a cold boot the control plane is therefore
1081
+ // immediately reachable while the non-critical UI runners finish starting.
1082
+ fastify.get('/viewer', async (_request, reply) => reply.redirect(`${VIEWER_MOUNT}/`));
1083
+ fastify.get('/viewer/', async (_request, reply) => reply.redirect(`${VIEWER_MOUNT}/`));
1084
+ fastify.get('/webrtc-test.html', async (_request, reply) => {
1085
+ const webrtcTestPath = path.join(dataPath, 'webrtc-test.html');
1086
+ if (!fs.existsSync(webrtcTestPath))
1087
+ return reply.callNotFound();
1088
+ return reply.type('text/html').send(fs.createReadStream(webrtcTestPath));
1089
+ });
1090
+ fastify.get('/viewer/*', async (request, reply) => {
1091
+ const { staticDir, indexPath } = viewerUiState;
1092
+ if (!staticDir || !indexPath) {
1093
+ return reply.status(503).send({ error: 'Viewer UI is starting' });
1094
+ }
1095
+ const pathOnly = request.url.split('?')[0] ?? request.url;
1096
+ let rel = '';
1097
+ if (pathOnly.startsWith(`${VIEWER_MOUNT}/`)) {
1098
+ rel = pathOnly.slice(VIEWER_MOUNT.length + 1);
1099
+ }
1100
+ if (rel && /\.[a-zA-Z0-9]+$/.test(pathOnly)) {
1101
+ const abs = path.join(staticDir, rel);
1102
+ if ((abs === staticDir || abs.startsWith(staticDir + path.sep)) && fs.existsSync(abs)) {
1103
+ reply.header('cache-control', (0, spa_static_1.spaAssetCacheControl)(rel));
1104
+ reply.type((0, spa_static_1.contentTypeForPath)(rel));
1105
+ return sendMaybePrecompressed(reply, abs, request.headers['accept-encoding']);
1117
1106
  }
1107
+ return reply.callNotFound();
1118
1108
  }
1119
- if (adminUI) {
1120
- const { staticDir } = await adminUI.getStaticDir();
1121
- const indexPath = path.join(staticDir, 'index.html');
1122
- if (fs.existsSync(staticDir) && fs.existsSync(indexPath)) {
1123
- spaIndexHtml = indexPath;
1124
- // `serve: false` registers no route — it only decorates
1125
- // `reply.sendFile`, so the single SPA `/*` handler below owns all
1126
- // routing and serves each asset LIVE from the current `staticDir`.
1127
- // The old `wildcard: false` registered one route per file enumerated
1128
- // AT BOOT, so a redeployed admin-ui's new content-hashed assets had no
1129
- // route and 404'd until a hub restart. Live `sendFile` removes that.
1130
- await fastify.register(static_1.default, {
1131
- root: staticDir,
1132
- serve: false,
1133
- decorateReply: true,
1134
- // Disable @fastify/static's automatic Cache-Control injection so
1135
- // the per-file headers set by the route handler (immutable for
1136
- // hashed assets, no-cache for index.html / SW) survive sendFile().
1137
- cacheControl: false,
1138
- });
1139
- // Dev diagnostic: serve webrtc-test.html from dataPath if it exists.
1140
- const webrtcTestPath = path.join(dataPath, 'webrtc-test.html');
1141
- if (fs.existsSync(webrtcTestPath)) {
1142
- fastify.get('/webrtc-test.html', async (_request, reply) => {
1143
- return reply.type('text/html').send(fs.createReadStream(webrtcTestPath));
1144
- });
1109
+ reply.header('cache-control', 'no-cache, must-revalidate');
1110
+ return reply.type('text/html').send(fs.createReadStream(indexPath));
1111
+ });
1112
+ fastify.get('/*', async (request, reply) => {
1113
+ const url = request.url;
1114
+ if (url.startsWith('/trpc') ||
1115
+ url.startsWith('/api/') ||
1116
+ url.startsWith('/agent') ||
1117
+ url.startsWith('/health') ||
1118
+ url.startsWith('/viewer')) {
1119
+ return reply.callNotFound();
1120
+ }
1121
+ const { staticDir, indexPath } = adminUiState;
1122
+ if (!staticDir || !indexPath) {
1123
+ return reply.status(503).send({ error: 'Admin UI is starting' });
1124
+ }
1125
+ const pathOnly = url.split('?')[0] ?? url;
1126
+ if (/\.[a-zA-Z0-9]+$/.test(pathOnly)) {
1127
+ const rel = pathOnly.replace(/^\/+/, '');
1128
+ const abs = path.join(staticDir, rel);
1129
+ if ((abs === staticDir || abs.startsWith(staticDir + path.sep)) && fs.existsSync(abs)) {
1130
+ reply.header('cache-control', (0, spa_static_1.spaAssetCacheControl)(rel));
1131
+ return sendMaybePrecompressed(reply, abs, request.headers['accept-encoding']);
1132
+ }
1133
+ return reply.callNotFound();
1134
+ }
1135
+ reply.header('cache-control', 'no-cache, must-revalidate');
1136
+ return reply.type('text/html').send(fs.createReadStream(indexPath));
1137
+ });
1138
+ const resolveAdminUi = async () => {
1139
+ try {
1140
+ const bootAddonRegistry = app.get(addon_registry_service_1.AddonRegistryService);
1141
+ const capRegistry = bootAddonRegistry.getCapabilityRegistry();
1142
+ // HUB-NODE-SCOPED resolution — never the cluster-global singleton.
1143
+ // Agent nodes register the same `admin-ui` cap through their own
1144
+ // agent-ui addon (placement: agent-only), and the cluster-elected
1145
+ // active provider can land on the AGENT's registration depending on
1146
+ // node boot order. Its `getStaticDir()` then answers with a path on
1147
+ // the agent's filesystem — nonexistent here — and the SPA catch-all
1148
+ // silently never registers (every `GET /` 404s until the next
1149
+ // restart re-rolls the election). `getSingletonForNode(cap, 'hub')`
1150
+ // only ever resolves providers hosted on THIS node.
1151
+ let adminUI = capRegistry?.getSingletonForNode('admin-ui', 'hub');
1152
+ // CAMSTACK_SKIP_ADMIN_UI_WAIT bypasses the background capability poll in
1153
+ // e2e runs that do not need the SPA.
1154
+ const skipAdminUIWait = process.env['CAMSTACK_SKIP_ADMIN_UI_WAIT'] === '1';
1155
+ if (!adminUI && capRegistry && !skipAdminUIWait) {
1156
+ // Forked-addon spawn + Moleculer registration + tRPC hydration
1157
+ // can take ~15-20s on cold boot, especially when several runners
1158
+ // are spawning in parallel. The previous 10s window was racing
1159
+ // the admin-ui runner's boot — fastify-static didn't register and
1160
+ // every `GET /` returned 404. Bump to 60s; we only pay this
1161
+ // wait once at boot.
1162
+ const ADMIN_UI_WAIT_MS = 60_000;
1163
+ const POLL_MS = 200;
1164
+ const deadline = Date.now() + ADMIN_UI_WAIT_MS;
1165
+ while (!adminUI && Date.now() < deadline) {
1166
+ await new Promise((r) => setTimeout(r, POLL_MS));
1167
+ adminUI = capRegistry.getSingletonForNode('admin-ui', 'hub');
1168
+ }
1169
+ }
1170
+ if (adminUI) {
1171
+ const { staticDir } = await adminUI.getStaticDir();
1172
+ const indexPath = path.join(staticDir, 'index.html');
1173
+ if (fs.existsSync(staticDir) && fs.existsSync(indexPath)) {
1174
+ spaIndexHtml = indexPath;
1175
+ adminUiState.staticDir = staticDir;
1176
+ adminUiState.indexPath = indexPath;
1177
+ (0, precompressed_asset_js_1.warmPrecompressedAssets)(staticDir, (msg, meta) => console.log(`[static] ${msg} ${JSON.stringify(meta)}`));
1178
+ const { version } = await adminUI.getVersion();
1179
+ console.log(`[bootstrap] Admin UI served from: ${staticDir} (v${version})`);
1180
+ }
1181
+ else {
1182
+ console.warn(`[bootstrap] Admin UI dist not found at: ${staticDir} — run 'npm run build' in addon-admin-ui`);
1145
1183
  }
1146
- // SPA fallback + live static serving: this single catch-all owns every
1147
- // GET. Core API prefixes fall through to their own routers via
1148
- // `callNotFound`. Uses a wildcard route instead of setNotFoundHandler.
1149
- fastify.get('/*', async (request, reply) => {
1150
- const url = request.url;
1151
- if (url.startsWith('/trpc') ||
1152
- url.startsWith('/api/') ||
1153
- url.startsWith('/agent') ||
1154
- url.startsWith('/health') ||
1155
- // The viewer SPA owns /viewer/** via its own catch-all (registered
1156
- // below). Fall through here so admin-ui never serves it — and so
1157
- // /viewer 404s cleanly when the viewer-ui addon isn't registered.
1158
- url.startsWith('/viewer')) {
1159
- return reply.callNotFound();
1160
- }
1161
- // A request whose last path segment has a file extension is a static
1162
- // asset: serve it LIVE from the current dist so a redeployed
1163
- // admin-ui's new content-hashed files are picked up without a hub
1164
- // restart. When the file is missing, 404 — never the SPA
1165
- // `index.html`: serving HTML under a `.js`/`.css` URL makes upstream
1166
- // caches (Cloudflare, the browser) pin `text/html`, which then fails
1167
- // the module MIME check long after the file is actually available.
1168
- const pathOnly = url.split('?')[0] ?? url;
1169
- if (/\.[a-zA-Z0-9]+$/.test(pathOnly)) {
1170
- const rel = pathOnly.replace(/^\/+/, '');
1171
- const abs = path.join(staticDir, rel);
1172
- if ((abs === staticDir || abs.startsWith(staticDir + path.sep)) && fs.existsSync(abs)) {
1173
- // Cache policy that lets PWA updates actually propagate (the
1174
- // stale-bundle bug): the service worker + registration + manifest
1175
- // MUST be revalidated every load or a redeploy never reaches the
1176
- // client (the SW keeps serving the old precache). Content-hashed
1177
- // build assets (assets/index-<hash>.js) are immutable. Everything
1178
- // else gets a short cache.
1179
- reply.header('cache-control', (0, spa_static_1.spaAssetCacheControl)(rel));
1180
- return sendMaybePrecompressed(reply, abs, request.headers['accept-encoding']);
1181
- }
1182
- return reply.callNotFound();
1183
- }
1184
- // index.html (the SPA shell) must never be cached — it references the
1185
- // content-hashed bundles, so a stale copy pins the old app forever.
1186
- reply.header('cache-control', 'no-cache, must-revalidate');
1187
- return reply.type('text/html').send(fs.createReadStream(spaIndexHtml));
1188
- });
1189
- // Compress the dist in the background so the first CLIENT does not pay
1190
- // for it — a 5 MB bundle takes ~6.7s at quality 11, and that is a worse
1191
- // first load than the per-request compression this replaced.
1192
- (0, precompressed_asset_js_1.warmPrecompressedAssets)(staticDir, (msg, meta) => console.log(`[static] ${msg} ${JSON.stringify(meta)}`));
1193
- const { version } = await adminUI.getVersion();
1194
- console.log(`[bootstrap] Admin UI served from: ${staticDir} (v${version})`);
1195
1184
  }
1196
1185
  else {
1197
- console.warn(`[bootstrap] Admin UI dist not found at: ${staticDir} run 'npm run build' in addon-admin-ui`);
1186
+ console.warn('[bootstrap] admin-ui capability not registeredno static file serving');
1198
1187
  }
1199
1188
  }
1200
- else {
1201
- console.warn('[bootstrap] admin-ui capability not registered no static file serving');
1189
+ catch (err) {
1190
+ console.error('[bootstrap] Failed to set up admin UI static serving:', err);
1202
1191
  }
1203
- }
1204
- catch (err) {
1205
- console.error('[bootstrap] Failed to set up admin UI static serving:', err);
1206
- }
1207
- // Serve the CamStack viewer SPA from the viewer-ui singleton capability.
1208
- // The viewer is built (Expo web export) with EXPO_PUBLIC_WEB_BASE_PATH=/viewer,
1209
- // so its baseUrl and therefore every asset URL — is /viewer/camstack/**. We
1210
- // mount it at exactly that prefix and redirect /viewer → the SPA root. Mirrors
1211
- // the admin-ui poll+serve, but streams assets directly from its own dist root
1212
- // so it stays independent of the admin-ui @fastify/static decorator.
1213
- try {
1214
- const bootAddonRegistry = app.get(addon_registry_service_1.AddonRegistryService);
1215
- const capRegistry = bootAddonRegistry.getCapabilityRegistry();
1216
- // Hub-node-scoped for the same reason as admin-ui above: a remote
1217
- // node's registration must never win the election for the provider
1218
- // whose staticDir this process reads from local disk.
1219
- let viewerUI = capRegistry?.getSingletonForNode('viewer-ui', 'hub');
1220
- const skipViewerUIWait = process.env['CAMSTACK_SKIP_VIEWER_UI_WAIT'] === '1';
1221
- if (!viewerUI && capRegistry && !skipViewerUIWait) {
1222
- // The viewer-ui addon runs in its own forked runner and registers a few
1223
- // seconds after boot. Poll briefly (shorter than admin-ui's 60s — the
1224
- // viewer is non-critical and admin-ui already gated the slow cold boot).
1225
- const VIEWER_UI_WAIT_MS = 30_000;
1226
- const POLL_MS = 200;
1227
- const deadline = Date.now() + VIEWER_UI_WAIT_MS;
1228
- while (!viewerUI && Date.now() < deadline) {
1229
- await new Promise((r) => setTimeout(r, POLL_MS));
1230
- viewerUI = capRegistry.getSingletonForNode('viewer-ui', 'hub');
1192
+ };
1193
+ const resolveViewerUi = async () => {
1194
+ try {
1195
+ const bootAddonRegistry = app.get(addon_registry_service_1.AddonRegistryService);
1196
+ const capRegistry = bootAddonRegistry.getCapabilityRegistry();
1197
+ // Hub-node-scoped for the same reason as admin-ui above: a remote
1198
+ // node's registration must never win the election for the provider
1199
+ // whose staticDir this process reads from local disk.
1200
+ let viewerUI = capRegistry?.getSingletonForNode('viewer-ui', 'hub');
1201
+ const skipViewerUIWait = process.env['CAMSTACK_SKIP_VIEWER_UI_WAIT'] === '1';
1202
+ if (!viewerUI && capRegistry && !skipViewerUIWait) {
1203
+ // The viewer-ui addon runs in its own forked runner and registers a few
1204
+ // seconds after boot. Poll briefly (shorter than admin-ui's 60s — the
1205
+ // viewer is non-critical and admin-ui already gated the slow cold boot).
1206
+ const VIEWER_UI_WAIT_MS = 30_000;
1207
+ const POLL_MS = 200;
1208
+ const deadline = Date.now() + VIEWER_UI_WAIT_MS;
1209
+ while (!viewerUI && Date.now() < deadline) {
1210
+ await new Promise((r) => setTimeout(r, POLL_MS));
1211
+ viewerUI = capRegistry.getSingletonForNode('viewer-ui', 'hub');
1212
+ }
1231
1213
  }
1232
- }
1233
- if (viewerUI) {
1234
- const { staticDir } = await viewerUI.getStaticDir();
1235
- const indexPath = path.join(staticDir, 'index.html');
1236
- if (fs.existsSync(staticDir) && fs.existsSync(indexPath)) {
1237
- // The Expo baseUrl (`${webBasePath}/camstack`) is the on-hub mount.
1238
- const VIEWER_MOUNT = '/viewer/camstack';
1239
- // Clean entry point: /viewer and /viewer/ → the SPA root.
1240
- fastify.get('/viewer', async (_request, reply) => reply.redirect(`${VIEWER_MOUNT}/`));
1241
- fastify.get('/viewer/', async (_request, reply) => reply.redirect(`${VIEWER_MOUNT}/`));
1242
- // Single catch-all owns every GET under /viewer/**: assets stream LIVE
1243
- // from the current dist; everything else is the SPA shell.
1244
- fastify.get('/viewer/*', async (request, reply) => {
1245
- const pathOnly = request.url.split('?')[0] ?? request.url;
1246
- // Map the request path to a dist-relative path by stripping the mount
1247
- // prefix. Anything not under the mount is treated as the SPA shell.
1248
- let rel = '';
1249
- if (pathOnly === VIEWER_MOUNT || pathOnly === `${VIEWER_MOUNT}/`) {
1250
- rel = '';
1251
- }
1252
- else if (pathOnly.startsWith(`${VIEWER_MOUNT}/`)) {
1253
- rel = pathOnly.slice(VIEWER_MOUNT.length + 1);
1254
- }
1255
- // A path whose last segment has a file extension is a static asset:
1256
- // serve it, or 404 — NEVER the SPA shell (serving HTML under a
1257
- // .js/.css URL pins text/html in caches and breaks the module MIME
1258
- // check long after the file is actually available).
1259
- if (rel && /\.[a-zA-Z0-9]+$/.test(pathOnly)) {
1260
- const abs = path.join(staticDir, rel);
1261
- if ((abs === staticDir || abs.startsWith(staticDir + path.sep)) && fs.existsSync(abs)) {
1262
- reply.header('cache-control', (0, spa_static_1.spaAssetCacheControl)(rel));
1263
- reply.type((0, spa_static_1.contentTypeForPath)(rel));
1264
- return sendMaybePrecompressed(reply, abs, request.headers['accept-encoding']);
1265
- }
1266
- return reply.callNotFound();
1267
- }
1268
- // SPA shell — never cached (it references content-hashed bundles).
1269
- reply.header('cache-control', 'no-cache, must-revalidate');
1270
- return reply.type('text/html').send(fs.createReadStream(indexPath));
1271
- });
1272
- (0, precompressed_asset_js_1.warmPrecompressedAssets)(staticDir, (msg, meta) => console.log(`[static] ${msg} ${JSON.stringify(meta)}`));
1273
- const { version } = await viewerUI.getVersion();
1274
- console.log(`[bootstrap] Viewer UI served from: ${staticDir} at ${VIEWER_MOUNT} (v${version})`);
1214
+ if (viewerUI) {
1215
+ const { staticDir } = await viewerUI.getStaticDir();
1216
+ const indexPath = path.join(staticDir, 'index.html');
1217
+ if (fs.existsSync(staticDir) && fs.existsSync(indexPath)) {
1218
+ viewerUiState.staticDir = staticDir;
1219
+ viewerUiState.indexPath = indexPath;
1220
+ (0, precompressed_asset_js_1.warmPrecompressedAssets)(staticDir, (msg, meta) => console.log(`[static] ${msg} ${JSON.stringify(meta)}`));
1221
+ const { version } = await viewerUI.getVersion();
1222
+ console.log(`[bootstrap] Viewer UI served from: ${staticDir} at ${VIEWER_MOUNT} (v${version})`);
1223
+ }
1224
+ else {
1225
+ console.warn(`[bootstrap] Viewer UI dist not found at: ${staticDir} run 'npm run build' in addon-viewer-ui`);
1226
+ }
1275
1227
  }
1276
1228
  else {
1277
- console.warn(`[bootstrap] Viewer UI dist not found at: ${staticDir} run 'npm run build' in addon-viewer-ui`);
1229
+ console.warn('[bootstrap] viewer-ui capability not registeredviewer not served');
1278
1230
  }
1279
1231
  }
1280
- else {
1281
- console.warn('[bootstrap] viewer-ui capability not registered viewer not served');
1232
+ catch (err) {
1233
+ console.error('[bootstrap] Failed to set up viewer UI static serving:', err);
1282
1234
  }
1283
- }
1284
- catch (err) {
1285
- console.error('[bootstrap] Failed to set up viewer UI static serving:', err);
1286
- }
1235
+ };
1287
1236
  try {
1288
1237
  await app.listen(port, host);
1289
1238
  }
@@ -1300,6 +1249,9 @@ async function bootstrap() {
1300
1249
  const logger = app.get(logging_service_1.LoggingService).createLogger('System');
1301
1250
  const protocol = tlsOptions ? 'https' : 'http';
1302
1251
  logger.info('CamStack server listening', { meta: { protocol, host, port, trpcRegistered } });
1252
+ // UI runners are not part of control-plane readiness. Their mutable route
1253
+ // state switches from 503 to static serving as soon as each capability is up.
1254
+ void Promise.all([resolveAdminUi(), resolveViewerUi()]);
1303
1255
  // Post-boot: fork workers, register device streams, emit system.boot
1304
1256
  const postBoot = app.get(post_boot_service_1.PostBootService);
1305
1257
  await postBoot.run({ port, host, dataPath, trpcRegistered });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.88",
3
+ "version": "1.2.89",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -38,9 +38,9 @@
38
38
  "@camstack/addon-auth": "1.2.11",
39
39
  "@camstack/addon-decoder-nodeav": "1.2.9",
40
40
  "@camstack/addon-notifiers": "1.2.13",
41
- "@camstack/addon-pipeline": "1.2.54",
42
- "@camstack/addon-pipeline-orchestrator": "1.2.36",
43
- "@camstack/addon-post-analysis": "1.2.56",
41
+ "@camstack/addon-pipeline": "1.2.57",
42
+ "@camstack/addon-pipeline-orchestrator": "1.2.38",
43
+ "@camstack/addon-post-analysis": "1.2.58",
44
44
  "@camstack/sdk": "1.2.11",
45
45
  "@camstack/shm-ring": "1.1.9",
46
46
  "@camstack/system": "1.2.75",