@teamscala/service-boot 0.23.319 → 0.23.321
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 +526 -526
- package/src/agreement-gate.d.ts +19 -0
- package/src/agreement-gate.ts +0 -12
- package/src/asset-cache-key.d.ts +11 -0
- package/src/asset-optimize.d.ts +17 -0
- package/src/asset-proxy.d.ts +11 -0
- package/src/asset-social-card.d.ts +17 -0
- package/src/attach-agreement-magic.d.ts +20 -0
- package/src/attach-portal-members.d.ts +23 -0
- package/src/attach-proposal-accept.d.ts +26 -0
- package/src/attach-proposal-magic.d.ts +18 -0
- package/src/attach-transfers-upload.d.ts +27 -0
- package/src/auth-gate.d.ts +28 -0
- package/src/auth-gate.ts +6 -68
- package/src/bind-page-handler.d.ts +18 -0
- package/src/build-client-assets/input-hashing.d.ts +25 -0
- package/src/build-client-assets/source-candidates.d.ts +27 -0
- package/src/build-client-assets.d.ts +50 -0
- package/src/build-client-assets.test.ts +1 -1
- package/src/build-client-assets.ts +1 -1
- package/src/compose-page-title.d.ts +41 -0
- package/src/content-gate/access.d.ts +15 -0
- package/src/content-gate/config-types.d.ts +99 -0
- package/src/content-gate/gate.d.ts +13 -0
- package/src/content-gate/parse-cookie.d.ts +9 -0
- package/src/content-gate/token.d.ts +48 -0
- package/src/content-page-floor.d.ts +39 -0
- package/src/discover-blocks.d.ts +28 -0
- package/src/discover-blocks.ts +1 -1
- package/src/expand-roster.d.ts +34 -0
- package/src/fn-routers/agreement-completion-email.d.ts +14 -0
- package/src/fn-routers/agreement-send-router.d.ts +82 -0
- package/src/fn-routers/ai-health-router.d.ts +35 -0
- package/src/fn-routers/auth-router.d.ts +47 -0
- package/src/fn-routers/cache-admin-router.d.ts +57 -0
- package/src/fn-routers/chat-session-router.d.ts +219 -0
- package/src/fn-routers/email-defaults.d.ts +1 -0
- package/src/fn-routers/email-link-ttl.d.ts +1 -0
- package/src/fn-routers/index.d.ts +18 -0
- package/src/fn-routers/index.ts +0 -2
- package/src/fn-routers/panel-data-router.d.ts +19 -0
- package/src/fn-routers/persist-chat-messages.d.ts +36 -0
- package/src/fn-routers/plans-router.d.ts +25 -0
- package/src/fn-routers/proposal-send-router.d.ts +58 -0
- package/src/fn-routers/public-agreement-router.d.ts +289 -0
- package/src/fn-routers/public-assessment-router.d.ts +68 -0
- package/src/fn-routers/public-gpt-session-router.d.ts +143 -0
- package/src/fn-routers/sign-in-flow-router.d.ts +107 -0
- package/src/fn-routers/task-sync-assign.d.ts +305 -0
- package/src/fn-routers/task-sync-assign.test.ts +1 -2
- package/src/fn-routers/task-sync-assign.ts +4 -4
- package/src/fn-routers/task-sync-bind.test.ts +18 -18
- package/src/fn-routers/task-sync-brief.d.ts +96 -0
- package/src/fn-routers/task-sync-brief.test.ts +17 -20
- package/src/fn-routers/task-sync-brief.ts +14 -15
- package/src/fn-routers/task-sync-dispatch.d.ts +61 -0
- package/src/fn-routers/task-sync-dispatch.test.ts +33 -51
- package/src/fn-routers/task-sync-dispatch.ts +124 -0
- package/src/fn-routers/task-sync-lane.d.ts +49 -0
- package/src/fn-routers/task-sync-lane.ts +125 -0
- package/src/fn-routers/task-sync-projection.d.ts +121 -0
- package/src/fn-routers/task-sync-projection.test.ts +0 -26
- package/src/fn-routers/task-sync-projection.ts +0 -51
- package/src/fn-routers/task-sync-router.d.ts +377 -0
- package/src/fn-routers/task-sync-router.test.ts +2 -14
- package/src/fn-routers/task-sync-router.ts +1075 -0
- package/src/fn-routers/tasks-router.d.ts +25 -0
- package/src/fn-routers/user-preferences-router.d.ts +77 -0
- package/src/fn-routers/work-items-router.d.ts +102 -0
- package/src/gate-ttls.d.ts +6 -0
- package/src/gpt-gate.d.ts +25 -0
- package/src/handlers/bun-stable-watch.d.ts +1 -0
- package/src/handlers/pane-outbox-drain.d.ts +1 -0
- package/src/handlers/projects-digest.d.ts +26 -0
- package/src/handlers/prune-stale-data.d.ts +30 -0
- package/src/handlers/registry-stale-inactive-sweep.d.ts +1 -0
- package/src/handlers/scala-tools-currency-watch.d.ts +1 -0
- package/src/handlers/singleton-tools-currency-watch.d.ts +1 -0
- package/src/handlers/singleton-tools-currency-watch.ts +11 -114
- package/src/handlers/sweep-event-log.d.ts +1 -0
- package/src/handlers/sweep-host-cache.d.ts +1 -0
- package/src/handlers/sweep-host-cache.ts +26 -143
- package/src/handlers/upstream-issue-sync.d.ts +1 -0
- package/src/handlers/whatsapp-liveness-logic.d.ts +34 -0
- package/src/handlers/whatsapp-liveness.d.ts +4 -0
- package/src/hono-error-handler.d.ts +25 -0
- package/src/install-harness.d.ts +45 -0
- package/src/install-logging.d.ts +34 -0
- package/src/install-logging.ts +0 -3
- package/src/load-service-install-row.d.ts +24 -0
- package/src/load-service-prisma-client.d.ts +18 -0
- package/src/mount-orpc-router.d.ts +15 -0
- package/src/mount-orpc-router.ts +1 -1
- package/src/mount-server-packages.d.ts +29 -0
- package/src/mount.d.ts +9 -0
- package/src/mutable-app-ref.d.ts +44 -0
- package/src/page-route-shell-types.d.ts +12 -0
- package/src/page-server-orpc/audit-revert-router.d.ts +37 -0
- package/src/page-server-orpc/hub-mcp-client.d.ts +12 -0
- package/src/page-server-orpc/portal-diagnostics-router.d.ts +49 -0
- package/src/page-server-orpc/puck-pages-router.d.ts +58 -0
- package/src/page-server-orpc/server-client.d.ts +16 -0
- package/src/page-server-orpc/templates-router.d.ts +34 -0
- package/src/page-server-orpc/user-preferences-router.d.ts +79 -0
- package/src/pg-boss-handler-registry.d.ts +27 -0
- package/src/pg-boss-mount.d.ts +42 -0
- package/src/proposal-authorized-emails.d.ts +28 -0
- package/src/proposal-chat-inject.d.ts +17 -0
- package/src/proposal-gate.d.ts +18 -0
- package/src/read-html-template.d.ts +38 -0
- package/src/read-viewport-from-request.d.ts +18 -0
- package/src/readiness-gate.d.ts +16 -0
- package/src/register-api-routes.d.ts +18 -0
- package/src/register-crash-handlers.d.ts +15 -0
- package/src/register-page-routes.d.ts +27 -0
- package/src/register-page-routes.ts +3 -0
- package/src/register-page-server-client.d.ts +11 -0
- package/src/register-registry-page-route/ai-chat-prefetch.d.ts +13 -0
- package/src/register-registry-page-route/graph-cache.d.ts +30 -0
- package/src/register-registry-page-route/meta-title.d.ts +2 -0
- package/src/register-registry-page-route/proposal-approval-email-token.d.ts +14 -0
- package/src/register-registry-page-route/proposal-brand-stamping.d.ts +28 -0
- package/src/register-registry-page-route/resolve-page-data.d.ts +1 -0
- package/src/register-registry-page-route.d.ts +27 -0
- package/src/register-static-assets.d.ts +13 -0
- package/src/register-ws-routes.d.ts +28 -0
- package/src/resolve-app-base-url.d.ts +13 -0
- package/src/resolve-brand-favicon.d.ts +28 -0
- package/src/resolve-brand-favicon.ts +1 -1
- package/src/resolve-from-loader-config.d.ts +32 -0
- package/src/resolve-service-context.d.ts +17 -0
- package/src/resolve-service-port.d.ts +75 -0
- package/src/resolve-theme-mode.d.ts +42 -0
- package/src/resolvers/fetch-org-brand.d.ts +2 -0
- package/src/resolvers/portal-route-paths.d.ts +39 -0
- package/src/resolvers/website-by-host-map.d.ts +12 -0
- package/src/resource-chat-inject.d.ts +46 -0
- package/src/resource-not-found.d.ts +60 -0
- package/src/resource-not-found.ts +0 -10
- package/src/run-boot-hook-list.d.ts +19 -0
- package/src/run-boot-hooks.d.ts +31 -0
- package/src/scala-tools-currency-watchdog.d.ts +10 -0
- package/src/security-headers.d.ts +12 -0
- package/src/serve/serve.d.ts +46 -0
- package/src/serve/stamp-transport-identity.d.ts +32 -0
- package/src/serve/stamp-transport-identity.ts +2 -14
- package/src/server-error-page.d.ts +29 -0
- package/src/start-scheduled-jobs.d.ts +18 -0
- package/src/two-phase-boot.d.ts +136 -0
- package/src/two-phase-boot.ts +17 -85
- package/src/two-phase-health-gate.test.ts +2 -13
- package/src/types.d.ts +46 -0
- package/src/ui-service.d.ts +46 -0
- package/src/ws-boot-self-test.d.ts +19 -0
- package/src/auth-gate.test.ts +0 -70
- package/src/fn-routers/task-sync-close.test.ts +0 -44
- package/src/fn-routers/task-sync-close.ts +0 -82
- package/src/fn-routers/task-sync-hook-lifecycle.test.ts +0 -204
- package/src/fn-routers/task-sync-lane.test.ts +0 -36
- package/src/fn-routers/task-sync-rename.test.ts +0 -53
- package/src/fn-routers/task-sync-rename.ts +0 -111
- package/src/handlers/sweep-host-cache.test.ts +0 -133
- package/src/serve/stamp-transport-identity.test.ts +0 -24
- package/src/two-phase-health-gate-client-only.test.ts +0 -77
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* The branded HTML error page for a FAILED PAGE RENDER — the page-server
|
|
7
|
+
* render catch's response surface (constitution `error-responses-via-error-
|
|
8
|
+
* report`). When SSR resolve+render fails (render budget exhausted, DEGRADED
|
|
9
|
+
* protective layer, a thrown loader), the browser is mid-navigation: serving
|
|
10
|
+
* a JSON body there is the violation this closes (2026-07-24: the operator
|
|
11
|
+
* saw a raw `{"error":"Request failed"}` blob where the dashboard should be).
|
|
12
|
+
*
|
|
13
|
+
* Classification stays in `buildErrorReport` (`@teamscala/os`) — the ONE
|
|
14
|
+
* classifier; this file is ONLY the HTML shaper for the page-navigation
|
|
15
|
+
* surface (API/ORPC `onError` keeps the JSON body, consumed by code). It
|
|
16
|
+
* renders the report's already-decided status + Retry-After + message, so the
|
|
17
|
+
* screen, the log line, and the GlitchTip event all share one `errorId`.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately NOT a Puck page: it must render even when data loading is what
|
|
20
|
+
* failed. Sibling of `resource-not-found.ts` (the missing-resource floor) —
|
|
21
|
+
* same dark branded template + org-logo resolver, distinct trigger.
|
|
22
|
+
*/
|
|
23
|
+
import type { ErrorReport } from "@teamscala/os/errors/error-report";
|
|
24
|
+
/**
|
|
25
|
+
* Render an `ErrorReport` as a branded HTML page. Pure shaper — the caller
|
|
26
|
+
* classifies via `buildErrorReport` and resolves the logo first. Skew-tolerant
|
|
27
|
+
* by construction: a missing logo degrades to a plain branded dark page.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildServerErrorPageResponse(report: ErrorReport, logoUrl?: string | null): Response;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system server-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Starts `service_install.scheduled_jobs[]`. Two kinds:
|
|
7
|
+
*
|
|
8
|
+
* - `long_running`: handler invoked once at boot, runs for the lifetime of
|
|
9
|
+
* the process (e.g. SSE subscribers, WAL watchers). Failures are logged.
|
|
10
|
+
* - `recurring`: declared here but cron scheduling lands via pg-boss in a
|
|
11
|
+
* follow-up — currently a no-op log.
|
|
12
|
+
*
|
|
13
|
+
* Implementations are registered by server_packages via
|
|
14
|
+
* ctx.registerScheduledJob during the mount phase.
|
|
15
|
+
*/
|
|
16
|
+
import type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|
|
17
|
+
import type { MountRegistry } from "@teamscala/os/runtime-contracts/mount-context";
|
|
18
|
+
export declare function startScheduledJobs(config: ServiceInstallConfig, mountRegistry: MountRegistry, serviceSlug: string): void;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Two-phase boot for UI services. Splits the boot process into:
|
|
7
|
+
* 1. Minimal app (tool-only surface): ORPC, data routes, auth gates
|
|
8
|
+
* 2. Full app (with pages and hydration): adds block discovery, page routes,
|
|
9
|
+
* client asset building, boot hooks, scheduled jobs
|
|
10
|
+
*
|
|
11
|
+
* The minimal app starts serving immediately (<10s), then the full app is
|
|
12
|
+
* built in the background and atomically swapped in. This eliminates the
|
|
13
|
+
* ~25s channel outage window during scala-agents restart.
|
|
14
|
+
*/
|
|
15
|
+
import { Hono, type Context } from "hono";
|
|
16
|
+
import type { ServiceInstallConfig } from "./load-service-install-row.ts";
|
|
17
|
+
import { mountServerPackages } from "./mount-server-packages.ts";
|
|
18
|
+
import type { PageServerDeps } from "./ui-service.ts";
|
|
19
|
+
import type { CreatePageHandler } from "./types.ts";
|
|
20
|
+
/**
|
|
21
|
+
* Set the real page handler (called when Phase 2 completes).
|
|
22
|
+
*/
|
|
23
|
+
export declare function setPageHandler(handler: CreatePageHandler): void;
|
|
24
|
+
/**
|
|
25
|
+
* Get the current page handler state.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getPageHandlerState(): {
|
|
28
|
+
ready: boolean;
|
|
29
|
+
hasHandler: boolean;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* `/health` for a UI service — 200 ONLY once the full app can actually render
|
|
33
|
+
* a page, 503 before that.
|
|
34
|
+
*
|
|
35
|
+
* This endpoint is not decoration: it is the wake gate for every scale-to-zero
|
|
36
|
+
* app. `scala-activator` (`@teamscala/scale-to-zero`) holds the client's TCP
|
|
37
|
+
* connection through a cold wake and pipes it the instant `isHealthy(port)` —
|
|
38
|
+
* a plain `GET /health` — returns ok, which is what `scale-to-zero-apps-sleep-
|
|
39
|
+
* on-demand` means by "`/health`-gates the wake (the client never sees a boot
|
|
40
|
+
* 503)".
|
|
41
|
+
*
|
|
42
|
+
* It previously answered a flat `{status:"ok"}` as soon as the MINIMAL app was
|
|
43
|
+
* serving, because the readiness gate flips ready after phase 1 so the ORPC/MCP
|
|
44
|
+
* surface can accept traffic early. But phase 1 CANNOT render: the phase-1 page
|
|
45
|
+
* handler below returns 503 until `setPageHandler` runs at the phase-2 swap. So
|
|
46
|
+
* `/health` reported ready while every page 503'd, the activator believed the
|
|
47
|
+
* wake had landed, and it piped the customer's first request straight into that
|
|
48
|
+
* window — the customer got the 503.
|
|
49
|
+
*
|
|
50
|
+
* Measured 2026-07-31 on quiz-dev/sign-dev: the activator logged `awake` for
|
|
51
|
+
* scala-quiz at 05:58:25.446 and the app logged `GET / 503 3ms` at 05:58:25.452
|
|
52
|
+
* — 6ms later, and 1004ms BEFORE "Swapped from minimal app to full app". Nine
|
|
53
|
+
* such 503s on scala-quiz and two on scala-sign, every one 0-23ms (the
|
|
54
|
+
* placeholder, which does no render work). scala-public logged ZERO, which is
|
|
55
|
+
* the whole reason it looked healthy while the other two did not: the gap is a
|
|
56
|
+
* RACE whose width is the phase-1→phase-2 build time (1420ms cold, 137ms warm),
|
|
57
|
+
* not a config difference between the apps.
|
|
58
|
+
*
|
|
59
|
+
* Gating this on `fullAppReady` makes the signal honest for every consumer at
|
|
60
|
+
* once (the activator's wake gate, `poller-post-restart-health-gate`, any
|
|
61
|
+
* health-gated router) rather than teaching each one its own probe, and it is
|
|
62
|
+
* what `readiness-gated-boot` already requires: "/health returns 503 until
|
|
63
|
+
* ready, 200 after — so it is the real readiness signal", a service being
|
|
64
|
+
* "binary: fully-ready or cleanly-unavailable". A UI service that cannot render
|
|
65
|
+
* is not ready, per `liveness-by-capability-not-process-existence` — liveness is
|
|
66
|
+
* a probe of the capability being REQUESTED, never of process existence or of
|
|
67
|
+
* some other surface that happens to answer.
|
|
68
|
+
*
|
|
69
|
+
* The early readiness-gate flip is deliberately left alone: it gates the ORPC
|
|
70
|
+
* tool surface, which genuinely IS usable at phase 1. Only this endpoint, whose
|
|
71
|
+
* one consumer contract is "can you serve me a page", is moved to phase 2.
|
|
72
|
+
*/
|
|
73
|
+
export declare function healthHandler(c: Context): Response;
|
|
74
|
+
export interface MinimalAppResult {
|
|
75
|
+
app: Hono;
|
|
76
|
+
mountRegistry: Awaited<ReturnType<typeof mountServerPackages>>;
|
|
77
|
+
apiRouteCount: number;
|
|
78
|
+
wsRouteCount: number;
|
|
79
|
+
pageRouteCount: number;
|
|
80
|
+
config: ServiceInstallConfig;
|
|
81
|
+
serviceSlug: string;
|
|
82
|
+
databaseUrl: string;
|
|
83
|
+
blockPackages: string[];
|
|
84
|
+
components: Record<string, unknown>;
|
|
85
|
+
loaded: string[];
|
|
86
|
+
skipped: string[];
|
|
87
|
+
}
|
|
88
|
+
export interface FullAppResult {
|
|
89
|
+
app: Hono;
|
|
90
|
+
blocks: {
|
|
91
|
+
slug: string;
|
|
92
|
+
packagePath: string;
|
|
93
|
+
}[];
|
|
94
|
+
routes: string[];
|
|
95
|
+
pgBossJobs: Array<{
|
|
96
|
+
handlerSlug: string;
|
|
97
|
+
}>;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Build the minimal tool-only app. This excludes the expensive operations:
|
|
101
|
+
* - Client asset building (Vite build)
|
|
102
|
+
* - Boot hooks (may have expensive initialization)
|
|
103
|
+
* - Scheduled jobs (can start later)
|
|
104
|
+
* - pg-boss (can start later)
|
|
105
|
+
*
|
|
106
|
+
* CRITICAL: All route registration MUST happen here before serving, because
|
|
107
|
+
* Hono's RegExpRouter compiles on the first request and becomes immutable.
|
|
108
|
+
* Once compiled, no new routes can be added. So we include:
|
|
109
|
+
* - Block discovery and page routes (registers all page paths)
|
|
110
|
+
* - All other routes (API, WS, data, static assets)
|
|
111
|
+
*
|
|
112
|
+
* The minimal app includes everything needed for MCP tools and ORPC:
|
|
113
|
+
* - Error handling, readiness gate, health check
|
|
114
|
+
* - Security headers, CORS, compression, timing
|
|
115
|
+
* - Content gate access, auth gates, static assets
|
|
116
|
+
* - ORPC router, API routes, WS routes, data routes, page routes
|
|
117
|
+
*/
|
|
118
|
+
export declare function buildMinimalApp(config: ServiceInstallConfig, serviceSlug: string, databaseUrl: string): Promise<MinimalAppResult>;
|
|
119
|
+
/**
|
|
120
|
+
* Build the full app with hydration. This adds the expensive operations that
|
|
121
|
+
* were deferred from the minimal app:
|
|
122
|
+
* - Client asset building (Vite build)
|
|
123
|
+
* - Boot hooks (may have expensive initialization)
|
|
124
|
+
* - Scheduled jobs (can start later)
|
|
125
|
+
* - pg-boss (can start later)
|
|
126
|
+
*
|
|
127
|
+
* This runs in the background after the minimal app is already serving.
|
|
128
|
+
* NOTE: All routes (including page routes) are already registered in Phase 1,
|
|
129
|
+
* so we don't need to modify the app here. The app reference stays the same.
|
|
130
|
+
*/
|
|
131
|
+
export declare function buildFullApp(minimalResult: MinimalAppResult, deps: PageServerDeps): Promise<FullAppResult>;
|
|
132
|
+
/**
|
|
133
|
+
* Swap the minimal app for the full app. This atomically replaces the
|
|
134
|
+
* app reference that Bun.serve is using.
|
|
135
|
+
*/
|
|
136
|
+
export declare function swapToFullApp(fullApp: Hono, serviceSlug: string): void;
|
package/src/two-phase-boot.ts
CHANGED
|
@@ -54,7 +54,6 @@ import { registerPageRoutes } from "./register-page-routes.ts";
|
|
|
54
54
|
import { runBootHookList } from "./run-boot-hook-list.ts";
|
|
55
55
|
import { buildClientAssets } from "./build-client-assets.ts";
|
|
56
56
|
import { discoverBlocks } from "./discover-blocks.ts";
|
|
57
|
-
import { registerPageServerClient } from "./register-page-server-client.ts";
|
|
58
57
|
import { swapApp } from "./mutable-app-ref.ts";
|
|
59
58
|
import type { CreatePageHandler } from "./types.ts";
|
|
60
59
|
|
|
@@ -64,60 +63,13 @@ let realPageHandler: CreatePageHandler | null = null;
|
|
|
64
63
|
let fullAppReady = false;
|
|
65
64
|
|
|
66
65
|
/**
|
|
67
|
-
* Set the real page handler (called when Phase 2 completes
|
|
68
|
-
*
|
|
69
|
-
* Registering the SSR handler implies the full app is built, so this also flips
|
|
70
|
-
* readiness — but the reverse does NOT hold, which is why `markFullAppReady`
|
|
71
|
-
* exists separately. See its note.
|
|
66
|
+
* Set the real page handler (called when Phase 2 completes).
|
|
72
67
|
*/
|
|
73
68
|
export function setPageHandler(handler: CreatePageHandler): void {
|
|
74
69
|
realPageHandler = handler;
|
|
75
|
-
markFullAppReady();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Flip readiness once phase 2 has built the full app.
|
|
80
|
-
*
|
|
81
|
-
* SEPARATE FROM `setPageHandler` BECAUSE THE TWO FACTS CAME APART. Readiness
|
|
82
|
-
* means "phase 2 finished"; the page handler means "this service renders pages
|
|
83
|
-
* server-side". Every service was SSR, so one flag honestly carried both — until
|
|
84
|
-
* `render_mode=client` (`granular runtime`) made a service that completes phase 2
|
|
85
|
-
* and never registers an SSR handler.
|
|
86
|
-
*
|
|
87
|
-
* Leaving readiness inside `setPageHandler` made such a service PERMANENTLY
|
|
88
|
-
* un-ready: `/health` answered `{"status":"starting"}` forever while the process
|
|
89
|
-
* was healthy and serving. That is the failure `readiness-gated-boot` is written
|
|
90
|
-
* against — a service must be "binary: fully-ready or cleanly-unavailable", and
|
|
91
|
-
* a never-ready health signal makes the activator's wake gate and every
|
|
92
|
-
* health-gated router treat a working service as down.
|
|
93
|
-
*
|
|
94
|
-
* Measured 2026-08-07 on scala-agents-ui: boot completed in 5023ms, "Full app
|
|
95
|
-
* built: 50 block(s)" logged, `GET /` correctly 302'd to sign-in, and `/health`
|
|
96
|
-
* still reported starting — while three untouched UI services reported ok.
|
|
97
|
-
*/
|
|
98
|
-
export function markFullAppReady(): void {
|
|
99
70
|
fullAppReady = true;
|
|
100
71
|
}
|
|
101
72
|
|
|
102
|
-
/**
|
|
103
|
-
* Reset both phase-2 flags. TEST-ONLY.
|
|
104
|
-
*
|
|
105
|
-
* `fullAppReady` is module state that only ever transitions false -> true in
|
|
106
|
-
* production, and Bun shares a module instance across every test file in a run —
|
|
107
|
-
* so the SSR readiness test and the client-only readiness test contaminate each
|
|
108
|
-
* other purely by file order. Splitting them across files does NOT isolate them;
|
|
109
|
-
* it only hides the coupling until CI runs them together.
|
|
110
|
-
*
|
|
111
|
-
* A test that passes alone and fails in a suite is worse than no test, so the
|
|
112
|
-
* reset is exported rather than left as an ordering convention nobody can see.
|
|
113
|
-
* Never called from production code — `setPageHandler`/`markFullAppReady` are
|
|
114
|
-
* the only writers on the boot path.
|
|
115
|
-
*/
|
|
116
|
-
export function __resetPhase2StateForTest(): void {
|
|
117
|
-
realPageHandler = null;
|
|
118
|
-
fullAppReady = false;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
73
|
/**
|
|
122
74
|
* Get the current page handler state.
|
|
123
75
|
*/
|
|
@@ -295,6 +247,16 @@ export async function buildMinimalApp(
|
|
|
295
247
|
preloadServerPackages(serverPackages),
|
|
296
248
|
);
|
|
297
249
|
const ttlPromise = runStep("init-gate-ttls", () => initGateTtls());
|
|
250
|
+
const discoverPromise = runStep("discover-blocks", () =>
|
|
251
|
+
discoverBlocks(blockPackages),
|
|
252
|
+
);
|
|
253
|
+
// Attach an immediate rejection sink: discoverPromise is fired here but
|
|
254
|
+
// awaited much later (after the gate chain + mounts). If discoverBlocks
|
|
255
|
+
// rejects during that overlap window, the rejection would surface as an
|
|
256
|
+
// unhandledRejection before the await below can catch it. The sink is a
|
|
257
|
+
// no-op — the real error still propagates at the `await discoverPromise`
|
|
258
|
+
// site because each consumer of a rejected promise receives the rejection.
|
|
259
|
+
discoverPromise.catch(() => {});
|
|
298
260
|
|
|
299
261
|
// Public access endpoints — MUST be registered before the auth gate (Hono
|
|
300
262
|
// dispatches middleware + handlers in registration order; the content-gate
|
|
@@ -343,21 +305,6 @@ export async function buildMinimalApp(
|
|
|
343
305
|
await runStep("mount-orpc-router", () => mountOrpcRouter(app, serviceSlug));
|
|
344
306
|
}
|
|
345
307
|
|
|
346
|
-
// Register the in-process ORPC server client BEFORE discoverBlocks — it
|
|
347
|
-
// calls getServerClient() which needs the client set here. Must follow
|
|
348
|
-
// mount-orpc-router (which builds the router getAppRouter uses).
|
|
349
|
-
// Previously ran inside register-page-routes (too late — discoverBlocks
|
|
350
|
-
// fired before it, hitting the empty-router fallback).
|
|
351
|
-
await runStep("register-page-server-client", () =>
|
|
352
|
-
registerPageServerClient(serviceSlug),
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
// NOW fire discoverBlocks (getServerClient is set). Awaited below before
|
|
356
|
-
// register-page-routes consumes the result.
|
|
357
|
-
const discoverPromise = runStep("discover-blocks", () =>
|
|
358
|
-
discoverBlocks(blockPackages),
|
|
359
|
-
);
|
|
360
|
-
|
|
361
308
|
const wsRouteCount = await runStep("register-ws-routes", () =>
|
|
362
309
|
registerWsRoutes(app, config, mountRegistry, serviceSlug),
|
|
363
310
|
);
|
|
@@ -429,8 +376,7 @@ export async function buildMinimalApp(
|
|
|
429
376
|
// (the same pulsating-logo holding page scale-to-zero uses) —
|
|
430
377
|
// never bare plain text (loading-via-logo-loading).
|
|
431
378
|
if (!fullAppReady || !realPageHandler) {
|
|
432
|
-
|
|
433
|
-
return new Response(buildWakingPageHtml({ logoUrl: logo }), {
|
|
379
|
+
return new Response(buildWakingPageHtml({}), {
|
|
434
380
|
status: 503,
|
|
435
381
|
headers: {
|
|
436
382
|
"content-type": "text/html; charset=utf-8",
|
|
@@ -552,25 +498,11 @@ export async function buildFullApp(
|
|
|
552
498
|
`${Object.keys(components).length} component(s)`,
|
|
553
499
|
);
|
|
554
500
|
|
|
555
|
-
// Set the real page handler so the dynamic handler in Phase 1 can delegate to it
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
setPageHandler(deps.createPageHandler);
|
|
561
|
-
log.info(
|
|
562
|
-
`[two-phase-boot:${serviceSlug}] Real page handler registered - page routes now delegate to full app`,
|
|
563
|
-
);
|
|
564
|
-
} else {
|
|
565
|
-
// The full app IS built — this service simply never renders server-side,
|
|
566
|
-
// so readiness is flipped directly rather than as a side effect of
|
|
567
|
-
// registering an SSR handler that will never exist. Without this a
|
|
568
|
-
// client-only service is permanently un-ready (see markFullAppReady).
|
|
569
|
-
markFullAppReady();
|
|
570
|
-
log.info(
|
|
571
|
-
`[two-phase-boot:${serviceSlug}] Client-only mode (render_mode=client) — no SSR page handler registered`,
|
|
572
|
-
);
|
|
573
|
-
}
|
|
501
|
+
// Set the real page handler so the dynamic handler in Phase 1 can delegate to it
|
|
502
|
+
setPageHandler(deps.createPageHandler);
|
|
503
|
+
log.info(
|
|
504
|
+
`[two-phase-boot:${serviceSlug}] Real page handler registered - page routes now delegate to full app`,
|
|
505
|
+
);
|
|
574
506
|
|
|
575
507
|
const resolvedBlocks = loaded.map((slug) => ({
|
|
576
508
|
slug,
|
|
@@ -27,14 +27,9 @@
|
|
|
27
27
|
* 137ms warm), which is why one app looked healthy and the others did not.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import {
|
|
30
|
+
import { describe, expect, test } from "bun:test";
|
|
31
31
|
import { Hono } from "hono";
|
|
32
|
-
import {
|
|
33
|
-
__resetPhase2StateForTest,
|
|
34
|
-
getPageHandlerState,
|
|
35
|
-
healthHandler,
|
|
36
|
-
setPageHandler,
|
|
37
|
-
} from "./two-phase-boot.ts";
|
|
32
|
+
import { healthHandler, setPageHandler, getPageHandlerState } from "./two-phase-boot.ts";
|
|
38
33
|
|
|
39
34
|
/** Mount the real handler exactly as buildMinimalApp does. */
|
|
40
35
|
const app = new Hono().get("/health", healthHandler);
|
|
@@ -46,12 +41,6 @@ const app = new Hono().get("/health", healthHandler);
|
|
|
46
41
|
* hook and adding one would exist only for tests.
|
|
47
42
|
*/
|
|
48
43
|
describe("/health is gated on page-serving readiness", () => {
|
|
49
|
-
// Bun shares one module instance across test files, so a sibling file that
|
|
50
|
-
// marks the app ready would otherwise decide this file's first assertion.
|
|
51
|
-
beforeAll(() => {
|
|
52
|
-
__resetPhase2StateForTest();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
44
|
test("before the phase-2 swap it reports 503, not ready", async () => {
|
|
56
45
|
expect(getPageHandlerState().ready).toBe(false);
|
|
57
46
|
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Shared type definitions for service-boot boot helpers.
|
|
7
|
+
*/
|
|
8
|
+
type RendererComponent = (props: Record<string, unknown>) => unknown;
|
|
9
|
+
export interface RendererComponentEntry {
|
|
10
|
+
render: RendererComponent;
|
|
11
|
+
fields?: Record<string, {
|
|
12
|
+
type: string;
|
|
13
|
+
}>;
|
|
14
|
+
/** block_schema.defaults, carried onto the entry by discoverBlocks so
|
|
15
|
+
* buildPuckConfig merges them under authored props at SSR
|
|
16
|
+
* (blocks-fully-programmable). */
|
|
17
|
+
defaultProps?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface PageHandlerExtras {
|
|
20
|
+
htmlTemplate: string;
|
|
21
|
+
userId?: string;
|
|
22
|
+
orgId?: string;
|
|
23
|
+
searchParams?: Record<string, string>;
|
|
24
|
+
nonce?: string;
|
|
25
|
+
}
|
|
26
|
+
export type CreatePageHandler = (config: PageHandlerConfig) => (request: Request, extras: PageHandlerExtras) => Promise<Response>;
|
|
27
|
+
export interface PageHandlerConfig {
|
|
28
|
+
components: Record<string, RendererComponentEntry>;
|
|
29
|
+
providers?: string[];
|
|
30
|
+
loadData: (request: Request) => Promise<unknown>;
|
|
31
|
+
renderMode?: "public" | "portals" | "utility";
|
|
32
|
+
/**
|
|
33
|
+
* Owning service slug — used by the page-server render catch to resolve
|
|
34
|
+
* the branded logo on the render-error page. Mirrors service-runtime's
|
|
35
|
+
* PageHandlerConfig.
|
|
36
|
+
*/
|
|
37
|
+
serviceSlug?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Redirect a signed-in user's `/` to their org-scoped portal dashboard.
|
|
40
|
+
* Set only for services serving a `/*` catchall page_route (the portal);
|
|
41
|
+
* single-site portal-mode services serve their real page at `/`.
|
|
42
|
+
* Mirrors service-runtime's PageHandlerConfig (1.15.82).
|
|
43
|
+
*/
|
|
44
|
+
portalHomeRedirect?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Boots a `kind: "ui"` service. Reads the service_install registry row and
|
|
7
|
+
* orchestrates a fixed pipeline of single-purpose stages onto one Hono app
|
|
8
|
+
* served by @teamscala/service-runtime/serve (one Bun.serve in dev and prod).
|
|
9
|
+
*/
|
|
10
|
+
import { Hono } from "hono";
|
|
11
|
+
import type { ServiceInstallConfig } from "./load-service-install-row.ts";
|
|
12
|
+
export interface ResolvedBlock {
|
|
13
|
+
slug: string;
|
|
14
|
+
packagePath: string;
|
|
15
|
+
}
|
|
16
|
+
export interface UiServiceBootResult {
|
|
17
|
+
kind: "ui";
|
|
18
|
+
blocks: ResolvedBlock[];
|
|
19
|
+
routes: string[];
|
|
20
|
+
port: number;
|
|
21
|
+
app: Hono;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Page-server symbols injected by the caller. service-boot is tier 4
|
|
25
|
+
* (server) and must not import from tier 2 (`@teamscala/service-runtime`)
|
|
26
|
+
* per `installer-and-runtime` Rule 1. The runtime's bin entrypoint
|
|
27
|
+
* boot pipeline imports these from `page-server/` (same package —
|
|
28
|
+
* legal) and passes them down here.
|
|
29
|
+
*/
|
|
30
|
+
export interface PageServerDeps {
|
|
31
|
+
createPageHandler: import("./types.ts").CreatePageHandler;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Boots a UI service using two-phase boot for fast availability.
|
|
35
|
+
*
|
|
36
|
+
* Phase 1 (fast): Build and serve minimal tool-only app (ORPC, data routes,
|
|
37
|
+
* auth gates). This becomes available within ~10s, enabling the MCP tool
|
|
38
|
+
* surface to accept requests immediately after restart.
|
|
39
|
+
*
|
|
40
|
+
* Phase 2 (background): Build full app (pages, hydration) and atomically swap
|
|
41
|
+
* it in. The readiness gate flips to ready after Phase 1, not Phase 2.
|
|
42
|
+
*
|
|
43
|
+
* The interim mitigation (503 + gateway retry + circuit breaker) remains in
|
|
44
|
+
* place and is NOT undone.
|
|
45
|
+
*/
|
|
46
|
+
export declare function bootUiService(config: ServiceInstallConfig, serviceSlug: string, deps: PageServerDeps, databaseUrl: string): Promise<UiServiceBootResult>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system server-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Post-listen WebSocket self-test. Runs after serveStart binds the port.
|
|
7
|
+
* For each declared websocket_routes path, opens a synthetic WebSocket
|
|
8
|
+
* connection and asserts 101 upgrade. Non-101 → BootFatalError.
|
|
9
|
+
*
|
|
10
|
+
* This catches the class of bugs where:
|
|
11
|
+
* - WS route registered after ORPC catch-all (order-dependent 404)
|
|
12
|
+
* - fetchWithPeerPort clones the Request on WS upgrade (breaks Bun.upgrade identity)
|
|
13
|
+
* - websocket_routes config row is stale / missing a handler
|
|
14
|
+
*
|
|
15
|
+
* Runs synchronously after serveStart so the boot aborts before the service
|
|
16
|
+
* reports healthy.
|
|
17
|
+
*/
|
|
18
|
+
import type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|
|
19
|
+
export declare function runWsBootSelfTest(config: ServiceInstallConfig, serviceSlug: string, port: number): Promise<void>;
|
package/src/auth-gate.test.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The signed-out screen must be able to render its own branding.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, expect, test } from "bun:test";
|
|
10
|
-
import { isStaticAssetPath } from "./auth-gate.ts";
|
|
11
|
-
|
|
12
|
-
describe("static assets are never behind the login gate", () => {
|
|
13
|
-
/**
|
|
14
|
-
* The regression this file exists for.
|
|
15
|
-
*
|
|
16
|
-
* Public paths were an ENUMERATED allowlist: `/assets/` was on it, `/logos/`
|
|
17
|
-
* never was. So `/logos/cropped.png` matched no prefix, fell through to the
|
|
18
|
-
* session gate, and answered 302 → /sign-in. A browser cannot act on a
|
|
19
|
-
* redirect it received for an <img>, so it painted nothing — a blank logo
|
|
20
|
-
* pulsing under a "starting up" splash, on the ONE screen guaranteed to need
|
|
21
|
-
* branding, for a user who by definition has no session.
|
|
22
|
-
*
|
|
23
|
-
* It kept coming back because nothing was broken by any change: the list
|
|
24
|
-
* simply never covered the path, and any build that moves images re-creates
|
|
25
|
-
* the gap. Enumerating what is public makes every new asset private by
|
|
26
|
-
* default; a predicate makes that unrepresentable.
|
|
27
|
-
*/
|
|
28
|
-
test("the logo that broke the signed-out screen is public", () => {
|
|
29
|
-
expect(isStaticAssetPath("/logos/cropped.png")).toBe(true);
|
|
30
|
-
expect(isStaticAssetPath("/logos/companyLogo.png")).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("every asset kind a browser fetches without a session is public", () => {
|
|
34
|
-
for (const p of [
|
|
35
|
-
"/logos/dark.svg",
|
|
36
|
-
"/img/hero.webp",
|
|
37
|
-
"/brand/mark.avif",
|
|
38
|
-
"/fonts/inter.woff2",
|
|
39
|
-
"/styles/main.css",
|
|
40
|
-
"/assets/chunk-4efmn1eh.js",
|
|
41
|
-
"/favicon.ico",
|
|
42
|
-
]) {
|
|
43
|
-
expect(isStaticAssetPath(p)).toBe(true);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("case does not decide whether an image is reachable", () => {
|
|
48
|
-
expect(isStaticAssetPath("/logos/Company.PNG")).toBe(true);
|
|
49
|
-
expect(isStaticAssetPath("/logos/Mark.SVG")).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* The predicate widens what is PUBLIC, so its limit is the security-relevant
|
|
54
|
-
* half: pages and APIs must keep failing closed. A path is an asset because
|
|
55
|
-
* of its EXTENSION, never because it merely contains an asset-looking word.
|
|
56
|
-
*/
|
|
57
|
-
test("pages and APIs are still gated", () => {
|
|
58
|
-
for (const p of [
|
|
59
|
-
"/",
|
|
60
|
-
"/dashboard",
|
|
61
|
-
"/api/rpc/fn/workItems/list",
|
|
62
|
-
"/settings/logos",
|
|
63
|
-
"/logos",
|
|
64
|
-
"/png",
|
|
65
|
-
"/reports/quarterly.pdf",
|
|
66
|
-
]) {
|
|
67
|
-
expect(isStaticAssetPath(p)).toBe(false);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, expect, it } from "bun:test";
|
|
8
|
-
import { closedTaskState } from "./task-sync-close.ts";
|
|
9
|
-
|
|
10
|
-
const task = {
|
|
11
|
-
id: "task-1",
|
|
12
|
-
kind_data: {
|
|
13
|
-
lane: { session: "acp-session", cli_kind: "zerostack", live: true },
|
|
14
|
-
assignee_type: "agent",
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
describe("closedTaskState", () => {
|
|
19
|
-
it("marks completed work done and the lane non-live", () => {
|
|
20
|
-
expect(closedTaskState(task, "completed")).toEqual({
|
|
21
|
-
task_id: "task-1",
|
|
22
|
-
task_status: "done",
|
|
23
|
-
completed: true,
|
|
24
|
-
kind_data: {
|
|
25
|
-
lane: { session: "acp-session", cli_kind: "zerostack", live: false },
|
|
26
|
-
assignee_type: "agent",
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("archives every disposition that does not prove completion", () => {
|
|
32
|
-
for (const disposition of [
|
|
33
|
-
"needs_retry",
|
|
34
|
-
"failed",
|
|
35
|
-
"orchestrator_closed",
|
|
36
|
-
"orphaned",
|
|
37
|
-
] as const) {
|
|
38
|
-
const state = closedTaskState(task, disposition);
|
|
39
|
-
expect(state.task_status).toBe("archived");
|
|
40
|
-
expect(state.completed).toBe(false);
|
|
41
|
-
expect((state.kind_data.lane as { live: boolean }).live).toBe(false);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Lane-close write-through for task-sync. The session-owning fleet runtime
|
|
7
|
-
* calls this ORPC procedure before releasing an ACP lane, so work_items remains
|
|
8
|
-
* the durable lifecycle source without a second raw-SQL writer in the host.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { ORPCError } from "@orpc/server";
|
|
12
|
-
import * as v from "valibot";
|
|
13
|
-
import { authed, type Context } from "./auth-router.ts";
|
|
14
|
-
import type { Prisma, WorkItem } from "./task-sync-router.ts";
|
|
15
|
-
|
|
16
|
-
export const closeLaneInput = v.object({
|
|
17
|
-
lane_session: v.pipe(v.string(), v.minLength(1, "lane_session is required")),
|
|
18
|
-
disposition: v.picklist([
|
|
19
|
-
"completed",
|
|
20
|
-
"needs_retry",
|
|
21
|
-
"failed",
|
|
22
|
-
"orchestrator_closed",
|
|
23
|
-
"orphaned",
|
|
24
|
-
]),
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
export interface ClosedTaskState {
|
|
28
|
-
task_id: string;
|
|
29
|
-
task_status: "done" | "archived";
|
|
30
|
-
completed: boolean;
|
|
31
|
-
kind_data: Record<string, unknown>;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function closedTaskState(
|
|
35
|
-
task: Pick<WorkItem, "id" | "kind_data">,
|
|
36
|
-
disposition: v.InferOutput<typeof closeLaneInput>["disposition"],
|
|
37
|
-
): ClosedTaskState {
|
|
38
|
-
const completed = disposition === "completed";
|
|
39
|
-
const lane =
|
|
40
|
-
task.kind_data.lane && typeof task.kind_data.lane === "object"
|
|
41
|
-
? (task.kind_data.lane as Record<string, unknown>)
|
|
42
|
-
: {};
|
|
43
|
-
return {
|
|
44
|
-
task_id: task.id,
|
|
45
|
-
task_status: completed ? "done" : "archived",
|
|
46
|
-
completed,
|
|
47
|
-
kind_data: { ...task.kind_data, lane: { ...lane, live: false } },
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export const closeLaneHandler = authed
|
|
52
|
-
.input(closeLaneInput)
|
|
53
|
-
.handler(async ({ input, context }) => {
|
|
54
|
-
const prisma = (context as Context).prisma as Prisma;
|
|
55
|
-
const task = await prisma.work_items.findFirst({
|
|
56
|
-
where: {
|
|
57
|
-
kind: "task",
|
|
58
|
-
kind_data: { path: ["lane", "session"], equals: input.lane_session },
|
|
59
|
-
},
|
|
60
|
-
orderBy: { created_at: "desc" },
|
|
61
|
-
});
|
|
62
|
-
if (!task) {
|
|
63
|
-
throw new ORPCError("NOT_FOUND", {
|
|
64
|
-
message: `No task is bound to ACP lane ${input.lane_session}`,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
const state = closedTaskState(task, input.disposition);
|
|
68
|
-
await prisma.work_items.update({
|
|
69
|
-
where: { id: task.id },
|
|
70
|
-
data: {
|
|
71
|
-
kind_data: state.kind_data,
|
|
72
|
-
status: state.task_status,
|
|
73
|
-
completed: state.completed,
|
|
74
|
-
},
|
|
75
|
-
select: { id: true },
|
|
76
|
-
});
|
|
77
|
-
return {
|
|
78
|
-
task_id: state.task_id,
|
|
79
|
-
task_status: state.task_status,
|
|
80
|
-
reconciled: true,
|
|
81
|
-
};
|
|
82
|
-
});
|