@tokenoftrust/storefront-runner 1.3.4-rc.4 โ 1.4.1
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 +15 -0
- package/apps/storefront/dev-plugins/dev-publish.mjs +55 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +86 -7
- package/apps/storefront/drizzle.config.apps.ts +13 -0
- package/apps/storefront/env.d.ts +10 -0
- package/apps/storefront/migrations/README.md +13 -7
- package/apps/storefront/migrations-apps/0000_fast_millenium_guard.sql +129 -0
- package/apps/storefront/migrations-apps/0001_woozy_lyja.sql +22 -0
- package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
- package/apps/storefront/migrations-apps/meta/0001_snapshot.json +990 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +20 -0
- package/apps/storefront/package.json +12 -1
- package/apps/storefront/perf/README.md +64 -0
- package/apps/storefront/perf/assert-budgets.ts +159 -0
- package/apps/storefront/playwright.config.ts +23 -0
- package/apps/storefront/public/js/dashboard-apps.js +173 -0
- package/apps/storefront/public/shared/commerce-marketing.css +221 -0
- package/apps/storefront/src/components/CollectionCard.astro +1 -0
- package/apps/storefront/src/components/ProductCard.astro +1 -0
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +3562 -0
- package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
- package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
- package/apps/storefront/src/components/content/Callout.astro +75 -0
- package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
- package/apps/storefront/src/components/content/ProseSections.astro +121 -0
- package/apps/storefront/src/components/content/ProseToc.astro +34 -0
- package/apps/storefront/src/components/content/RichText.astro +44 -0
- package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
- package/apps/storefront/src/components/home/Hero.astro +14 -0
- package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
- package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
- package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
- package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
- package/apps/storefront/src/config/storeName.ts +34 -0
- package/apps/storefront/src/layouts/Layout.astro +97 -23
- package/apps/storefront/src/lib/activity/alerts.ts +428 -0
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
- package/apps/storefront/src/lib/activity/deployVersion.ts +127 -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 +105 -0
- package/apps/storefront/src/lib/activity/store.ts +122 -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/analytics/budgets.json +69 -0
- package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
- package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
- package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
- package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
- package/apps/storefront/src/lib/apps/adminService.ts +106 -0
- package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
- package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
- package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
- package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
- package/apps/storefront/src/lib/apps/credentials.ts +153 -0
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
- package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
- package/apps/storefront/src/lib/apps/registryService.ts +579 -0
- package/apps/storefront/src/lib/apps/scopes.ts +79 -0
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
- package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
- package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +219 -0
- package/apps/storefront/src/lib/auth/route.ts +60 -5
- package/apps/storefront/src/lib/auth/session.ts +8 -0
- package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
- package/apps/storefront/src/lib/blog/provider.ts +39 -0
- package/apps/storefront/src/lib/blog/types.ts +26 -0
- package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
- package/apps/storefront/src/lib/chrome/model.ts +11 -0
- package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
- package/apps/storefront/src/lib/content/callout.ts +78 -0
- package/apps/storefront/src/lib/content/index.ts +27 -0
- package/apps/storefront/src/lib/content/needsReview.ts +58 -0
- package/apps/storefront/src/lib/content/prose.ts +186 -0
- package/apps/storefront/src/lib/content/richtext.ts +76 -0
- package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
- package/apps/storefront/src/lib/content-edit/client.ts +70 -14
- package/apps/storefront/src/lib/d1/catalog.ts +12 -0
- package/apps/storefront/src/lib/d1/schema-apps.ts +267 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
- package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
- package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
- package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
- package/apps/storefront/src/lib/dev/vcBinding.ts +80 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
- package/apps/storefront/src/lib/env.ts +63 -0
- package/apps/storefront/src/lib/jsonld.ts +12 -17
- package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
- package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
- package/apps/storefront/src/lib/publish/domainState.ts +351 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +369 -0
- package/apps/storefront/src/lib/rawChrome.ts +34 -3
- package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
- package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
- package/apps/storefront/src/lib/subscription/model.ts +114 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
- package/apps/storefront/src/lib/tot/query.ts +32 -0
- package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
- package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
- package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
- package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
- package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
- package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
- package/apps/storefront/src/lib/webhooks/events.ts +98 -0
- package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
- package/apps/storefront/src/middleware/index.ts +45 -13
- package/apps/storefront/src/pages/404.astro +21 -9
- package/apps/storefront/src/pages/[...slug].astro +36 -2
- package/apps/storefront/src/pages/admin/ops-timeline.astro +420 -0
- package/apps/storefront/src/pages/admin.astro +183 -9
- 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 +86 -0
- package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
- package/apps/storefront/src/pages/api/apps/admin/install.ts +125 -0
- package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +79 -0
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +80 -0
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +98 -0
- package/apps/storefront/src/pages/api/apps/admin/update.ts +126 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +70 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +183 -0
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +162 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
- package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
- package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
- package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
- package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
- package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +74 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
- package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
- package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
- package/apps/storefront/src/pages/api/cache-purge.ts +11 -0
- package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
- package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
- package/apps/storefront/src/pages/auth/login.astro +202 -45
- package/apps/storefront/src/pages/auth/magic.astro +75 -43
- package/apps/storefront/src/pages/blog/[slug].astro +107 -0
- package/apps/storefront/src/pages/blog/index.astro +98 -0
- package/apps/storefront/src/pages/capabilities.astro +8 -0
- package/apps/storefront/src/pages/cockpit.astro +470 -69
- package/apps/storefront/src/pages/collections/[handle].astro +8 -0
- package/apps/storefront/src/pages/collections/index.astro +10 -2
- package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
- package/apps/storefront/src/pages/index.astro +57 -0
- package/apps/storefront/src/pages/llms.txt.ts +31 -10
- package/apps/storefront/src/pages/products/[handle].astro +100 -9
- package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
- package/apps/storefront/src/pages/style-guide/index.astro +10 -0
- package/apps/storefront/src/styles/fonts.css +54 -0
- package/apps/storefront/src/styles/global.css +22 -2
- package/apps/storefront/src/themes/schema.ts +3 -25
- package/apps/storefront/tsconfig.json +1 -1
- package/apps/storefront/vitest.config.ts +4 -1
- package/package.json +1 -1
- package/packages/public-runtime/src/activity/README.md +146 -0
- package/packages/public-runtime/src/activity/catalog.ts +501 -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 +324 -0
- package/packages/public-runtime/src/checkout.ts +94 -2
- package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
- package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
- package/packages/public-runtime/src/compliance/index.ts +22 -0
- package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
- package/packages/public-runtime/src/compliance/profile.ts +198 -0
- package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
- package/packages/public-runtime/src/compliance/verification.ts +81 -0
- package/packages/public-runtime/src/csp.ts +27 -3
- package/packages/public-runtime/src/customization-reconcile.ts +35 -0
- package/packages/public-runtime/src/customization-runtime.ts +8 -0
- package/packages/public-runtime/src/customization-versioning.ts +17 -0
- package/packages/public-runtime/src/extension-contract.ts +2 -1
- package/packages/public-runtime/src/hash.ts +25 -0
- package/packages/public-runtime/src/index.ts +12 -0
- package/packages/public-runtime/src/membership.ts +353 -0
- package/packages/public-runtime/src/product.ts +24 -2
- package/packages/public-runtime/src/review-trust-proof.ts +194 -0
- package/packages/public-runtime/src/tenant-assets.ts +40 -5
- package/packages/public-runtime/src/tenant.ts +236 -0
- package/packages/public-runtime/src/widget-postmessage.ts +205 -0
- package/scripts/dev/publish.mjs +158 -0
- package/scripts/dev/transient-files.mjs +2 -1
- package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
- package/tenants/home/public/pages/storefront.css +23 -0
- package/apps/storefront/src/lib/dev/hostedCockpit.ts +0 -169
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
buildCsp,
|
|
31
31
|
STOREFRONT_CSP_ALLOWLIST,
|
|
32
32
|
checkoutCspAdditions,
|
|
33
|
+
appWidgetCspAdditions,
|
|
33
34
|
CSP_REPORT_GROUP,
|
|
34
35
|
CSP_REPORT_PATH,
|
|
35
36
|
readPublishedArtifact,
|
|
@@ -46,13 +47,15 @@ import {
|
|
|
46
47
|
parseGateMode,
|
|
47
48
|
isAuthPath,
|
|
48
49
|
sessionAdmitsHost,
|
|
50
|
+
sessionHostCapability,
|
|
49
51
|
PUBLIC_HOME_TENANT_ID,
|
|
50
52
|
} from "@/lib/auth/loginGate";
|
|
51
53
|
import { gatedHoldingResponse } from "@/lib/auth/gatePage";
|
|
52
54
|
import { readSession, KvSessionStore, SESSION_COOKIE } from "@/lib/auth/session";
|
|
53
55
|
import { isMaintenanceOn, maintenanceResponse } from "@/lib/maintenance";
|
|
54
56
|
import { HEALTH_PATH, healthResponse } from "@/lib/health";
|
|
55
|
-
import {
|
|
57
|
+
import { observeDeployVersion } from "@/lib/activity/deployVersion";
|
|
58
|
+
import { primeActivityKillSwitch } from "@/lib/activity/killSwitch";
|
|
56
59
|
import type { DeepPartial, ThemeTokens } from "@tot/public-runtime";
|
|
57
60
|
|
|
58
61
|
export const onRequest = defineMiddleware(async (context, next) => {
|
|
@@ -61,6 +64,21 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
61
64
|
const host =
|
|
62
65
|
request.headers.get("host") ?? url.host ?? "localhost";
|
|
63
66
|
|
|
67
|
+
// ---- Activity telemetry kill switch (card D8) -----------------------------
|
|
68
|
+
// Prime the ONE `ACTIVITY_TELEMETRY_DISABLED` flag from the (async) Worker env
|
|
69
|
+
// once per isolate, BEFORE the first emit, so every telemetry chokepoint in this
|
|
70
|
+
// isolate (deploy-provenance, recordActivity, ingest) observes it. Fail-open,
|
|
71
|
+
// never throws. Off by default (unset โ telemetry on).
|
|
72
|
+
await primeActivityKillSwitch();
|
|
73
|
+
|
|
74
|
+
// ---- Deploy provenance (card D2) โ worker_commit / deploy.version_observed -
|
|
75
|
+
// Once per worker isolate (cold start), emit `deploy.version_observed` carrying
|
|
76
|
+
// this build's commit SHA so D5 can derive deploy skew (running build vs umbrella
|
|
77
|
+
// tip). Idempotent per isolate, best-effort, never throws โ so it's safe here at
|
|
78
|
+
// the very front door, ahead of /health, and fires regardless of Host/tenant/gate.
|
|
79
|
+
// A no-op when the D8 kill switch above is set.
|
|
80
|
+
observeDeployVersion();
|
|
81
|
+
|
|
64
82
|
// ---- /health โ liveness + deploy-identity probe ---------------------------
|
|
65
83
|
// Answered FIRST: before tenant resolution, the access gate, the maintenance
|
|
66
84
|
// kill-switch, and the edge cache โ so it reports the running build's commit SHA
|
|
@@ -160,17 +178,18 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
160
178
|
return gatedHoldingResponse(decision.location);
|
|
161
179
|
}
|
|
162
180
|
if (admitted && viewerRecord) {
|
|
163
|
-
// Surface the viewer's
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
// Surface the viewer's standing FOR THIS host's tenant as chrome, BOUND to
|
|
182
|
+
// the host so a capability the session holds for another tenant never leaks
|
|
183
|
+
// in: a member sees their tenants[] role; the owner of their own tenant sees
|
|
184
|
+
// the resource-scoped session's capability; a ToT-staff viewer admitted via
|
|
185
|
+
// an explicit vendor selection sees the staffRoles-scoped capability. The
|
|
186
|
+
// ship gates (u10 decideIsOwner/resolveShipPrincipal) read this capability,
|
|
187
|
+
// so binding it here keeps owner-resolution server-side + tenant-correct.
|
|
188
|
+
const hostCapability = sessionHostCapability(viewerRecord, tenant.appDomain, nowSeconds);
|
|
170
189
|
locals.viewer = {
|
|
171
190
|
email: viewerRecord.email,
|
|
172
|
-
roles:
|
|
173
|
-
capability:
|
|
191
|
+
roles: hostCapability ? [hostCapability] : viewerRecord.roles,
|
|
192
|
+
capability: hostCapability ?? viewerRecord.capability,
|
|
174
193
|
};
|
|
175
194
|
}
|
|
176
195
|
}
|
|
@@ -210,8 +229,14 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
210
229
|
if (hit) return hit;
|
|
211
230
|
}
|
|
212
231
|
|
|
213
|
-
//
|
|
214
|
-
//
|
|
232
|
+
// Catalog source selection (F17 graduation path โ docs/architecture/product-catalog.md).
|
|
233
|
+
// A store's product source graduates over its life: JSON fixtures (dev default) ->
|
|
234
|
+
// D1 edge replica (staging) -> hosted ToT catalog (go-live). Here we pick the read
|
|
235
|
+
// backend: USE_D1_CATALOG=1 (+ a CATALOG_DB binding) reads the D1 edge replica;
|
|
236
|
+
// otherwise createToTClient falls back to the bundled JSON fixture snapshot. The
|
|
237
|
+
// hosted catalog graduates the SAME canonical records into the D1 read model
|
|
238
|
+
// (emitCatalogSql), so JSON/D1/hosted render identically โ pinned by
|
|
239
|
+
// lib/tot/sourceSelection.test.ts.
|
|
215
240
|
const d1 =
|
|
216
241
|
(await readEnv("USE_D1_CATALOG")) === "1" ? await readD1("CATALOG_DB") : undefined;
|
|
217
242
|
const tot = await createToTClient(tenant, d1);
|
|
@@ -360,6 +385,10 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
360
385
|
locals.canonicalBase = canonicalBase;
|
|
361
386
|
locals.customizationEnv = customization.env;
|
|
362
387
|
locals.cspNonce = cspNonce;
|
|
388
|
+
// Seeded here, filled in by whichever page/layout renders a widget slot
|
|
389
|
+
// (renderSlot.ts) as the page renders inside next() below โ read back out
|
|
390
|
+
// after next() resolves so the CSP frame-src reflects exactly what rendered.
|
|
391
|
+
locals.activeWidgetPlacements = [];
|
|
363
392
|
// Prefix that path-routed pages live under ("" for host-routed). Pages stay
|
|
364
393
|
// tenant-agnostic; the Layout uses this to keep root-absolute links inside
|
|
365
394
|
// the tenant prefix on the client.
|
|
@@ -427,6 +456,9 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
427
456
|
// strict policy only when this tenant has a store โ so add-to-cart, the
|
|
428
457
|
// sidecart iframe/XHR, and the cross-origin form POST all pass.
|
|
429
458
|
const checkoutCsp = commerce ? checkoutCspAdditions(commerce.origin) : null;
|
|
459
|
+
// Private-app widget origins (D5) โ read back AFTER next() so this reflects
|
|
460
|
+
// exactly the placements that rendered on THIS page, never a wildcard.
|
|
461
|
+
const widgetCsp = appWidgetCspAdditions(locals.activeWidgetPlacements ?? []);
|
|
430
462
|
response.headers.set(
|
|
431
463
|
cspHeader,
|
|
432
464
|
buildCsp({
|
|
@@ -450,7 +482,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
450
482
|
...(checkoutCsp?.connectSrc ?? []),
|
|
451
483
|
],
|
|
452
484
|
imgSrc: [...STOREFRONT_CSP_ALLOWLIST.imgSrc, ...(checkoutCsp?.imgSrc ?? [])],
|
|
453
|
-
frameSrc: checkoutCsp?.frameSrc ?? [],
|
|
485
|
+
frameSrc: [...(checkoutCsp?.frameSrc ?? []), ...widgetCsp.frameSrc],
|
|
454
486
|
formAction: checkoutCsp?.formAction ?? [],
|
|
455
487
|
reportGroup: CSP_REPORT_GROUP,
|
|
456
488
|
reportUri: CSP_REPORT_PATH,
|
|
@@ -2,15 +2,25 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* 404 โ written in the Northwind voice, not a stub. Offers a way home and a
|
|
4
4
|
* few collections to wander into so a dead link still lands somewhere useful.
|
|
5
|
+
*
|
|
6
|
+
* DELIBERATELY on-demand โ do NOT prerender (audit finding "selective-prerender").
|
|
7
|
+
* Fully tenant-varying: it reads Astro.locals.tenant + tot.listCollections() and
|
|
8
|
+
* renders under locals.themeStyle, all set per-request by the middleware from the
|
|
9
|
+
* resolved Host. Prerendering would freeze a single build-time tenant AND strip the
|
|
10
|
+
* middleware-applied CSP + security headers (no static _headers fallback exists).
|
|
5
11
|
*/
|
|
12
|
+
export const prerender = false;
|
|
13
|
+
|
|
6
14
|
import Layout from "@/layouts/Layout.astro";
|
|
7
15
|
import CollectionCard from "@/components/CollectionCard.astro";
|
|
8
16
|
import { withBase } from "@/lib/basePath";
|
|
9
17
|
|
|
10
|
-
const { tenant, tot, basePath } = Astro.locals;
|
|
18
|
+
const { tenant, tot, basePath, features } = Astro.locals;
|
|
11
19
|
|
|
12
|
-
// A handful of collections to redirect lost visitors toward.
|
|
13
|
-
|
|
20
|
+
// A handful of collections to redirect lost visitors toward. Marketing tenants
|
|
21
|
+
// (features.catalog === false) have no /collections route at all โ skip the
|
|
22
|
+
// fetch entirely rather than trust it to come back empty.
|
|
23
|
+
const collections = features.catalog ? (await tot.listCollections()).slice(0, 3) : [];
|
|
14
24
|
---
|
|
15
25
|
|
|
16
26
|
<Layout
|
|
@@ -36,12 +46,14 @@ const collections = (await tot.listCollections()).slice(0, 3);
|
|
|
36
46
|
>
|
|
37
47
|
Back home
|
|
38
48
|
</a>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
{features.catalog && (
|
|
50
|
+
<a
|
|
51
|
+
href={withBase(basePath, "/collections")}
|
|
52
|
+
class="inline-flex items-center rounded-full border border-[var(--color-border)] px-6 py-3 font-display text-sm font-medium text-[var(--color-text)] transition-colors hover:border-[var(--color-primary)]"
|
|
53
|
+
>
|
|
54
|
+
Browse collections
|
|
55
|
+
</a>
|
|
56
|
+
)}
|
|
45
57
|
</div>
|
|
46
58
|
</div>
|
|
47
59
|
</section>
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import Layout from "@/layouts/Layout.astro";
|
|
9
9
|
import Breadcrumbs from "@/components/Breadcrumbs.astro";
|
|
10
|
+
import ProseSections from "@/components/content/ProseSections.astro";
|
|
11
|
+
import ProseToc from "@/components/content/ProseToc.astro";
|
|
10
12
|
import { breadcrumbLd } from "@/lib/jsonld";
|
|
11
13
|
import { truncate } from "@/lib/format";
|
|
12
14
|
import { rawHtmlPolicy, rawHtmlResponse } from "@/lib/rawMarketingHtml";
|
|
@@ -56,6 +58,27 @@ const crumbs = [
|
|
|
56
58
|
const description =
|
|
57
59
|
page.seoDescription ||
|
|
58
60
|
(page.intro ? truncate(page.intro, 160) : undefined);
|
|
61
|
+
|
|
62
|
+
// Structured pages (F4) drive the body from `sections`; legacy pages keep the
|
|
63
|
+
// sanitized `body_html`. Prefer structured whenever a page ships sections.
|
|
64
|
+
const structured = (page.sections?.length ?? 0) > 0;
|
|
65
|
+
|
|
66
|
+
// Reviewer-only needs-review flags render in preview/dev only โ mirrors the
|
|
67
|
+
// middleware's "production read" definition (no preview host / token / cookie),
|
|
68
|
+
// so a real shopper never sees scaffolding but a reviewer does.
|
|
69
|
+
const isPreview =
|
|
70
|
+
import.meta.env.DEV ||
|
|
71
|
+
Astro.url.hostname.toLowerCase().startsWith("preview.") ||
|
|
72
|
+
Astro.url.searchParams.has("__preview") ||
|
|
73
|
+
Astro.cookies.has("tot_preview");
|
|
74
|
+
|
|
75
|
+
const updatedLabel = page.updated
|
|
76
|
+
? new Date(page.updated).toLocaleDateString("en-US", {
|
|
77
|
+
year: "numeric",
|
|
78
|
+
month: "long",
|
|
79
|
+
day: "numeric",
|
|
80
|
+
})
|
|
81
|
+
: null;
|
|
59
82
|
---
|
|
60
83
|
|
|
61
84
|
<Layout
|
|
@@ -80,10 +103,21 @@ const description =
|
|
|
80
103
|
{page.intro}
|
|
81
104
|
</p>
|
|
82
105
|
)}
|
|
106
|
+
{updatedLabel && (
|
|
107
|
+
<p class="mt-3 text-sm text-[var(--color-muted)]">Last updated {updatedLabel}</p>
|
|
108
|
+
)}
|
|
109
|
+
{structured && <ProseToc sections={page.sections!} />}
|
|
83
110
|
</header>
|
|
84
111
|
|
|
85
|
-
{/* Body โ constrained to a readable measure.
|
|
112
|
+
{/* Body โ constrained to a readable measure. Structured pages (F4) render
|
|
113
|
+
framework widgets from `sections`; legacy pages keep the sanitized HTML. */}
|
|
86
114
|
<div class="container-prose pb-[clamp(3rem,7vw,6rem)] pt-8">
|
|
87
|
-
|
|
115
|
+
{structured ? (
|
|
116
|
+
<div class="mx-auto max-w-[68ch]">
|
|
117
|
+
<ProseSections sections={page.sections!} includeNeedsReview={isPreview} />
|
|
118
|
+
</div>
|
|
119
|
+
) : (
|
|
120
|
+
<div class="rte mx-auto max-w-[68ch] text-[var(--color-text)]" set:html={page.body_html} />
|
|
121
|
+
)}
|
|
88
122
|
</div>
|
|
89
123
|
</Layout>
|
|
@@ -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>
|