@tokenoftrust/storefront-runner 1.4.0 โ 1.4.2-rc.0
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/apps/storefront/astro.config.mjs +17 -1
- package/apps/storefront/integrations/materialize-guard.mjs +56 -0
- package/apps/storefront/migrations-apps/0001_woozy_lyja.sql +22 -0
- package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
- package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
- package/apps/storefront/migrations-apps/meta/0001_snapshot.json +990 -0
- package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
- package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +21 -0
- package/apps/storefront/package.json +0 -1
- package/apps/storefront/public/js/dashboard-team.js +38 -2
- package/apps/storefront/src/components/Seo.astro +65 -1
- package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +1432 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1756 -1382
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
- package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
- package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
- package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
- package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
- package/apps/storefront/src/layouts/Layout.astro +48 -3
- package/apps/storefront/src/lib/activity/alerts.ts +426 -0
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
- package/apps/storefront/src/lib/activity/deployVersion.ts +120 -0
- package/apps/storefront/src/lib/activity/ingest.ts +188 -0
- package/apps/storefront/src/lib/activity/ingestAuth.ts +105 -0
- package/apps/storefront/src/lib/activity/killSwitch.ts +80 -0
- package/apps/storefront/src/lib/activity/query.ts +403 -0
- package/apps/storefront/src/lib/activity/recordActivity.ts +101 -0
- package/apps/storefront/src/lib/activity/store.ts +120 -0
- package/apps/storefront/src/lib/activity/uiActor.ts +150 -0
- package/apps/storefront/src/lib/activity/workerCommit.ts +70 -0
- package/apps/storefront/src/lib/admin/adminShell.ts +290 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
- package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
- package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
- package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
- package/apps/storefront/src/lib/apps/adminService.ts +4 -4
- package/apps/storefront/src/lib/apps/adminSession.ts +53 -44
- package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
- package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
- package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
- package/apps/storefront/src/lib/apps/credentials.ts +2 -2
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
- package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +6 -6
- package/apps/storefront/src/lib/apps/registryService.ts +6 -6
- package/apps/storefront/src/lib/apps/scopes.ts +2 -2
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
- package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
- package/apps/storefront/src/lib/auth/adminEntry.ts +9 -9
- package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
- package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +6 -4
- package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
- package/apps/storefront/src/lib/auth/route.ts +44 -4
- package/apps/storefront/src/lib/auth/session.ts +7 -8
- package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
- package/apps/storefront/src/lib/basePath.ts +9 -2
- package/apps/storefront/src/lib/blog/access.ts +29 -0
- package/apps/storefront/src/lib/blog/collection.ts +313 -0
- package/apps/storefront/src/lib/blog/markdown.ts +174 -0
- package/apps/storefront/src/lib/blog/provider.ts +5 -0
- package/apps/storefront/src/lib/blog/reactions.ts +87 -0
- package/apps/storefront/src/lib/blog/rss.ts +92 -0
- package/apps/storefront/src/lib/blog/teaser.ts +83 -0
- package/apps/storefront/src/lib/blog/types.ts +34 -0
- package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
- package/apps/storefront/src/lib/d1/catalog.ts +2 -2
- package/apps/storefront/src/lib/d1/schema-apps.ts +147 -14
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
- package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
- package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
- package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
- package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
- package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
- package/apps/storefront/src/lib/edgeCache.ts +17 -0
- package/apps/storefront/src/lib/env.ts +51 -0
- package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
- package/apps/storefront/src/lib/i18n.ts +25 -0
- package/apps/storefront/src/lib/jsonld.ts +118 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
- package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
- package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
- package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
- package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
- package/apps/storefront/src/lib/newsletter/send.ts +265 -0
- package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +3519 -0
- package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
- package/apps/storefront/src/lib/publish/domainState.ts +22 -13
- package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +100 -42
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
- package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
- package/apps/storefront/src/lib/search/index.ts +2 -2
- package/apps/storefront/src/lib/seo/alternates.ts +42 -0
- package/apps/storefront/src/lib/seo/meta.ts +65 -0
- package/apps/storefront/src/lib/social/golive.ts +212 -0
- package/apps/storefront/src/lib/social/notify.ts +32 -0
- package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
- package/apps/storefront/src/lib/the-build/provider.ts +27 -0
- package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
- package/apps/storefront/src/lib/the-build/toc.ts +95 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
- package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
- package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
- package/apps/storefront/src/middleware/index.ts +130 -2
- package/apps/storefront/src/pages/admin/ops-timeline.astro +420 -0
- package/apps/storefront/src/pages/admin.astro +52 -1206
- package/apps/storefront/src/pages/api/activity.ts +137 -0
- package/apps/storefront/src/pages/api/admin/activity-alerts.ts +73 -0
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +27 -1
- package/apps/storefront/src/pages/api/apps/admin/install.ts +26 -1
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +25 -1
- package/apps/storefront/src/pages/api/apps/admin/update.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +11 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +11 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +20 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +12 -0
- package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
- package/apps/storefront/src/pages/api/cache-purge.ts +11 -0
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -0
- package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
- package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
- package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
- package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +21 -2
- package/apps/storefront/src/pages/auth/login.astro +5 -4
- package/apps/storefront/src/pages/blog/[slug].astro +96 -16
- package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
- package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
- package/apps/storefront/src/pages/blog/index.astro +2 -0
- package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
- package/apps/storefront/src/pages/cockpit.astro +74 -12
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
- package/apps/storefront/src/pages/search.astro +1 -1
- package/apps/storefront/src/pages/sitemap.xml.ts +20 -0
- package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
- package/apps/storefront/src/pages/the-build/index.astro +146 -0
- package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
- package/apps/storefront/src/styles/admin.css +1080 -0
- package/apps/storefront/tsconfig.json +5 -0
- package/package.json +1 -1
- package/packages/public-runtime/package.json +1 -0
- package/packages/public-runtime/src/activity/README.md +146 -0
- package/packages/public-runtime/src/activity/catalog.ts +553 -0
- package/packages/public-runtime/src/activity/event.ts +168 -0
- package/packages/public-runtime/src/activity/index.ts +16 -0
- package/packages/public-runtime/src/activity/redaction.ts +263 -0
- package/packages/public-runtime/src/candidate-index.ts +13 -0
- package/packages/public-runtime/src/catalog-d1.ts +1 -1
- package/packages/public-runtime/src/customization-preview.ts +25 -2
- package/packages/public-runtime/src/customization-runtime.ts +125 -3
- package/packages/public-runtime/src/customization-versioning.ts +10 -0
- package/packages/public-runtime/src/index.ts +7 -0
- package/packages/public-runtime/src/static-bundle.ts +357 -0
- package/packages/public-runtime/src/tenant.ts +36 -8
- package/scripts/build/copy-tenant-assets.mjs +14 -4
- package/scripts/dev/ai-edit.mjs +1 -1
- package/scripts/dev/checkout-watch.mjs +2 -2
- package/scripts/dev/file-browser.mjs +2 -4
- package/scripts/dev/file-writer.mjs +1 -1
- package/scripts/dev/git-status.mjs +2 -2
- package/scripts/dev/locate-handler.mjs +3 -3
- package/scripts/dev/port-check.mjs +3 -3
- package/scripts/dev/publish.mjs +5 -9
- package/scripts/dev/shot.mjs +1 -1
- package/scripts/dev/unified-diff.mjs +8 -3
- package/scripts/tot-dev.mjs +6 -9
- package/apps/storefront/src/lib/webhooks/signing.ts +0 -29
- package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +0 -146
|
@@ -11,7 +11,7 @@ import { getTenantResolver } from "@/config/resolver";
|
|
|
11
11
|
import { referenceTheme } from "@/themes/reference";
|
|
12
12
|
import { mergeTheme, themeToCssVars } from "@/themes/schema";
|
|
13
13
|
import { createToTClient } from "@/lib/tot/ToTClient";
|
|
14
|
-
import { readEnv, readKv, readD1 } from "@/lib/env";
|
|
14
|
+
import { readEnv, readKv, readD1, readR2 } from "@/lib/env";
|
|
15
15
|
import { resolveTenantRouting } from "@/lib/tenantRouting";
|
|
16
16
|
import { createContentProvider, DevDraftContentProvider } from "@/lib/storyblok/provider";
|
|
17
17
|
import { DEV_DRAFT_COOKIE, devDraftKey, verifyDevDraftToken } from "@/lib/dev/devDraft";
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
readCacheVersion,
|
|
22
22
|
getCachedPage,
|
|
23
23
|
putCachedPage,
|
|
24
|
+
staticBundleCacheKey,
|
|
24
25
|
} from "@/lib/edgeCache";
|
|
25
26
|
import { STATIC_SCRIPT_HASHES, STATIC_STYLE_HASHES } from "@/lib/cspStaticHashes";
|
|
26
27
|
import { applySecurityHeaders } from "@/lib/securityHeaders";
|
|
@@ -38,8 +39,12 @@ import {
|
|
|
38
39
|
resolveTenantFeatures,
|
|
39
40
|
resolveCapabilities,
|
|
40
41
|
capabilitiesArtifactPath,
|
|
42
|
+
resolveChannelDigest,
|
|
43
|
+
resolveServableChannelVersion,
|
|
44
|
+
channelFromEnv,
|
|
45
|
+
serveStaticBundle,
|
|
41
46
|
} from "@tot/public-runtime";
|
|
42
|
-
import type { CapabilityConfig } from "@tot/public-runtime";
|
|
47
|
+
import type { CapabilityConfig, StaticBundleR2BucketLike } from "@tot/public-runtime";
|
|
43
48
|
import { resolveTenantCommerce } from "@/lib/checkoutCommerce";
|
|
44
49
|
import { evaluateDemoGate, hostInGateList } from "@/lib/demoGate";
|
|
45
50
|
import {
|
|
@@ -54,6 +59,9 @@ import { gatedHoldingResponse } from "@/lib/auth/gatePage";
|
|
|
54
59
|
import { readSession, KvSessionStore, SESSION_COOKIE } from "@/lib/auth/session";
|
|
55
60
|
import { isMaintenanceOn, maintenanceResponse } from "@/lib/maintenance";
|
|
56
61
|
import { HEALTH_PATH, healthResponse } from "@/lib/health";
|
|
62
|
+
import { observeDeployVersion } from "@/lib/activity/deployVersion";
|
|
63
|
+
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
64
|
+
import { primeActivityKillSwitch } from "@/lib/activity/killSwitch";
|
|
57
65
|
import type { DeepPartial, ThemeTokens } from "@tot/public-runtime";
|
|
58
66
|
|
|
59
67
|
export const onRequest = defineMiddleware(async (context, next) => {
|
|
@@ -62,6 +70,21 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
62
70
|
const host =
|
|
63
71
|
request.headers.get("host") ?? url.host ?? "localhost";
|
|
64
72
|
|
|
73
|
+
// ---- Activity telemetry kill switch (card D8) -----------------------------
|
|
74
|
+
// Prime the ONE `ACTIVITY_TELEMETRY_DISABLED` flag from the (async) Worker env
|
|
75
|
+
// once per isolate, BEFORE the first emit, so every telemetry chokepoint in this
|
|
76
|
+
// isolate (deploy-provenance, recordActivity, ingest) observes it. Fail-open,
|
|
77
|
+
// never throws. Off by default (unset โ telemetry on).
|
|
78
|
+
await primeActivityKillSwitch();
|
|
79
|
+
|
|
80
|
+
// ---- Deploy provenance (card D2) โ worker_commit / deploy.version_observed -
|
|
81
|
+
// Once per worker isolate (cold start), emit `deploy.version_observed` carrying
|
|
82
|
+
// this build's commit SHA so D5 can derive deploy skew (running build vs umbrella
|
|
83
|
+
// tip). Idempotent per isolate, best-effort, never throws โ so it's safe here at
|
|
84
|
+
// the very front door, ahead of /health, and fires regardless of Host/tenant/gate.
|
|
85
|
+
// A no-op when the D8 kill switch above is set.
|
|
86
|
+
observeDeployVersion();
|
|
87
|
+
|
|
65
88
|
// ---- /health โ liveness + deploy-identity probe ---------------------------
|
|
66
89
|
// Answered FIRST: before tenant resolution, the access gate, the maintenance
|
|
67
90
|
// kill-switch, and the edge cache โ so it reports the running build's commit SHA
|
|
@@ -147,6 +170,14 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
147
170
|
}
|
|
148
171
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
149
172
|
const admitted = sessionAdmitsHost(viewerRecord, tenant.appDomain, nowSeconds);
|
|
173
|
+
// QA content-bypass: only when the server has configured DEMO_GATE_BYPASS_TOKEN
|
|
174
|
+
// (never in a prod slot) AND the request presents a matching X-Demo-Gate-Bypass
|
|
175
|
+
// header. Unset token โ false โ fully inert; a client cannot self-grant. The
|
|
176
|
+
// public-content-only restriction (admin/cockpit/api excluded) is enforced
|
|
177
|
+
// inside evaluateLoginGate via isBypassPrivilegedPath.
|
|
178
|
+
const bypassToken = await readEnv("DEMO_GATE_BYPASS_TOKEN");
|
|
179
|
+
const qaBypass =
|
|
180
|
+
!!bypassToken && request.headers.get("x-demo-gate-bypass") === bypassToken;
|
|
150
181
|
const decision = evaluateLoginGate({
|
|
151
182
|
host,
|
|
152
183
|
pathname: url.pathname,
|
|
@@ -155,6 +186,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
155
186
|
gatedHosts: demoGateHosts,
|
|
156
187
|
hasValidSession: admitted,
|
|
157
188
|
tenantExempt,
|
|
189
|
+
qaBypass,
|
|
158
190
|
});
|
|
159
191
|
if (decision.action === "redirect") {
|
|
160
192
|
// Withhold content but offer sign-in (carrying the intended destination).
|
|
@@ -188,6 +220,79 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
188
220
|
// signature, expiry, tenant binding โ before the overlay is applied further down.
|
|
189
221
|
const draftCookie = context.cookies.get(DEV_DRAFT_COOKIE)?.value ?? null;
|
|
190
222
|
|
|
223
|
+
// ---- Digest-addressed static-bundle serving (ADR 0012) --------------------
|
|
224
|
+
// For a static-publish tenant's PRODUCTION reads only (never preview/test โ the
|
|
225
|
+
// "Worker serves interactive preview, R2 serves prod" split), read the live
|
|
226
|
+
// bundle straight out of R2 instead of falling through to SSR. Deliberately
|
|
227
|
+
// BEFORE the edge page cache / catalog / theme / customization machinery below:
|
|
228
|
+
// none of that applies to bundle bytes served verbatim, and this path must
|
|
229
|
+
// never SSR-fallthrough once it's eligible (a missing digest/object fails
|
|
230
|
+
// closed, see `serveStaticBundle`), so it has to fully own the response, not
|
|
231
|
+
// degrade into the normal render.
|
|
232
|
+
// Host-resolved reads only (`basePath === ""`): bundle bytes are served
|
|
233
|
+
// VERBATIM with root-absolute links, so a path-prefixed view
|
|
234
|
+
// (/<domain>/โฆ on the shared .store host) would escape its prefix on every
|
|
235
|
+
// href โ those views keep SSR, which rewrites hrefs through basePath.
|
|
236
|
+
const staticBundleEligible =
|
|
237
|
+
tenant.staticPublish != null &&
|
|
238
|
+
tenant.staticPublish.enabled !== false &&
|
|
239
|
+
basePath === "" &&
|
|
240
|
+
request.method.toUpperCase() === "GET" &&
|
|
241
|
+
!isPreviewHost &&
|
|
242
|
+
!queryToken &&
|
|
243
|
+
!cookieToken &&
|
|
244
|
+
!draftCookie &&
|
|
245
|
+
!url.pathname.startsWith("/api/");
|
|
246
|
+
if (staticBundleEligible) {
|
|
247
|
+
const bundleBucket = await readR2("STATIC_BUNDLES");
|
|
248
|
+
if (bundleBucket) {
|
|
249
|
+
const kvGet = kv ? (key: string) => kv.get(key) : async () => null;
|
|
250
|
+
// The R2 bundle key is the `treeDigest`, NOT the `live` channel versionId
|
|
251
|
+
// (the Gitea sha). Read the reconciled digest companion (`channelDigests.live`)
|
|
252
|
+
// โ `gatedPromote`/`rollbackCandidateAtomic` write it in the same CAS flip as
|
|
253
|
+
// the versionId. No digest = this tenant has never staged a bundle โ SSR
|
|
254
|
+
// serves it (a static-publish tenant must be able to bootstrap, and its
|
|
255
|
+
// app surfaces must never be downed by an unstaged static plane).
|
|
256
|
+
const digest = await resolveChannelDigest(kvGet, tenant.tenant_id, "live");
|
|
257
|
+
if (digest) {
|
|
258
|
+
const bundleCacheKey = cache
|
|
259
|
+
? staticBundleCacheKey(tenant.tenant_id, digest, url.pathname, url.search)
|
|
260
|
+
: null;
|
|
261
|
+
if (bundleCacheKey && cache) {
|
|
262
|
+
const hit = await getCachedPage(cache, bundleCacheKey);
|
|
263
|
+
if (hit) return hit;
|
|
264
|
+
}
|
|
265
|
+
const bundleResponse = await serveStaticBundle(
|
|
266
|
+
{
|
|
267
|
+
bucket: bundleBucket as unknown as StaticBundleR2BucketLike,
|
|
268
|
+
digest,
|
|
269
|
+
onFailClosed: (detail) => {
|
|
270
|
+
console.error(
|
|
271
|
+
`[static-bundle] fail-closed for ${tenant.tenant_id} ${url.pathname}: ${detail}`,
|
|
272
|
+
);
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
url.pathname,
|
|
276
|
+
);
|
|
277
|
+
// `null` = the manifest proves this path is outside the bundle's static
|
|
278
|
+
// surface (an app surface like /admin) โ SSR owns it below.
|
|
279
|
+
if (bundleResponse) {
|
|
280
|
+
if (bundleCacheKey && cache) {
|
|
281
|
+
await putCachedPage(cache, bundleCacheKey, bundleResponse);
|
|
282
|
+
}
|
|
283
|
+
return bundleResponse;
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
console.error(
|
|
287
|
+
`[static-bundle] no live digest for ${tenant.tenant_id} โ SSR until a bundle is staged`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// No STATIC_BUNDLES binding (plain `astro dev`, or unconfigured) โ
|
|
292
|
+
// degrading to SSR here rather than a hard failure keeps `astro dev`/tests
|
|
293
|
+
// usable without provisioning R2.
|
|
294
|
+
}
|
|
295
|
+
|
|
191
296
|
// ---- Edge page cache (D1): short-circuit production browse reads on a hit,
|
|
192
297
|
// BEFORE building the client / theme / rendering. Misses store the final
|
|
193
298
|
// response at the end of this middleware.
|
|
@@ -245,6 +350,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
245
350
|
token: previewToken,
|
|
246
351
|
secret: previewSecret,
|
|
247
352
|
nowSeconds,
|
|
353
|
+
pathname: url.pathname,
|
|
248
354
|
});
|
|
249
355
|
if (customization.reject) {
|
|
250
356
|
return new Response(customization.reject.message, {
|
|
@@ -273,6 +379,28 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
273
379
|
// pointer fall back to the legacy per-path selection โ no behavior change.
|
|
274
380
|
const kvGet = kv ? (key: string) => kv.get(key) : async () => null;
|
|
275
381
|
|
|
382
|
+
// Self-heal a DANGLING channel pointer: the pointer for this env's channel may
|
|
383
|
+
// name a version that was never built (or whose manifest was evicted).
|
|
384
|
+
// `readVersionedArtifact` already serves last-built content instead of emptying
|
|
385
|
+
// the surface; here we do the once-per-request detection to ENQUEUE a rebuild of
|
|
386
|
+
// the dangling version. Fail-open: a KV hiccup never blocks the render (the reads
|
|
387
|
+
// below still run).
|
|
388
|
+
if (kv) {
|
|
389
|
+
const channel = channelFromEnv(customization.env);
|
|
390
|
+
await resolveServableChannelVersion(kvGet, tenant.tenant_id, channel, ({ versionId }) => {
|
|
391
|
+
recordActivity({
|
|
392
|
+
action: "preview.build.requested",
|
|
393
|
+
actor: { kind: "system", id: "storefront" },
|
|
394
|
+
source: "server",
|
|
395
|
+
outcome: { status: "succeeded" },
|
|
396
|
+
scope: { tenantId: tenant.tenant_id },
|
|
397
|
+
payload: { args: { changeId: versionId } },
|
|
398
|
+
});
|
|
399
|
+
}).catch(() => {
|
|
400
|
+
/* fail-open: self-heal detection never breaks the request */
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
276
404
|
// Verify the developer-draft cookie (signature + expiry + tenant binding) โ the
|
|
277
405
|
// overlay below only engages for the developer who minted the draft, on THIS
|
|
278
406
|
// tenant. Same secret + trust model as the preview token; invalid/expired/foreign
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* `/admin/ops-timeline` โ the operational-activity timeline VIEW (card D5).
|
|
4
|
+
*
|
|
5
|
+
* The READ surface of the actorรaction telemetry spine: a chronological,
|
|
6
|
+
* tenant-scoped list of lifecycle activity answering "who shipped/rejected what,
|
|
7
|
+
* when, why โ and what happened to my change/deploy". It reads the D1
|
|
8
|
+
* `activity_events` timeline cohort (DECISIONS.md ยง8) through the pure,
|
|
9
|
+
* tenant-scoped query module (`lib/activity/query.ts`).
|
|
10
|
+
*
|
|
11
|
+
* ROUTING / AUTH โ mirrors `admin.astro`: middleware resolves the tenant, injects
|
|
12
|
+
* CSP/security headers, and already held anonymous/non-member visitors at the
|
|
13
|
+
* sign-in gate. This surface exposes operational detail, so it renders the data
|
|
14
|
+
* ONLY for a viewer with owner/admin standing for the routed tenant, or a ToT-staff
|
|
15
|
+
* session (`resolveAdminEntry` / `resolveAdminTenantMode`, the same machinery the
|
|
16
|
+
* admin shell uses). A signed-in non-owner sees the chrome + a "no standing" notice,
|
|
17
|
+
* never another tenant's rows.
|
|
18
|
+
*
|
|
19
|
+
* TENANT ISOLATION โ the query is ALWAYS scoped to `Astro.locals.tenant.tenant_id`
|
|
20
|
+
* (the served tenant, never a URL value); `parseTimelineQuery` pins that tenant and
|
|
21
|
+
* ignores any `tenant`/`tenantId` query param. See `query.isolation.test.ts` for the
|
|
22
|
+
* hostile read-side proof.
|
|
23
|
+
*
|
|
24
|
+
* DEPLOY SKEW (card D2) โ the page surfaces `deriveDeploySkew()`: a banner when the
|
|
25
|
+
* build serving this page (or the timeline) shows more than one live version, plus a
|
|
26
|
+
* per-row "stale build" chip on superseded `deploy.version_observed` rows.
|
|
27
|
+
*
|
|
28
|
+
* ALERTS (card D7) โ the page also runs `checkActivityAlerts()` (silent-terminal,
|
|
29
|
+
* recurring preview.serve miss_no_env, reconcile-hang watchdog, and โ promoting the
|
|
30
|
+
* skew signal above โ a persistent deploy-skew alert) and renders an alerts banner
|
|
31
|
+
* above the deploy-skew visibility banner. Same on-demand posture: computed fresh on
|
|
32
|
+
* every page load, no cron. `GET /api/admin/activity-alerts` exposes the identical
|
|
33
|
+
* check for an external poller.
|
|
34
|
+
*/
|
|
35
|
+
export const prerender = false;
|
|
36
|
+
|
|
37
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
38
|
+
import { resolveAdminEntry } from "@/lib/auth/adminEntry";
|
|
39
|
+
import { resolveAdminTenantMode } from "@/lib/adminTenantEnvelope";
|
|
40
|
+
import { readD1 } from "@/lib/env";
|
|
41
|
+
import { fromD1 } from "@/lib/d1/catalog";
|
|
42
|
+
import {
|
|
43
|
+
parseTimelineQuery,
|
|
44
|
+
loadOpsTimeline,
|
|
45
|
+
type OpsTimelineData,
|
|
46
|
+
type ActivityTimelineRow,
|
|
47
|
+
} from "@/lib/activity/query";
|
|
48
|
+
import { checkActivityAlerts, type ActivityAlert } from "@/lib/activity/alerts";
|
|
49
|
+
|
|
50
|
+
const { tenant, basePath, cspNonce } = Astro.locals;
|
|
51
|
+
const adminPath = `${basePath || ""}/admin`;
|
|
52
|
+
const timelinePath = `${adminPath}/ops-timeline`;
|
|
53
|
+
const tenantLabel = tenant.displayName || tenant.appDomain || tenant.tenant_id;
|
|
54
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
55
|
+
|
|
56
|
+
const viewerSession = await readViewerSession(Astro);
|
|
57
|
+
const viewerEmail = (Astro.locals.viewer?.email ?? viewerSession?.email ?? "").toLowerCase();
|
|
58
|
+
|
|
59
|
+
const adminEntry = resolveAdminEntry({
|
|
60
|
+
record: viewerSession,
|
|
61
|
+
hostResource: tenant.appDomain,
|
|
62
|
+
nowSeconds,
|
|
63
|
+
});
|
|
64
|
+
const { isStaff } = resolveAdminTenantMode({
|
|
65
|
+
session: viewerSession,
|
|
66
|
+
adminAppTenant: tenant.appDomain,
|
|
67
|
+
requestedTarget: null,
|
|
68
|
+
nowSeconds,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Gate: a signed-in owner/admin of THIS tenant, or ToT staff. Everyone else gets
|
|
72
|
+
// chrome + a notice, and the query never runs.
|
|
73
|
+
const authorized = Boolean(viewerSession) && (adminEntry.isOwnerViewer || isStaff);
|
|
74
|
+
|
|
75
|
+
// The query is scoped to the SERVED tenant's id โ never a URL value.
|
|
76
|
+
const query = parseTimelineQuery(Astro.url.searchParams, tenant.tenant_id);
|
|
77
|
+
|
|
78
|
+
let data: OpsTimelineData | null = null;
|
|
79
|
+
let alerts: ActivityAlert[] = [];
|
|
80
|
+
let dbAvailable = true;
|
|
81
|
+
if (authorized) {
|
|
82
|
+
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
83
|
+
if (d1) {
|
|
84
|
+
const db = fromD1(d1);
|
|
85
|
+
[data, alerts] = await Promise.all([
|
|
86
|
+
loadOpsTimeline(db, query),
|
|
87
|
+
checkActivityAlerts(db, tenant.tenant_id),
|
|
88
|
+
]);
|
|
89
|
+
} else {
|
|
90
|
+
// Dev/Node or an env without the binding โ the timeline lives only in D1.
|
|
91
|
+
dbAvailable = false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const rows = data?.rows ?? [];
|
|
96
|
+
const skew = data?.deploySkew ?? null;
|
|
97
|
+
// Versions superseded by a newer observation โ a "stale build" chip on those rows.
|
|
98
|
+
const staleVersions = new Set(
|
|
99
|
+
(skew?.observedVersions ?? [])
|
|
100
|
+
.filter((v) => v.version !== skew?.latestObservedVersion)
|
|
101
|
+
.map((v) => v.version),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
/** Short, non-PII actor label (the id is already an opaque pseudonym). */
|
|
105
|
+
function shortActor(row: ActivityTimelineRow): string {
|
|
106
|
+
const id = row.actorId.length > 14 ? `${row.actorId.slice(0, 14)}โฆ` : row.actorId;
|
|
107
|
+
return `${row.actorKind}:${id}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Map a status to a chip color class. */
|
|
111
|
+
function statusClass(status: string): string {
|
|
112
|
+
switch (status) {
|
|
113
|
+
case "succeeded":
|
|
114
|
+
return "green";
|
|
115
|
+
case "failed":
|
|
116
|
+
return "red";
|
|
117
|
+
case "refused":
|
|
118
|
+
return "amber";
|
|
119
|
+
default:
|
|
120
|
+
return "blue"; // invoked
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Compact, human detail line from the (already redacted) payload. */
|
|
125
|
+
function detailFor(row: ActivityTimelineRow): string {
|
|
126
|
+
const parts: string[] = [];
|
|
127
|
+
const rendered = row.payload.rendered ?? {};
|
|
128
|
+
for (const [k, v] of Object.entries(rendered)) parts.push(`${k}: ${v}`);
|
|
129
|
+
const args = row.payload.args ?? {};
|
|
130
|
+
for (const [k, v] of Object.entries(args)) {
|
|
131
|
+
if (k === "changeId") continue; // shown in its own column
|
|
132
|
+
parts.push(`${k}=${String(v)}`);
|
|
133
|
+
}
|
|
134
|
+
return parts.join(" ยท ");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The version an event refers to (deploy.version_observed rows carry it). */
|
|
138
|
+
function versionOf(row: ActivityTimelineRow): string | undefined {
|
|
139
|
+
const v = row.payload.args?.version;
|
|
140
|
+
return typeof v === "string" ? v : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const statusOptions = ["", "invoked", "succeeded", "failed", "refused"];
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
<!doctype html>
|
|
147
|
+
<html lang="en">
|
|
148
|
+
<head>
|
|
149
|
+
<meta charset="utf-8" />
|
|
150
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
151
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
152
|
+
<title>{tenantLabel} ยท Ops Timeline</title>
|
|
153
|
+
<style is:global nonce={cspNonce}>
|
|
154
|
+
:root {
|
|
155
|
+
color-scheme: light;
|
|
156
|
+
--bg: #f5f7f6;
|
|
157
|
+
--surface: #ffffff;
|
|
158
|
+
--surface-soft: #f9fbfa;
|
|
159
|
+
--ink: #16211f;
|
|
160
|
+
--muted: #64716d;
|
|
161
|
+
--line: #dce5e2;
|
|
162
|
+
--teal: #007f79;
|
|
163
|
+
--green-soft: #e3f5eb;
|
|
164
|
+
--green: #137a55;
|
|
165
|
+
--red-soft: #fde7e4;
|
|
166
|
+
--red: #b42318;
|
|
167
|
+
--amber-soft: #fff0d5;
|
|
168
|
+
--amber: #a45d00;
|
|
169
|
+
--blue-soft: #e7efff;
|
|
170
|
+
--blue: #2458c7;
|
|
171
|
+
--radius: 8px;
|
|
172
|
+
--font: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
173
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
174
|
+
}
|
|
175
|
+
* { box-sizing: border-box; }
|
|
176
|
+
body { margin: 0; background: var(--bg); color: var(--ink); font: 14px/1.5 var(--font); }
|
|
177
|
+
a { color: var(--teal); }
|
|
178
|
+
.wrap { max-width: 1180px; margin: 0 auto; padding: 24px 20px 64px; }
|
|
179
|
+
header.page { margin-bottom: 18px; }
|
|
180
|
+
header.page h1 { margin: 0; font-size: 22px; font-weight: 760; }
|
|
181
|
+
header.page p { margin: 4px 0 0; color: var(--muted); font-size: 13px; }
|
|
182
|
+
header.page code { font-family: var(--mono); }
|
|
183
|
+
.notice {
|
|
184
|
+
padding: 14px 16px; border: 1px solid var(--amber); border-radius: var(--radius);
|
|
185
|
+
background: var(--amber-soft); color: #5c3600; margin-bottom: 18px; font-size: 13px;
|
|
186
|
+
}
|
|
187
|
+
.skew {
|
|
188
|
+
display: flex; align-items: flex-start; gap: 12px;
|
|
189
|
+
padding: 12px 14px; border: 1px solid #f1c883; border-radius: var(--radius);
|
|
190
|
+
background: #fff6e7; margin-bottom: 18px; font-size: 13px;
|
|
191
|
+
}
|
|
192
|
+
.skew strong { display: block; }
|
|
193
|
+
.skew .versions { color: var(--muted); font-size: 12px; margin-top: 3px; }
|
|
194
|
+
.skew code { font-family: var(--mono); }
|
|
195
|
+
.alerts { display: grid; gap: 8px; margin-bottom: 18px; }
|
|
196
|
+
.alert {
|
|
197
|
+
display: flex; align-items: flex-start; gap: 12px;
|
|
198
|
+
padding: 12px 14px; border-radius: var(--radius); font-size: 13px;
|
|
199
|
+
}
|
|
200
|
+
.alert strong { display: block; }
|
|
201
|
+
.alert .meta { color: inherit; opacity: 0.75; font-size: 12px; margin-top: 3px; }
|
|
202
|
+
.alert.page, .alert.critical {
|
|
203
|
+
border: 1px solid var(--red); background: var(--red-soft); color: var(--red);
|
|
204
|
+
}
|
|
205
|
+
.alert.warning {
|
|
206
|
+
border: 1px solid #f1c883; background: #fff6e7; color: #7a4a00;
|
|
207
|
+
}
|
|
208
|
+
.alert.info {
|
|
209
|
+
border: 1px solid var(--line); background: var(--surface-soft); color: var(--muted);
|
|
210
|
+
}
|
|
211
|
+
form.filters {
|
|
212
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
213
|
+
gap: 10px; align-items: end;
|
|
214
|
+
padding: 14px; border: 1px solid var(--line); border-radius: var(--radius);
|
|
215
|
+
background: var(--surface); margin-bottom: 18px;
|
|
216
|
+
}
|
|
217
|
+
form.filters .field { display: grid; gap: 4px; min-width: 0; }
|
|
218
|
+
form.filters label { color: var(--muted); font-size: 11px; font-weight: 760; text-transform: uppercase; }
|
|
219
|
+
form.filters input, form.filters select {
|
|
220
|
+
min-height: 36px; padding: 0 9px; font: inherit;
|
|
221
|
+
border: 1px solid var(--line); border-radius: 6px; background: #fcfdfc; color: var(--ink);
|
|
222
|
+
}
|
|
223
|
+
form.filters .actions { display: flex; gap: 8px; }
|
|
224
|
+
.btn {
|
|
225
|
+
min-height: 36px; display: inline-flex; align-items: center; padding: 0 14px;
|
|
226
|
+
border: 1px solid var(--teal); border-radius: 6px; background: var(--teal); color: #fff;
|
|
227
|
+
font-weight: 680; text-decoration: none; cursor: pointer;
|
|
228
|
+
}
|
|
229
|
+
.btn.ghost { background: var(--surface); color: var(--ink); border-color: var(--line); }
|
|
230
|
+
.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); }
|
|
231
|
+
table { width: 100%; min-width: 920px; border-collapse: collapse; font-size: 13px; }
|
|
232
|
+
th {
|
|
233
|
+
height: 38px; padding: 0 12px; border-bottom: 1px solid var(--line);
|
|
234
|
+
background: var(--surface-soft); color: var(--muted); font-size: 11px;
|
|
235
|
+
font-weight: 760; text-align: left; text-transform: uppercase; white-space: nowrap;
|
|
236
|
+
}
|
|
237
|
+
td { padding: 9px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
238
|
+
tr:last-child td { border-bottom: 0; }
|
|
239
|
+
td.at { white-space: nowrap; font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
|
240
|
+
td.action code, td.change code, td.actor code { font-family: var(--mono); font-size: 12px; }
|
|
241
|
+
td.action small { display: block; color: var(--muted); font-size: 11px; margin-top: 2px; }
|
|
242
|
+
td.detail { color: var(--muted); overflow-wrap: anywhere; }
|
|
243
|
+
.chip {
|
|
244
|
+
display: inline-flex; align-items: center; min-height: 20px; padding: 0 8px;
|
|
245
|
+
border-radius: 999px; background: var(--surface-soft); color: #3f4d49;
|
|
246
|
+
font-size: 11px; font-weight: 740; white-space: nowrap;
|
|
247
|
+
}
|
|
248
|
+
.chip.green { background: var(--green-soft); color: var(--green); }
|
|
249
|
+
.chip.red { background: var(--red-soft); color: var(--red); }
|
|
250
|
+
.chip.amber { background: var(--amber-soft); color: var(--amber); }
|
|
251
|
+
.chip.blue { background: var(--blue-soft); color: var(--blue); }
|
|
252
|
+
.chip.stale { background: #ffe0d6; color: #a5330a; margin-left: 6px; }
|
|
253
|
+
.empty { padding: 40px 16px; text-align: center; color: var(--muted); }
|
|
254
|
+
@media (max-width: 760px) {
|
|
255
|
+
form.filters { grid-template-columns: 1fr 1fr; }
|
|
256
|
+
}
|
|
257
|
+
</style>
|
|
258
|
+
</head>
|
|
259
|
+
<body>
|
|
260
|
+
<div class="wrap">
|
|
261
|
+
<header class="page">
|
|
262
|
+
<h1>Operational activity timeline</h1>
|
|
263
|
+
<p>
|
|
264
|
+
{tenantLabel} ยท <code>{tenant.tenant_id}</code> ยท lifecycle events, newest first ยท
|
|
265
|
+
<a href={adminPath}>โ Admin</a>
|
|
266
|
+
</p>
|
|
267
|
+
</header>
|
|
268
|
+
|
|
269
|
+
{!viewerSession && (
|
|
270
|
+
<div class="notice">
|
|
271
|
+
Sign in as an owner/admin of {tenantLabel} to view the operational activity timeline.
|
|
272
|
+
<a href="/auth/login">Sign in</a>.
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
|
|
276
|
+
{viewerSession && !authorized && (
|
|
277
|
+
<div class="notice">
|
|
278
|
+
{viewerEmail} is signed in but has no owner/admin standing for {tenantLabel}. The ops
|
|
279
|
+
timeline is limited to owners/admins of this store and ToT staff.
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{authorized && !dbAvailable && (
|
|
284
|
+
<div class="notice">
|
|
285
|
+
The activity timeline is stored in D1 (<code>STOREFRONT_APPS_DB</code>), which isn't
|
|
286
|
+
bound in this environment โ no rows to show here.
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
{authorized && alerts.length > 0 && (
|
|
291
|
+
<div class="alerts" role="alert">
|
|
292
|
+
{alerts.map((a) => (
|
|
293
|
+
<div class={`alert ${a.severity}`}>
|
|
294
|
+
<span aria-hidden="true">{a.severity === "page" || a.severity === "critical" ? "๐จ" : "โ ๏ธ"}</span>
|
|
295
|
+
<div>
|
|
296
|
+
<strong>{a.title}</strong>
|
|
297
|
+
<div>{a.summary}</div>
|
|
298
|
+
<div class="meta">
|
|
299
|
+
{a.changeId && <>Change <code>{a.changeId}</code> ยท </>}
|
|
300
|
+
since {a.since}
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
307
|
+
|
|
308
|
+
{authorized && skew?.skewDetected && (
|
|
309
|
+
<div class="skew" role="status">
|
|
310
|
+
<span aria-hidden="true">โ ๏ธ</span>
|
|
311
|
+
<div>
|
|
312
|
+
<strong>Deploy skew detected</strong>
|
|
313
|
+
<div class="versions">
|
|
314
|
+
{skew.runningVersion
|
|
315
|
+
? <>Serving build <code>{skew.runningVersion}</code>. </>
|
|
316
|
+
: <>Serving build unknown. </>}
|
|
317
|
+
Observed builds:{" "}
|
|
318
|
+
{skew.observedVersions.map((v, i) => (
|
|
319
|
+
<>
|
|
320
|
+
{i > 0 ? ", " : ""}
|
|
321
|
+
<code>{v.version}</code>{v.env ? ` (${v.env})` : ""}
|
|
322
|
+
{v.version === skew.latestObservedVersion ? " โ latest" : " โ stale"}
|
|
323
|
+
</>
|
|
324
|
+
))}.
|
|
325
|
+
This is a visibility signal only (alerting is a separate concern).
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
|
|
331
|
+
{authorized && (
|
|
332
|
+
<form class="filters" method="get" action={timelinePath}>
|
|
333
|
+
<div class="field">
|
|
334
|
+
<label for="f-changeId">Change ID</label>
|
|
335
|
+
<input id="f-changeId" type="text" name="changeId" value={query.changeId ?? ""} placeholder="chg-โฆ" />
|
|
336
|
+
</div>
|
|
337
|
+
<div class="field">
|
|
338
|
+
<label for="f-action">Action</label>
|
|
339
|
+
<input id="f-action" type="text" name="action" value={query.action ?? ""} placeholder="change.ship" />
|
|
340
|
+
</div>
|
|
341
|
+
<div class="field">
|
|
342
|
+
<label for="f-actionPrefix">Action family</label>
|
|
343
|
+
<input id="f-actionPrefix" type="text" name="actionPrefix" value={query.actionPrefix ?? ""} placeholder="change." />
|
|
344
|
+
</div>
|
|
345
|
+
<div class="field">
|
|
346
|
+
<label for="f-actorId">Actor ID</label>
|
|
347
|
+
<input id="f-actorId" type="text" name="actorId" value={query.actorId ?? ""} placeholder="h_โฆ" />
|
|
348
|
+
</div>
|
|
349
|
+
<div class="field">
|
|
350
|
+
<label for="f-status">Status</label>
|
|
351
|
+
<select id="f-status" name="status">
|
|
352
|
+
{statusOptions.map((s) => (
|
|
353
|
+
<option value={s} selected={query.status === s || (s === "" && !query.status)}>
|
|
354
|
+
{s === "" ? "any" : s}
|
|
355
|
+
</option>
|
|
356
|
+
))}
|
|
357
|
+
</select>
|
|
358
|
+
</div>
|
|
359
|
+
<div class="field">
|
|
360
|
+
<label for="f-since">Since (ISO)</label>
|
|
361
|
+
<input id="f-since" type="text" name="since" value={query.since ?? ""} placeholder="2026-08-01" />
|
|
362
|
+
</div>
|
|
363
|
+
<div class="field">
|
|
364
|
+
<label for="f-until">Until (ISO)</label>
|
|
365
|
+
<input id="f-until" type="text" name="until" value={query.until ?? ""} placeholder="2026-08-31" />
|
|
366
|
+
</div>
|
|
367
|
+
<div class="field actions">
|
|
368
|
+
<button class="btn" type="submit">Filter</button>
|
|
369
|
+
<a class="btn ghost" href={timelinePath}>Reset</a>
|
|
370
|
+
</div>
|
|
371
|
+
</form>
|
|
372
|
+
)}
|
|
373
|
+
|
|
374
|
+
{authorized && dbAvailable && (
|
|
375
|
+
<div class="table-wrap">
|
|
376
|
+
<table>
|
|
377
|
+
<thead>
|
|
378
|
+
<tr>
|
|
379
|
+
<th>When</th>
|
|
380
|
+
<th>Actor</th>
|
|
381
|
+
<th>Action</th>
|
|
382
|
+
<th>Status</th>
|
|
383
|
+
<th>Change</th>
|
|
384
|
+
<th>Detail</th>
|
|
385
|
+
</tr>
|
|
386
|
+
</thead>
|
|
387
|
+
<tbody>
|
|
388
|
+
{rows.length === 0 && (
|
|
389
|
+
<tr>
|
|
390
|
+
<td class="empty" colspan="6">No activity matches these filters yet.</td>
|
|
391
|
+
</tr>
|
|
392
|
+
)}
|
|
393
|
+
{rows.map((row) => {
|
|
394
|
+
const version = versionOf(row);
|
|
395
|
+
const isStale = version != null && staleVersions.has(version);
|
|
396
|
+
return (
|
|
397
|
+
<tr>
|
|
398
|
+
<td class="at">{row.at}</td>
|
|
399
|
+
<td class="actor"><code>{shortActor(row)}</code></td>
|
|
400
|
+
<td class="action">
|
|
401
|
+
<code>{row.action}</code>
|
|
402
|
+
{row.actionDesc && <small>{row.actionDesc}</small>}
|
|
403
|
+
</td>
|
|
404
|
+
<td>
|
|
405
|
+
<span class={`chip ${statusClass(row.status)}`}>{row.status}</span>
|
|
406
|
+
{row.errorClass && <span class="chip amber">{row.errorClass}</span>}
|
|
407
|
+
{isStale && <span class="chip stale">stale build</span>}
|
|
408
|
+
</td>
|
|
409
|
+
<td class="change">{row.changeId ? <code>{row.changeId}</code> : "โ"}</td>
|
|
410
|
+
<td class="detail">{detailFor(row) || "โ"}</td>
|
|
411
|
+
</tr>
|
|
412
|
+
);
|
|
413
|
+
})}
|
|
414
|
+
</tbody>
|
|
415
|
+
</table>
|
|
416
|
+
</div>
|
|
417
|
+
)}
|
|
418
|
+
</div>
|
|
419
|
+
</body>
|
|
420
|
+
</html>
|