@tokenoftrust/storefront-runner 1.4.1 → 2.0.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/env.d.ts +54 -5
- package/apps/storefront/integrations/materialize-guard.mjs +54 -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/0004_deep_morph.sql +36 -0
- package/apps/storefront/migrations-apps/0005_common_mystique.sql +23 -0
- package/apps/storefront/migrations-apps/0006_broad_microchip.sql +16 -0
- package/apps/storefront/migrations-apps/0007_brave_outlaw_kid.sql +15 -0
- package/apps/storefront/migrations-apps/0008_blue_dazzler.sql +11 -0
- package/apps/storefront/migrations-apps/0009_bored_shinobi_shaw.sql +15 -0
- package/apps/storefront/migrations-apps/0010_ws4_verified_provider_measurement.sql +35 -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/0004_snapshot.json +1474 -0
- package/apps/storefront/migrations-apps/meta/0005_snapshot.json +1617 -0
- package/apps/storefront/migrations-apps/meta/0006_snapshot.json +1716 -0
- package/apps/storefront/migrations-apps/meta/0007_snapshot.json +1812 -0
- package/apps/storefront/migrations-apps/meta/0008_snapshot.json +1881 -0
- package/apps/storefront/migrations-apps/meta/0009_snapshot.json +1979 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +56 -0
- package/apps/storefront/public/apps/tot.demo.analytics/0.1.0/entry.js +42 -0
- package/apps/storefront/public/js/dashboard-team.js +38 -2
- package/apps/storefront/public/platform/amplitude-http/v1/entry.mjs +19 -0
- package/apps/storefront/public/platform/google-measurement/v1/entry.mjs +19 -0
- package/apps/storefront/public/shared/commerce-chrome.css +20 -2
- package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +68 -0
- package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +111 -0
- package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +2 -0
- package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +247 -0
- package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +73 -0
- package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +79 -0
- package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +10982 -0
- package/apps/storefront/scripts/import-v84-through-ws2.ts +38 -0
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +2 -0
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +4 -0
- package/apps/storefront/scripts/install-v84-native-through-ws1.ts +185 -0
- package/apps/storefront/scripts/prove-clone-isolation.ts +506 -0
- package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +148 -0
- package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +35 -0
- package/apps/storefront/scripts/replay-v84-clean-clone.ts +214 -0
- package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +18 -0
- package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +18 -0
- package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +18 -0
- package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +17 -0
- package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +15 -0
- package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +21 -0
- package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +42 -0
- package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +145 -0
- package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +18 -0
- package/apps/storefront/src/components/Img.astro +1 -1
- 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 +1435 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
- package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +105 -0
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1980 -2564
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +5 -5
- 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/components/chrome/NavDropdown.astro +12 -0
- package/apps/storefront/src/components/chrome/SiteFooter.astro +7 -2
- package/apps/storefront/src/components/chrome/SiteHeader.astro +46 -10
- package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +1 -1
- package/apps/storefront/src/components/compliance/ComplianceNotice.astro +1 -2
- package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +1 -2
- package/apps/storefront/src/components/compliance/PactActNotice.astro +1 -1
- package/apps/storefront/src/components/compliance/Prop65Warning.astro +1 -2
- package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +2 -3
- package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +2 -2
- package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +2 -3
- package/apps/storefront/src/components/content/TrustStrip.astro +4 -4
- package/apps/storefront/src/components/islands/MobileNav.tsx +26 -1
- package/apps/storefront/src/components/islands/VariantSelector.tsx +27 -2
- package/apps/storefront/src/components/islands/useCheckoutHandoffCorrelation.ts +75 -0
- package/apps/storefront/src/components/membership/IllustrativeLabel.astro +1 -2
- package/apps/storefront/src/components/membership/TierCards.astro +2 -2
- package/apps/storefront/src/components/membership/TierComparisonTable.astro +2 -2
- package/apps/storefront/src/components/subscription/SavingsExplainer.astro +2 -2
- package/apps/storefront/src/config/adminTenantLookup.ts +35 -0
- package/apps/storefront/src/config/devTenantSeed.ts +9 -9
- package/apps/storefront/src/config/resolver.ts +4 -4
- package/apps/storefront/src/layouts/Layout.astro +146 -21
- package/apps/storefront/src/lib/activity/alerts.ts +17 -19
- package/apps/storefront/src/lib/activity/candidateSubmitLease.ts +87 -0
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +89 -9
- package/apps/storefront/src/lib/activity/deployVersion.ts +20 -27
- package/apps/storefront/src/lib/activity/ingest.ts +8 -8
- package/apps/storefront/src/lib/activity/ingestAuth.ts +1 -1
- package/apps/storefront/src/lib/activity/killSwitch.ts +6 -6
- package/apps/storefront/src/lib/activity/query.ts +9 -9
- package/apps/storefront/src/lib/activity/recordActivity.ts +11 -15
- package/apps/storefront/src/lib/activity/store.ts +4 -6
- package/apps/storefront/src/lib/activity/uiActor.ts +12 -12
- package/apps/storefront/src/lib/activity/workerCommit.ts +6 -6
- package/apps/storefront/src/lib/admin/adminShell.ts +292 -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/envelope.ts +1 -2
- 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 +14 -4
- package/apps/storefront/src/lib/apps/adminSession.ts +54 -45
- 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/appVersionStore.ts +281 -0
- package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
- package/apps/storefront/src/lib/apps/consentConfigStore.ts +228 -0
- 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/gtmMigrationInstall.ts +447 -0
- package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +367 -0
- package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +315 -0
- package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
- package/apps/storefront/src/lib/apps/integrationLifecycle.ts +614 -0
- package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +125 -0
- package/apps/storefront/src/lib/apps/integrationReadModel.ts +563 -0
- package/apps/storefront/src/lib/apps/integrationsTab.ts +547 -0
- package/apps/storefront/src/lib/apps/integrationsView.ts +106 -0
- package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +599 -0
- 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/orderConversionD1.ts +150 -0
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +9 -1
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +7 -7
- package/apps/storefront/src/lib/apps/orders/verifiedProviderMeasurement.ts +117 -0
- package/apps/storefront/src/lib/apps/registryService.ts +270 -16
- package/apps/storefront/src/lib/apps/scopes.ts +2 -2
- package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +170 -0
- 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/v84CleanCloneReplayContext.ts +16 -0
- package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +105 -0
- package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +177 -0
- package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +251 -0
- package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +241 -0
- package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +211 -0
- package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +409 -0
- package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +368 -0
- package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +188 -0
- package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +291 -0
- 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/apps/ws1ManagedAnalyticsClient.ts +95 -0
- package/apps/storefront/src/lib/auth/adminApiGuard.ts +66 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +11 -11
- package/apps/storefront/src/lib/auth/adminLanding.ts +88 -0
- 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 +2 -4
- package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
- 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 +400 -0
- package/apps/storefront/src/lib/blog/markdown.ts +174 -0
- package/apps/storefront/src/lib/blog/provider.ts +62 -3
- 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/chrome/examples/tokenoftrust.chrome-assignments.example.json +6 -0
- package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome.example.json +79 -0
- package/apps/storefront/src/lib/chrome/model.ts +48 -253
- package/apps/storefront/src/lib/chrome/navCurrent.ts +29 -0
- package/apps/storefront/src/lib/chrome/parseConfig.ts +18 -32
- package/apps/storefront/src/lib/chrome/variant.ts +39 -0
- package/apps/storefront/src/lib/commerce/checkoutHandoffCorrelation.ts +105 -0
- package/apps/storefront/src/lib/compliance/tamper-sentinel-boot.ts +152 -0
- package/apps/storefront/src/lib/compliance/tamper-sentinel.ts +327 -0
- package/apps/storefront/src/lib/content/index.ts +5 -5
- package/apps/storefront/src/lib/content-edit/applyDiff.ts +1 -1
- package/apps/storefront/src/lib/content-edit/route.ts +1 -1
- package/apps/storefront/src/lib/d1/catalog.ts +14 -2
- package/apps/storefront/src/lib/d1/schema-apps.ts +372 -17
- 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/cockpit/ask.ts +49 -0
- package/apps/storefront/src/lib/dev/cockpit/clickToSource.ts +71 -0
- package/apps/storefront/src/lib/dev/cockpit/copyCommand.ts +227 -0
- package/apps/storefront/src/lib/dev/cockpit/files.ts +249 -0
- package/apps/storefront/src/lib/dev/cockpit/hostedActivity.ts +500 -0
- package/apps/storefront/src/lib/dev/cockpit/infoPopover.ts +12 -0
- package/apps/storefront/src/lib/dev/cockpit/mcpCopy.ts +34 -0
- package/apps/storefront/src/lib/dev/cockpit/monitor.ts +107 -0
- package/apps/storefront/src/lib/dev/cockpit/stepper.ts +116 -0
- package/apps/storefront/src/lib/dev/cockpit/themeToggle.ts +23 -0
- 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/dev/runtimeStore.ts +1 -1
- package/apps/storefront/src/lib/edgeCache.ts +34 -5
- package/apps/storefront/src/lib/email/candidateConflictEmail.ts +80 -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/integration/browserEnvironment.ts +137 -0
- package/apps/storefront/src/lib/integration/managedInstallDeclarations.ts +72 -0
- package/apps/storefront/src/lib/integration/nonceStamp.ts +49 -0
- package/apps/storefront/src/lib/integration/tenantDeclarations.ts +116 -0
- package/apps/storefront/src/lib/jsonld.ts +118 -0
- package/apps/storefront/src/lib/membership/eligibility.ts +1 -1
- package/apps/storefront/src/lib/membership/model.ts +1 -2
- 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/previewReload.ts +57 -1
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +4625 -0
- package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
- package/apps/storefront/src/lib/publish/buildStream.ts +269 -0
- package/apps/storefront/src/lib/publish/collapsePersistence.ts +72 -0
- 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/lifecycleState.ts +369 -0
- package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +29 -0
- package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +78 -0
- package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +62 -0
- package/apps/storefront/src/lib/publish/mergeRowFindings.ts +124 -0
- package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +75 -0
- package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
- package/apps/storefront/src/lib/publish/publish-guard.ts +88 -0
- package/apps/storefront/src/lib/publish/publishFormat.ts +165 -0
- package/apps/storefront/src/lib/publish/publishHero.ts +177 -0
- package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +66 -0
- package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +141 -0
- package/apps/storefront/src/lib/publish/reviewRelease/index.ts +41 -0
- package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +47 -0
- package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +42 -0
- package/apps/storefront/src/lib/publish/reviewRelease/types.ts +59 -0
- package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +440 -0
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +161 -0
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +325 -0
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +241 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +121 -51
- package/apps/storefront/src/lib/publish/sseRoute.ts +119 -0
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
- package/apps/storefront/src/lib/publish/streamLogConsole.ts +142 -0
- package/apps/storefront/src/lib/publish/versionNumber.ts +52 -0
- package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
- package/apps/storefront/src/lib/rawChrome.ts +125 -47
- package/apps/storefront/src/lib/rawMarketingHtml.ts +74 -3
- 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/storyblok/content-model.ts +7 -33
- package/apps/storefront/src/lib/storyblok/provider.ts +168 -60
- package/apps/storefront/src/lib/subscription/model.ts +4 -4
- package/apps/storefront/src/lib/the-build/provider.ts +32 -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 +385 -52
- package/apps/storefront/src/pages/[...slug].astro +9 -4
- package/apps/storefront/src/pages/admin/review-release.astro +265 -0
- package/apps/storefront/src/pages/admin/select.astro +96 -0
- package/apps/storefront/src/pages/admin.astro +25 -1313
- package/apps/storefront/src/pages/api/activity.ts +8 -8
- package/apps/storefront/src/pages/api/admin/activity-alerts.ts +5 -20
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +7 -2
- package/apps/storefront/src/pages/api/admin/orders.ts +8 -2
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +174 -0
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +96 -0
- package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +118 -0
- package/apps/storefront/src/pages/api/apps/admin/integrations.ts +89 -0
- package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +85 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +74 -2
- package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
- package/apps/storefront/src/pages/api/commerce/checkout-handoff.ts +77 -0
- package/apps/storefront/src/pages/api/compliance/tamper.ts +88 -0
- package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +108 -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 +20 -2
- package/apps/storefront/src/pages/blog/[slug].astro +99 -19
- 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 +4 -2
- package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
- package/apps/storefront/src/pages/cockpit.astro +77 -1353
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
- package/apps/storefront/src/pages/index.astro +10 -5
- package/apps/storefront/src/pages/llms.txt.ts +1 -1
- package/apps/storefront/src/pages/products/[handle].astro +7 -6
- package/apps/storefront/src/pages/search.astro +1 -1
- package/apps/storefront/src/pages/sitemap.xml.ts +21 -1
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +31 -27
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +1 -1
- package/apps/storefront/src/pages/tenants/[id]/[...path].ts +38 -2
- 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 +1106 -0
- package/apps/storefront/tsconfig.json +6 -1
- package/package.json +1 -1
- package/packages/public-runtime/package.json +3 -1
- package/packages/public-runtime/schemas/managed-analytics/v1/adapter-definition.schema.json +427 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/create-integration-install-request.schema.json +161 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/create-migration-draft-request.schema.json +157 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/delete-integration-install-request.schema.json +84 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/delete-migration-draft-request.schema.json +84 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/error-envelope.schema.json +180 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-adapter-definition-request.schema.json +52 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-install-request.schema.json +52 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-revision-request.schema.json +79 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-migration-draft-request.schema.json +52 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/integration-install.schema.json +167 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/integration-revision.schema.json +208 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/list-adapter-definitions-request.schema.json +53 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-installs-request.schema.json +53 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-revisions-request.schema.json +82 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/migration-draft.schema.json +218 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/mutation-request.schema.json +52 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/mutation-success-envelope.schema.json +49 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/read-request.schema.json +20 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/read-success-envelope.schema.json +45 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/revise-integration-install-request.schema.json +156 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/set-integration-install-status-request.schema.json +92 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/update-migration-draft-request.schema.json +184 -0
- package/packages/public-runtime/scripts/generate-managed-analytics-api-v1-schemas.mjs +13 -0
- package/packages/public-runtime/src/activity/catalog.ts +89 -33
- package/packages/public-runtime/src/activity/event.ts +6 -6
- package/packages/public-runtime/src/activity/index.ts +2 -2
- package/packages/public-runtime/src/activity/redaction.ts +4 -4
- package/packages/public-runtime/src/candidate-index.ts +42 -8
- package/packages/public-runtime/src/catalog-d1.ts +1 -1
- package/packages/public-runtime/src/checkout-orders.ts +2 -2
- package/packages/public-runtime/src/checkout-style-publish.ts +2 -2
- package/packages/public-runtime/src/checkout-style.ts +7 -7
- package/packages/public-runtime/src/checkout.ts +3 -3
- package/packages/public-runtime/src/chrome.ts +777 -0
- package/packages/public-runtime/src/compliance-obligations.ts +310 -0
- package/packages/public-runtime/src/csp.ts +99 -0
- package/packages/public-runtime/src/customization-preview.ts +25 -2
- package/packages/public-runtime/src/customization-runtime.ts +156 -3
- package/packages/public-runtime/src/customization-versioning.ts +10 -0
- package/packages/public-runtime/src/destination-webhook/contract.ts +353 -0
- package/packages/public-runtime/src/embed-catalog.d.mts +9 -0
- package/packages/public-runtime/src/embed-catalog.mjs +40 -0
- package/packages/public-runtime/src/evidence.ts +288 -0
- package/packages/public-runtime/src/gtm-migration/bounds.ts +138 -0
- package/packages/public-runtime/src/gtm-migration/disposition-rules.ts +361 -0
- package/packages/public-runtime/src/gtm-migration/dispositions.ts +181 -0
- package/packages/public-runtime/src/gtm-migration/draft-generator.ts +400 -0
- package/packages/public-runtime/src/gtm-migration/graph.ts +468 -0
- package/packages/public-runtime/src/gtm-migration/index.ts +95 -0
- package/packages/public-runtime/src/gtm-migration/parse.ts +410 -0
- package/packages/public-runtime/src/gtm-migration/references.ts +266 -0
- package/packages/public-runtime/src/gtm-migration/types.ts +86 -0
- package/packages/public-runtime/src/index.ts +17 -1
- package/packages/public-runtime/src/integration/assurance-decision.ts +259 -0
- package/packages/public-runtime/src/integration/assurance.ts +228 -0
- package/packages/public-runtime/src/integration/candidate-evidence.ts +377 -0
- package/packages/public-runtime/src/integration/commerce-broker.ts +497 -0
- package/packages/public-runtime/src/integration/consent-broker.ts +337 -0
- package/packages/public-runtime/src/integration/consent-cmp-demand.ts +115 -0
- package/packages/public-runtime/src/integration/consent-config.ts +260 -0
- package/packages/public-runtime/src/integration/consent-preview.ts +125 -0
- package/packages/public-runtime/src/integration/correlation-token.ts +444 -0
- package/packages/public-runtime/src/integration/csp-compiler.ts +267 -0
- package/packages/public-runtime/src/integration/csp.ts +105 -0
- package/packages/public-runtime/src/integration/digest.ts +43 -0
- package/packages/public-runtime/src/integration/environment.ts +228 -0
- package/packages/public-runtime/src/integration/events-catalog.ts +186 -0
- package/packages/public-runtime/src/integration/events.ts +701 -0
- package/packages/public-runtime/src/integration/g2-action-boundary.ts +106 -0
- package/packages/public-runtime/src/integration/index.ts +90 -0
- package/packages/public-runtime/src/integration/interim-bridge.ts +138 -0
- package/packages/public-runtime/src/integration/lifecycle-boot.ts +317 -0
- package/packages/public-runtime/src/integration/lifecycle-runtime.ts +393 -0
- package/packages/public-runtime/src/integration/lifecycle.ts +94 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.d.mts +2 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.mjs +10 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +382 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +684 -0
- package/packages/public-runtime/src/integration/managed-analytics-bridge.ts +383 -0
- package/packages/public-runtime/src/integration/managed-analytics-value-safety.ts +14 -0
- package/packages/public-runtime/src/integration/managed-analytics.ts +277 -0
- package/packages/public-runtime/src/integration/manifest.ts +470 -0
- package/packages/public-runtime/src/integration/native-amplitude.ts +362 -0
- package/packages/public-runtime/src/integration/native-ga4.ts +352 -0
- package/packages/public-runtime/src/integration/observability.ts +451 -0
- package/packages/public-runtime/src/integration/order-conversion.ts +0 -0
- package/packages/public-runtime/src/integration/phases.ts +182 -0
- package/packages/public-runtime/src/integration/plan.ts +112 -0
- package/packages/public-runtime/src/integration/platform-adapter-runtime.ts +146 -0
- package/packages/public-runtime/src/integration/platform-adapters.ts +671 -0
- package/packages/public-runtime/src/integration/promotion-gate.ts +451 -0
- package/packages/public-runtime/src/integration/provider-checkout-extension.ts +425 -0
- package/packages/public-runtime/src/integration/receipt-measurement.ts +370 -0
- package/packages/public-runtime/src/integration/regulated-parent-cert.ts +311 -0
- package/packages/public-runtime/src/integration/render-phases.ts +267 -0
- package/packages/public-runtime/src/integration/resolve.ts +465 -0
- package/packages/public-runtime/src/integration/result.ts +239 -0
- package/packages/public-runtime/src/integration/secrets.ts +92 -0
- package/packages/public-runtime/src/integration/tamper-evidence.ts +205 -0
- package/packages/public-runtime/src/integration/tamper-recovery.ts +127 -0
- package/packages/public-runtime/src/integration/version.ts +92 -0
- package/packages/public-runtime/src/integration-capabilities.ts +520 -0
- package/packages/public-runtime/src/membership.ts +2 -2
- package/packages/public-runtime/src/raw-html-policy.ts +2 -4
- package/packages/public-runtime/src/regulated-commerce/broker/consent-dedupe.ts +144 -0
- package/packages/public-runtime/src/regulated-commerce/correlation/contract.ts +177 -0
- package/packages/public-runtime/src/regulated-commerce/diagnostics/local-dispatch.ts +24 -0
- package/packages/public-runtime/src/regulated-commerce/diagnostics/read-model.ts +69 -0
- package/packages/public-runtime/src/regulated-commerce/dispatch/core.ts +134 -0
- package/packages/public-runtime/src/regulated-commerce/events/contract.ts +113 -0
- package/packages/public-runtime/src/regulated-commerce/ledger/core.ts +81 -0
- package/packages/public-runtime/src/regulated-commerce/mappings/ga4-ads.ts +87 -0
- package/packages/public-runtime/src/regulated-commerce/provider-order/bridge.ts +52 -0
- package/packages/public-runtime/src/regulated-commerce/purchase-truth/contract.ts +252 -0
- package/packages/public-runtime/src/regulated-commerce/sources/accepted-browser-operation.ts +67 -0
- package/packages/public-runtime/src/regulated-commerce/testing/fake-destination.ts +93 -0
- package/packages/public-runtime/src/static-bundle.ts +357 -0
- package/packages/public-runtime/src/tenant-assets.ts +143 -4
- package/packages/public-runtime/src/tenant.ts +81 -17
- package/packages/public-runtime/tests/fixtures/integration/browser-environment.example.json +8 -0
- package/packages/public-runtime/tests/fixtures/integration/commerce-event.example.json +44 -0
- package/packages/public-runtime/tests/fixtures/integration/commerce-order-event.example.json +44 -0
- package/packages/public-runtime/tests/fixtures/integration/compliance-assurance-record.example.json +50 -0
- package/packages/public-runtime/tests/fixtures/integration/integration-manifest-v2.example.json +120 -0
- package/packages/public-runtime/tests/fixtures/integration/managed-analytics-v1/fake-adapter-catalog.json +35 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-action-registry.example.json +11 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/README.md +22 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/entry.js +30 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/manifest.json +75 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/public-config.json +3 -0
- package/packages/public-runtime/tests/fixtures/regulated-commerce/fake-destinations.ts +38 -0
- package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.provenance.json +29 -0
- package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.redacted.json +3397 -0
- 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 +4 -6
- package/scripts/dev/file-writer.mjs +3 -3
- package/scripts/dev/git-status.mjs +2 -2
- package/scripts/dev/integration-guardrails.mjs +498 -0
- 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/safe-path.mjs +2 -2
- package/scripts/dev/shot.mjs +1 -1
- package/scripts/dev/tenant-source-root.mjs +1 -1
- package/scripts/dev/unified-diff.mjs +8 -3
- package/scripts/tot-dev.mjs +8 -11
- package/apps/storefront/src/lib/dev/vcBinding.ts +0 -80
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* - OPEN CHANGE QUEUE from GET /api/changes ({ current, changes }) with
|
|
8
8
|
* per-change reconcile/compliance state, preview link, head sha, and age.
|
|
9
|
-
* - ACCEPT TO PREVIEW (POST /api/changes/integrate —
|
|
9
|
+
* - ACCEPT TO PREVIEW (POST /api/changes/integrate — the accept-to-preview
|
|
10
10
|
* split; advances the shared `preview` aggregate, does NOT go live) and
|
|
11
11
|
* REJECT (with a note) per change. Going live is the aggregate card's
|
|
12
12
|
* exact-digest PUBLISH (POST /api/changes/ship), never a per-change accept.
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* OWNER_CAPABILITIES) — others see the queue read-only with a sign-in note.
|
|
24
24
|
* - Non-premium tenants see the go-live control as a PAYWALL UPSELL — never
|
|
25
25
|
* hidden, never "coming soon" (recorded product direction). The
|
|
26
|
-
* entitlement signal is the existing premium model
|
|
26
|
+
* entitlement signal is the existing premium model: a custom-domain
|
|
27
27
|
* tenant is premium/pilot; a `<label>.tokenoftrust.store` self-serve store
|
|
28
28
|
* is not. TODO(paywall workstream): replace with the real billing
|
|
29
29
|
* entitlement when one exists.
|
|
@@ -32,10 +32,13 @@ import { readViewerSession } from "@/lib/auth/route";
|
|
|
32
32
|
import { withBase } from "@/lib/basePath";
|
|
33
33
|
import { readEnv } from "@/lib/env";
|
|
34
34
|
import { resolveViewerShipCapability } from "@/lib/change";
|
|
35
|
+
import { resolveLiveVersion } from "@/lib/change/views/live-version";
|
|
36
|
+
import { isPublicSiteCurrent, staticLiveManifestUrl } from "@/lib/change/views/live-digest";
|
|
35
37
|
import { isHostedStoreDomain, isInternalTestTenant } from "@tot/public-runtime";
|
|
38
|
+
import { mintPreviewShareToken } from "@/lib/preview/previewShareLink";
|
|
36
39
|
|
|
37
40
|
const { envelope } = Astro.props;
|
|
38
|
-
const { tenant, basePath
|
|
41
|
+
const { tenant, basePath } = Astro.locals;
|
|
39
42
|
|
|
40
43
|
// Viewer capability, mirrored from the same session model the API routes gate
|
|
41
44
|
// on (resolveOwnerSession admits capability owner|admin). UX-only: the server
|
|
@@ -49,7 +52,7 @@ const viewerCapability =
|
|
|
49
52
|
// A signed-in DEVELOPER (not owner/admin) may hold an explicit, owner-revocable
|
|
50
53
|
// ship-on-behalf grant. Resolve it LIVE from the MCP subject-capability store —
|
|
51
54
|
// NEVER the coarse role, NEVER a client header — so a ship-granted developer sees
|
|
52
|
-
// the queue + Go-live/Reject
|
|
55
|
+
// the queue + Go-live/Reject. This MIRRORS the server (resolveShipSession /
|
|
53
56
|
// effectiveShipCapability); the server re-authorizes every action. UX-only.
|
|
54
57
|
let viewerCanShip = viewerCapability === "owner" || viewerCapability === "admin";
|
|
55
58
|
if (!viewerCanShip && Astro.locals.viewer?.email) {
|
|
@@ -75,40 +78,63 @@ const internalTestTenant = isInternalTestTenant(tenant);
|
|
|
75
78
|
const staticPublishEnabled = Boolean(
|
|
76
79
|
tenant.staticPublish && tenant.staticPublish.enabled !== false,
|
|
77
80
|
);
|
|
78
|
-
// Presence-only signal for the
|
|
81
|
+
// Presence-only signal for the dispatch sink (never the value).
|
|
79
82
|
const dispatchConfigured = Boolean((await readEnv("WWW_PUBLISH_DISPATCH_TOKEN"))?.trim());
|
|
80
83
|
|
|
84
|
+
// "What does the public site actually serve right now?" — the read-back half of
|
|
85
|
+
// the static-publish seam (resolveLiveVersion's staticLiveDigest), computed here
|
|
86
|
+
// so it's ALWAYS visible rather than only living in GET /api/changes JSON. This is
|
|
87
|
+
// the exact gap that can let tokenoftrust.com's published public site
|
|
88
|
+
// sit two days stale behind several accepted-but-unpublished changes with nothing
|
|
89
|
+
// in the console calling it out — going live and staying live for the PUBLIC
|
|
90
|
+
// static target are two different facts, and this card makes the second one
|
|
91
|
+
// honest instead of assumed. Best-effort: resolveLiveVersion never throws.
|
|
92
|
+
// BUDGETED: resolveLiveVersion chains several TENANT_CACHE reads (resolvePreviewTenant,
|
|
93
|
+
// resolveChannelVersion, readSiteVersion) that are pathologically slow + variable on the
|
|
94
|
+
// preview worker (measured 2-10s EACH, spiking together — a KV/storage-latency issue, not
|
|
95
|
+
// this code). It runs in blocking SSR frontmatter, so it gated the whole admin document
|
|
96
|
+
// stream. It's best-effort display chrome ("what does live/ serve right now"), and the
|
|
97
|
+
// client-side GET /api/changes already returns `current`, so cap it: on timeout the card
|
|
98
|
+
// renders from the client fetch instead of blocking first paint.
|
|
99
|
+
const liveVersion = staticPublishEnabled
|
|
100
|
+
? await Promise.race([
|
|
101
|
+
resolveLiveVersion(tenant.appDomain),
|
|
102
|
+
new Promise<null>((resolve) => setTimeout(() => resolve(null), 1500)),
|
|
103
|
+
])
|
|
104
|
+
: null;
|
|
105
|
+
const publicSiteDigest = liveVersion?.staticLiveDigest ?? null;
|
|
106
|
+
// The actual verdict: does the public target's manifest sha match what was last
|
|
107
|
+
// promoted to "live" here? Both are real tenant Gitea shas — a direct,
|
|
108
|
+
// honest comparison, not a heuristic. See liveDigest.ts's isPublicSiteCurrent.
|
|
109
|
+
const publicSiteVerdict = publicSiteDigest
|
|
110
|
+
? isPublicSiteCurrent(liveVersion?.versionId ?? null, publicSiteDigest)
|
|
111
|
+
: null;
|
|
112
|
+
|
|
81
113
|
const changesEndpoint = withBase(basePath || "", "/api/changes");
|
|
82
|
-
//
|
|
83
|
-
// - integrateEndpoint =
|
|
114
|
+
// The branch-integration lifecycle SPLIT of accept from publish:
|
|
115
|
+
// - integrateEndpoint = "accept = integrate into the protected preview
|
|
84
116
|
// aggregate" (POST /api/changes/integrate → TenantIntegrationQueue.enqueue).
|
|
85
117
|
// This is the candidate-queue "Accept to preview" action — it advances the
|
|
86
118
|
// shared aggregate, it does NOT go live. Returns an IntegrateOutcome
|
|
87
119
|
// (queueState/runState/statusMessage), never a bare merged:true.
|
|
88
|
-
// - shipEndpoint =
|
|
89
|
-
//
|
|
120
|
+
// - shipEndpoint = the exact-digest go-live (POST /api/changes/ship →
|
|
121
|
+
// AggregateShipOrchestrator.plan/ship). The aggregate card's "Publish"
|
|
90
122
|
// action: it ships the reviewed pinned SHA + digest, double-gated
|
|
91
123
|
// (paywall + human confirm). The read-only plan is served on GET /api/changes
|
|
92
124
|
// (`plan`), so the confirm surface never has to POST to preview it.
|
|
93
|
-
// If b08/b10's exact paths differ at fold, the integrator reconciles here —
|
|
94
|
-
// these are the agreed contract paths (see the unit brief).
|
|
95
125
|
const integrateEndpoint = withBase(basePath || "", "/api/changes/integrate");
|
|
96
126
|
const shipEndpoint = withBase(basePath || "", "/api/changes/ship");
|
|
97
|
-
//
|
|
98
|
-
// DISTINCT from reject (which closes the change). Gated server-side by retireGate.
|
|
99
|
-
const retireEndpoint = withBase(basePath || "", "/api/preview/retire");
|
|
100
|
-
// rux5 — build-on-demand for a synthetic not-built PR row: materialize the PR's
|
|
127
|
+
// Build-on-demand for a synthetic not-built PR row: materialize the PR's
|
|
101
128
|
// hosted preview (INERT — flips no live channel). Gated server-side by buildGate
|
|
102
129
|
// (the ship-on-behalf floor); the endpoint resolves the head sha via forge PR-read
|
|
103
130
|
// when the row supplies only the PR number.
|
|
104
131
|
const buildEndpoint = withBase(basePath || "", "/api/preview/build");
|
|
105
|
-
//
|
|
132
|
+
// The ship-operations workspace woven into this tab: versions/promotion
|
|
106
133
|
// history, per-candidate diff, and rollback (through the ONE rollback
|
|
107
134
|
// orchestrator). Read endpoints mirror the change-queue's auth/scope.
|
|
108
135
|
const versionsEndpoint = withBase(basePath || "", "/api/changes/versions");
|
|
109
136
|
const rollbackEndpoint = withBase(basePath || "", "/api/changes/rollback");
|
|
110
|
-
//
|
|
111
|
-
// DISTINCT endpoint from retireEndpoint above (branch delete vs artifact evict).
|
|
137
|
+
// Branch retention: GET dry-run classification / POST guarded delete.
|
|
112
138
|
const branchesEndpoint = withBase(basePath || "", "/api/changes/branches");
|
|
113
139
|
// The client builds `${diffEndpointBase}/<changeId>/diff` per candidate.
|
|
114
140
|
const diffEndpointBase = changesEndpoint;
|
|
@@ -116,9 +142,47 @@ const diffEndpointBase = changesEndpoint;
|
|
|
116
142
|
// digest actually serves. Rendered into "already live" copy + the publish
|
|
117
143
|
// ceremony's "View your live store" CTA.
|
|
118
144
|
const liveHref = withBase(basePath || "", "/");
|
|
145
|
+
|
|
146
|
+
// LIVE-SITE ENTRY POINTS — surfaced explicitly so "where does my store actually
|
|
147
|
+
// serve?" is never a mystery (it was). Two public URLs, always shown + clickable:
|
|
148
|
+
// 1. Hosted store — THIS worker at the request host + tenant base path
|
|
149
|
+
// (e.g. storefront.tokenoftrust.store/tokenoftrust.com/).
|
|
150
|
+
// 2. Public site — the static target's canonical URL, derived from the SAME
|
|
151
|
+
// manifest URL the freshness read-back probes (WWW_PUBLISH_VERIFY_URL or
|
|
152
|
+
// https://<canonicalDomain>/manifest.json), minus the /manifest.json tail
|
|
153
|
+
// (e.g. published.tokenoftrust.com). Only for static-publish tenants; a
|
|
154
|
+
// hosted-only tenant has no separate public site. Computed PURELY (no probe)
|
|
155
|
+
// so the URL shows even when the freshness probe above timed out.
|
|
156
|
+
const requestHost = Astro.request.headers.get("host") ?? Astro.url.host;
|
|
157
|
+
const liveStoreUrl = `${Astro.url.protocol}//${requestHost}${liveHref}`;
|
|
158
|
+
const publicManifestUrl = staticPublishEnabled
|
|
159
|
+
? staticLiveManifestUrl({
|
|
160
|
+
canonicalDomain: tenant.canonicalDomain,
|
|
161
|
+
// Per-tenant override first (never bleeds across stores), then the global env
|
|
162
|
+
// escape hatch, then the canonical-apex default — mirrors resolveLiveVersion.
|
|
163
|
+
explicitUrl: tenant.staticPublish?.verifyUrl ?? (await readEnv("WWW_PUBLISH_VERIFY_URL")) ?? null,
|
|
164
|
+
})
|
|
165
|
+
: null;
|
|
166
|
+
const publicSiteUrl = publicManifestUrl
|
|
167
|
+
? publicManifestUrl.replace(/\/manifest\.json$/, "/")
|
|
168
|
+
: null;
|
|
169
|
+
const stripScheme = (u: string): string => u.replace(/^https?:\/\//, "");
|
|
170
|
+
|
|
171
|
+
// r08 — a signed, tenant-scoped, expiring `?__preview` capability link an owner can
|
|
172
|
+
// copy and hand to a non-technical stakeholder (view-only, no sign-in required).
|
|
173
|
+
// Minted server-side so the signing secret never reaches the browser (mirrors
|
|
174
|
+
// preview/dashboard.astro). Signs the tenant_id claim: the shared-preview
|
|
175
|
+
// middleware verifies against `tenant.tenant_id` and the candidate renderer accepts
|
|
176
|
+
// tenant_id too, so ONE token authorizes BOTH the shared next-release preview AND
|
|
177
|
+
// per-candidate `preview/pr/<N>` links. Only minted for a ship-capable viewer, and
|
|
178
|
+
// absent when signing isn't configured — the copy buttons then stay hidden rather
|
|
179
|
+
// than offering a dead link. TTL 24h so a link copied late in a long session still
|
|
180
|
+
// works; a reload re-mints a fresh one.
|
|
181
|
+
const previewShareToken = viewerCanShip ? await mintPreviewShareToken(tenant.tenant_id) : null;
|
|
182
|
+
|
|
119
183
|
const domainStatusEndpoint = withBase(basePath || "", "/api/domain/status");
|
|
120
184
|
const domainDispatchEndpoint = withBase(basePath || "", "/api/domain/dispatch");
|
|
121
|
-
//
|
|
185
|
+
// The machine-verifiable apex-cutover readiness gate — the SAME server gate the
|
|
122
186
|
// `tot go-live` CLI reads, so the admin display and the CLI never diverge.
|
|
123
187
|
const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness");
|
|
124
188
|
---
|
|
@@ -134,7 +198,6 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
134
198
|
data-changes-endpoint={changesEndpoint}
|
|
135
199
|
data-integrate-endpoint={integrateEndpoint}
|
|
136
200
|
data-ship-endpoint={shipEndpoint}
|
|
137
|
-
data-retire-endpoint={retireEndpoint}
|
|
138
201
|
data-build-endpoint={buildEndpoint}
|
|
139
202
|
data-versions-endpoint={versionsEndpoint}
|
|
140
203
|
data-rollback-endpoint={rollbackEndpoint}
|
|
@@ -145,17 +208,20 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
145
208
|
data-domain-readiness-endpoint={domainReadinessEndpoint}
|
|
146
209
|
data-repo={tenant.appDomain}
|
|
147
210
|
data-app-domain={tenant.appDomain}
|
|
211
|
+
data-canonical-domain={tenant.canonicalDomain ?? ""}
|
|
212
|
+
data-preview-share-token={previewShareToken ?? ""}
|
|
148
213
|
data-live-href={liveHref}
|
|
149
214
|
data-can-ship={viewerCanShip ? "true" : "false"}
|
|
150
215
|
data-is-owner={viewerIsOwner ? "true" : "false"}
|
|
151
216
|
data-entitled={goLiveEntitled ? "true" : "false"}
|
|
152
217
|
data-static-publish={staticPublishEnabled ? "true" : "false"}
|
|
153
218
|
data-dispatch-configured={dispatchConfigured ? "true" : "false"}
|
|
219
|
+
data-public-site-host={publicSiteUrl ? stripScheme(publicSiteUrl).replace(/\/$/, "") : ""}
|
|
154
220
|
>
|
|
155
221
|
<div class="section-title">
|
|
156
222
|
<div>
|
|
157
223
|
<h2 id="publish-title">Publish</h2>
|
|
158
|
-
<p>
|
|
224
|
+
<p>Review what's changed, then publish your next release when you're ready.</p>
|
|
159
225
|
</div>
|
|
160
226
|
<div class="actions">
|
|
161
227
|
<button class="btn" type="button" data-publish-refresh>
|
|
@@ -206,11 +272,79 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
206
272
|
</div>
|
|
207
273
|
)}
|
|
208
274
|
|
|
275
|
+
<!-- State-driven "next action" hero (r01) — the SINGLE top-of-tab state
|
|
276
|
+
element. ONE sentence + ONE primary CTA, derived by lib/publish/publishHero.ts
|
|
277
|
+
from the SAME /api/changes read the queue + aggregate already fetch (no extra
|
|
278
|
+
read). It ABSORBS the two former standalone banners: the merge-doctor health
|
|
279
|
+
summary (B1) and the platform-fault "Report to support" escalation (B3) are
|
|
280
|
+
folded into the hero's state model, expressed through this one element — a
|
|
281
|
+
merge conflict/platform blocker turns the hero red/urgent instead of rendering
|
|
282
|
+
a separate banner. Rendered by renderHero() in adminPublishTab.ts; starts
|
|
283
|
+
hidden until the first /api/changes read resolves. The r03 stale-public-site
|
|
284
|
+
alarm sits alongside it in this region (a distinct top-level signal that
|
|
285
|
+
deep-links into Live Site), so there is ONE hero/alert region, not floating
|
|
286
|
+
banners. Urgency is never color-only: the tone drives the icon + an explicit
|
|
287
|
+
"Action needed" label. -->
|
|
288
|
+
<div class="publish-hero-region" data-publish-hero-region>
|
|
289
|
+
<div class="publish-hero" data-publish-hero data-tone="calm" hidden role="status" aria-live="polite">
|
|
290
|
+
<div class="publish-hero-main">
|
|
291
|
+
<span class="publish-hero-icon" data-publish-hero-icon aria-hidden="true">
|
|
292
|
+
<svg><use href="#i-check"></use></svg>
|
|
293
|
+
</span>
|
|
294
|
+
<div class="publish-hero-copy">
|
|
295
|
+
<span class="publish-hero-kicker" data-publish-hero-kicker hidden>Action needed</span>
|
|
296
|
+
<b class="publish-hero-headline" data-publish-hero-headline></b>
|
|
297
|
+
<span class="publish-hero-detail" data-publish-hero-detail></span>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
<div class="publish-hero-actions">
|
|
301
|
+
<button class="btn primary publish-hero-cta" type="button" data-publish-hero-cta hidden></button>
|
|
302
|
+
<a class="btn primary publish-hero-cta" data-publish-hero-cta-link hidden href="mailto:success@tokenoftrust.com"></a>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
|
|
306
|
+
<!-- r03: stale-public-site alarm, LIFTED to top-level. The "public site is
|
|
307
|
+
behind the live pointer" verdict (resolveLiveVersion / isPublicSiteCurrent)
|
|
308
|
+
used to sit buried inside the collapsible Live Site section; when the public
|
|
309
|
+
target is behind, surface it here as a top-level amber alarm that deep-links
|
|
310
|
+
into Live Site (where the full breakdown stays nested). SSR-driven from the
|
|
311
|
+
same publicSiteVerdict the Live Site chip reads — shown only for a
|
|
312
|
+
static-publish tenant whose public target is confirmed behind. -->
|
|
313
|
+
{staticPublishEnabled && publicSiteVerdict && publicSiteVerdict.status === "behind" && (
|
|
314
|
+
<div class="risk-alert amber publish-stale-site" data-publish-stale-site role="status" aria-live="polite">
|
|
315
|
+
<div class="risk-alert-main">
|
|
316
|
+
<svg><use href="#i-alert"></use></svg>
|
|
317
|
+
<div>
|
|
318
|
+
<b>Public site is behind — re-publish to catch it up</b>
|
|
319
|
+
<span>
|
|
320
|
+
Your public site is still serving an older release
|
|
321
|
+
(<code class="publish-mono">{publicSiteVerdict.publishedSha.slice(0, 12)}</code>)
|
|
322
|
+
while your live pointer has moved on to
|
|
323
|
+
(<code class="publish-mono">{publicSiteVerdict.livePointerSha.slice(0, 12)}</code>).
|
|
324
|
+
Re-publish, or open the publishing details for the full breakdown.
|
|
325
|
+
</span>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
<a class="btn" href="#publish-live-site" data-publish-stale-site-link>Open publishing details</a>
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
|
|
209
333
|
<div class="grid cols-3">
|
|
210
334
|
<div class="metric">
|
|
211
335
|
<span>Live version</span>
|
|
212
|
-
<strong
|
|
336
|
+
<strong>
|
|
337
|
+
<!-- The human version label ("v42") links straight to the live store; the
|
|
338
|
+
raw SHA is demoted to the Technical details disclosure below. -->
|
|
339
|
+
<a class="live-version-link" data-publish-live-version-link href={liveHref}
|
|
340
|
+
target="_blank" rel="noopener" hidden>—</a>
|
|
341
|
+
<span data-publish-live-version-none>—</span>
|
|
342
|
+
</strong>
|
|
213
343
|
<small data-publish-live-source>What shoppers see right now</small>
|
|
344
|
+
<details class="metric-tech" data-publish-live-tech hidden>
|
|
345
|
+
<summary>Technical details</summary>
|
|
346
|
+
<span class="publish-mono subtle" data-publish-live-sha>—</span>
|
|
347
|
+
</details>
|
|
214
348
|
</div>
|
|
215
349
|
<div class="metric">
|
|
216
350
|
<span>Published</span>
|
|
@@ -240,11 +374,12 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
240
374
|
</div>
|
|
241
375
|
)}
|
|
242
376
|
|
|
243
|
-
<div class="panel">
|
|
244
|
-
<
|
|
245
|
-
|
|
377
|
+
<div class="panel" id="publish-queue">
|
|
378
|
+
<details class="publish-queue-details" open>
|
|
379
|
+
<summary class="panel-head">
|
|
380
|
+
<h3><svg><use href="#i-export"></use></svg>What needs your approval?</h3>
|
|
246
381
|
<span class="chip" data-publish-queue-chip>Loading</span>
|
|
247
|
-
</
|
|
382
|
+
</summary>
|
|
248
383
|
<div class="panel-body grid">
|
|
249
384
|
<p class="note publish-status" data-publish-status role="status" aria-live="polite"></p>
|
|
250
385
|
<!-- rux1: forge-listing degraded strip. Shown ONLY when GET /api/changes
|
|
@@ -291,246 +426,470 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
291
426
|
</p>
|
|
292
427
|
)}
|
|
293
428
|
</div>
|
|
429
|
+
</details>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<!-- "See all" history: changes that LEFT the queue by being rejected. Rejecting
|
|
433
|
+
is otherwise a black hole — this collapsed panel keeps them visible (and their
|
|
434
|
+
preview reachable) so a reject is recoverable-from-history. Collapsed by
|
|
435
|
+
default (a reference, not a work surface); the whole panel is hidden by the
|
|
436
|
+
client when there are none, so it never adds empty chrome. Excludes shipped
|
|
437
|
+
(merged) changes — those live in the Live Site / versions panel. -->
|
|
438
|
+
<div class="panel" id="publish-closed" data-publish-closed-panel hidden>
|
|
439
|
+
<details class="closed-changes-details">
|
|
440
|
+
<summary class="panel-head">
|
|
441
|
+
<h3><svg><use href="#i-undo"></use></svg>Closed & rejected changes</h3>
|
|
442
|
+
<span class="chip" data-publish-closed-chip>0</span>
|
|
443
|
+
</summary>
|
|
444
|
+
<div class="panel-body grid">
|
|
445
|
+
<p class="note subtle">
|
|
446
|
+
Changes you rejected. They're not in your next release and won't go live —
|
|
447
|
+
kept here so nothing you closed silently disappears. Open a preview to review
|
|
448
|
+
what it was.
|
|
449
|
+
</p>
|
|
450
|
+
<div class="table-wrap">
|
|
451
|
+
<table class="publish-table">
|
|
452
|
+
<thead>
|
|
453
|
+
<tr>
|
|
454
|
+
<th>Change</th>
|
|
455
|
+
<th>Closed</th>
|
|
456
|
+
<th>Head</th>
|
|
457
|
+
</tr>
|
|
458
|
+
</thead>
|
|
459
|
+
<tbody data-publish-closed-body></tbody>
|
|
460
|
+
</table>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
</details>
|
|
294
464
|
</div>
|
|
295
465
|
|
|
296
466
|
<!-- b11: the AGGREGATE card — the protected `preview` aggregate the accepted
|
|
297
467
|
candidates integrate into, its combined evidence (the last integration
|
|
298
468
|
run's honest state), and the exact-digest Publish (b09/b10 ship the
|
|
299
469
|
reviewed pinned SHA + digest, never a rebuild). -->
|
|
300
|
-
<div class="panel" data-publish-aggregate>
|
|
301
|
-
<
|
|
302
|
-
|
|
470
|
+
<div class="panel agg-console" id="publish-next-release" data-publish-aggregate>
|
|
471
|
+
<details class="agg-console-details" open>
|
|
472
|
+
<summary class="panel-head">
|
|
473
|
+
<h3><svg><use href="#i-box"></use></svg>Next release</h3>
|
|
474
|
+
<!-- r11 — the human release name (aggregate.name), shown beside the heading.
|
|
475
|
+
Client-populated by renderAggregate; hidden (falls back to just "Next
|
|
476
|
+
release") when the release has no name. -->
|
|
477
|
+
<span class="agg-release-name" data-aggregate-name hidden></span>
|
|
478
|
+
<!-- One-click hop to the current preview build. Shown only when a
|
|
479
|
+
genuinely-built revision exists (renderAggregate toggles it via
|
|
480
|
+
classifyBuildLink), so it can never lead to a 404. The click is
|
|
481
|
+
stop-propagated in adminPublishTab.ts so opening the preview never
|
|
482
|
+
also toggles this collapsible. -->
|
|
483
|
+
<a class="btn btn-compact" data-aggregate-open-preview target="_blank" rel="noopener" hidden>Open preview</a>
|
|
484
|
+
<!-- r08 — copy a shareable, view-only preview link for the next release to hand
|
|
485
|
+
to a non-technical stakeholder (no sign-in). Unhidden by the client only
|
|
486
|
+
when a share token was minted; the click is stop-propagated so copying
|
|
487
|
+
never also toggles this collapsible. -->
|
|
488
|
+
<button class="btn btn-compact" type="button" data-aggregate-copy-preview hidden>
|
|
489
|
+
<svg><use href="#i-copy"></use></svg><span data-copy-preview-label>Copy preview link</span>
|
|
490
|
+
</button>
|
|
303
491
|
<span class="chip" data-aggregate-chip>Loading</span>
|
|
304
|
-
</
|
|
492
|
+
</summary>
|
|
305
493
|
<div class="panel-body grid">
|
|
306
494
|
<p class="note publish-status" data-aggregate-status role="status" aria-live="polite"></p>
|
|
307
495
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
<strong class="publish-mono" data-aggregate-sha>—</strong>
|
|
312
|
-
<p data-aggregate-run-link>The current green preview build for this store.</p>
|
|
313
|
-
</div>
|
|
314
|
-
<div class="field">
|
|
315
|
-
<label>Combined evidence</label>
|
|
316
|
-
<strong data-aggregate-evidence>—</strong>
|
|
317
|
-
<p data-aggregate-evidence-detail>The last integration run's verdict.</p>
|
|
318
|
-
</div>
|
|
319
|
-
<div class="field">
|
|
320
|
-
<label>Reviewed digest</label>
|
|
321
|
-
<strong class="publish-mono" data-aggregate-digest>—</strong>
|
|
322
|
-
<p>The content digest that will be published — exactly what you reviewed.</p>
|
|
323
|
-
</div>
|
|
324
|
-
</div>
|
|
325
|
-
|
|
326
|
-
<!-- Included PRs — the batch this aggregate composes (b01 includedPrs). -->
|
|
327
|
-
<div class="aggregate-prs" data-aggregate-prs-wrap hidden>
|
|
328
|
-
<b>Included changes</b>
|
|
329
|
-
<ul class="aggregate-prs-list" data-aggregate-prs></ul>
|
|
330
|
-
</div>
|
|
331
|
-
|
|
332
|
-
<!-- The exact-digest Publish plan (b09 plan()): pinned SHA + digest +
|
|
333
|
-
included PRs + rollback target + paywall verdict + run link. Shown
|
|
334
|
-
inline (from GET /api/changes `plan`) so the confirm never POSTs to
|
|
335
|
-
preview. Publish is refused until the plan is `ok`. -->
|
|
336
|
-
<div class="aggregate-plan" data-aggregate-plan hidden>
|
|
337
|
-
<div class="aggregate-plan-head">
|
|
338
|
-
<b>Publish plan</b>
|
|
339
|
-
<span class="chip" data-aggregate-plan-chip></span>
|
|
340
|
-
</div>
|
|
341
|
-
<dl class="aggregate-plan-grid" data-aggregate-plan-grid></dl>
|
|
342
|
-
<p class="note" data-aggregate-plan-note></p>
|
|
343
|
-
</div>
|
|
344
|
-
|
|
345
|
-
<div class="check-row">
|
|
496
|
+
<!-- Keep the primary action beside the release status. The detail table can
|
|
497
|
+
be long; publishing must never be buried beneath it. -->
|
|
498
|
+
<div class="check-row agg-launch">
|
|
346
499
|
<div>
|
|
347
|
-
<b>
|
|
500
|
+
<b>
|
|
501
|
+
{staticPublishEnabled
|
|
502
|
+
? "Publish this release to the hosted and public sites"
|
|
503
|
+
: "Publish this release to your live store"}
|
|
504
|
+
</b>
|
|
348
505
|
<p data-aggregate-publish-detail>
|
|
349
|
-
|
|
350
|
-
|
|
506
|
+
{publicSiteUrl ? (
|
|
507
|
+
<>
|
|
508
|
+
Publishes the exact build you reviewed to the hosted store, then
|
|
509
|
+
automatically republishes <code>{stripScheme(publicSiteUrl).replace(/\/$/, "")}</code>.
|
|
510
|
+
</>
|
|
511
|
+
) : (
|
|
512
|
+
<>Publishes the exact build you reviewed — no rebuild.</>
|
|
513
|
+
)}
|
|
351
514
|
</p>
|
|
352
515
|
</div>
|
|
353
|
-
<button class="btn primary" type="button" data-aggregate-publish disabled>
|
|
354
|
-
<svg><use href="#i-export"></use></svg>
|
|
516
|
+
<button class="btn primary agg-publish-btn" type="button" data-aggregate-publish disabled>
|
|
517
|
+
<svg><use href="#i-export"></use></svg>
|
|
518
|
+
{staticPublishEnabled ? "Publish to both live sites" : "Publish live"}
|
|
355
519
|
</button>
|
|
356
520
|
</div>
|
|
357
|
-
</div>
|
|
358
|
-
</div>
|
|
359
521
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
<thead>
|
|
373
|
-
<tr>
|
|
374
|
-
<th>Version</th>
|
|
375
|
-
<th>Digest</th>
|
|
376
|
-
<th>Promoted</th>
|
|
377
|
-
<th>Preview</th>
|
|
378
|
-
<th>State</th>
|
|
379
|
-
<th class="action">Actions</th>
|
|
380
|
-
</tr>
|
|
381
|
-
</thead>
|
|
382
|
-
<tbody data-versions-body>
|
|
383
|
-
<tr data-versions-placeholder>
|
|
384
|
-
<td colspan="6"><span class="subtle">Loading published versions…</span></td>
|
|
385
|
-
</tr>
|
|
386
|
-
</tbody>
|
|
387
|
-
</table>
|
|
522
|
+
<!-- Only visible after an actually failed aggregate rebuild. -->
|
|
523
|
+
<div class="check-row" data-aggregate-retry-row hidden>
|
|
524
|
+
<div>
|
|
525
|
+
<b>Retry the build</b>
|
|
526
|
+
<p>
|
|
527
|
+
Your change was added, but rebuilding the preview failed. Retry rebuilds it
|
|
528
|
+
and re-runs your store's checks — nothing new is added.
|
|
529
|
+
</p>
|
|
530
|
+
</div>
|
|
531
|
+
<button class="btn" type="button" data-aggregate-retry-build>
|
|
532
|
+
<svg><use href="#i-refresh"></use></svg>Retry build
|
|
533
|
+
</button>
|
|
388
534
|
</div>
|
|
389
|
-
</div>
|
|
390
|
-
</div>
|
|
391
535
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
<p class="note">
|
|
410
|
-
Cleans up candidate <b>branches</b> once their pull request is merged or
|
|
411
|
-
closed — a different action on a different object than <b>Retire</b> in
|
|
412
|
-
the queue above, which only evicts a hosted preview build (rebuildable
|
|
413
|
-
on demand, never a branch). Every classification below is a fresh
|
|
414
|
-
dry-run; deleting always re-checks eligibility on the server and asks
|
|
415
|
-
you to confirm before removing anything. Orphan branches (no PR record
|
|
416
|
-
at all) are quarantined for your review, never deleted automatically.
|
|
417
|
-
</p>
|
|
418
|
-
<p class="note publish-status" data-branches-status role="status" aria-live="polite"></p>
|
|
419
|
-
<div class="table-wrap">
|
|
420
|
-
<table class="publish-table">
|
|
421
|
-
<thead>
|
|
422
|
-
<tr>
|
|
423
|
-
<th>Branch</th>
|
|
424
|
-
<th>Classification</th>
|
|
425
|
-
<th>PR</th>
|
|
426
|
-
<th>Tip commit</th>
|
|
427
|
-
<th>Age</th>
|
|
428
|
-
<th>Evidence</th>
|
|
429
|
-
<th class="action">Actions</th>
|
|
430
|
-
</tr>
|
|
431
|
-
</thead>
|
|
432
|
-
<tbody data-branches-body>
|
|
433
|
-
<tr data-branches-placeholder>
|
|
434
|
-
<td colspan="7"><span class="subtle">Loading branch classification…</span></td>
|
|
435
|
-
</tr>
|
|
436
|
-
</tbody>
|
|
437
|
-
</table>
|
|
536
|
+
<!-- AI-assembled release summary — a plain-language "what am I about to
|
|
537
|
+
publish?" ahead of the technical trust chain. Rendered ONLY when the
|
|
538
|
+
server supplies data.releaseSummary; otherwise it stays hidden and the
|
|
539
|
+
panel degrades to today's behavior (graceful fallback). -->
|
|
540
|
+
<div class="agg-summary" data-aggregate-summary hidden>
|
|
541
|
+
<div class="agg-summary-head">
|
|
542
|
+
<span class="agg-summary-glyph" aria-hidden="true">
|
|
543
|
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l1.9 4.7L18.5 8.6l-4.6 1.9L12 15.2l-1.9-4.7L5.5 8.6l4.6-1.9L12 2z"></path><path d="M18.5 14l.9 2.2 2.3.9-2.3.9-.9 2.2-.9-2.2-2.3-.9 2.3-.9.9-2.2z"></path></svg>
|
|
544
|
+
</span>
|
|
545
|
+
<span class="agg-summary-label">What’s in this release</span>
|
|
546
|
+
<span class="agg-ai-chip">
|
|
547
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l1.9 4.7L18.5 8.6l-4.6 1.9L12 15.2l-1.9-4.7L5.5 8.6l4.6-1.9L12 2z"></path></svg>
|
|
548
|
+
AI summary
|
|
549
|
+
</span>
|
|
550
|
+
</div>
|
|
551
|
+
<p class="agg-summary-lead" data-aggregate-summary-lead></p>
|
|
552
|
+
<ul class="agg-summary-points" data-aggregate-summary-points></ul>
|
|
438
553
|
</div>
|
|
439
|
-
</div>
|
|
440
|
-
</div>
|
|
441
554
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
<div class="
|
|
450
|
-
<div class="
|
|
451
|
-
<label>
|
|
452
|
-
<strong data-
|
|
453
|
-
<p data-
|
|
555
|
+
<!-- Chain of custody: the build that was verified → its combined evidence →
|
|
556
|
+
the digest that ships. Collapsed by default — this is verification
|
|
557
|
+
detail for a developer, not something a merchant needs to read to
|
|
558
|
+
decide whether to publish; the plain-language summary above and the
|
|
559
|
+
Publish button below are the actual decision surface. -->
|
|
560
|
+
<details class="agg-chain-details">
|
|
561
|
+
<summary>Technical details</summary>
|
|
562
|
+
<div class="agg-chain">
|
|
563
|
+
<div class="agg-stop">
|
|
564
|
+
<span class="agg-stop-label">Aggregate build</span>
|
|
565
|
+
<span class="agg-stop-val"><strong class="publish-mono" data-aggregate-sha>—</strong></span>
|
|
566
|
+
<p class="agg-stop-cap" data-aggregate-run-link>The current green preview build for this store.</p>
|
|
567
|
+
</div>
|
|
568
|
+
<div class="agg-link" aria-hidden="true">
|
|
569
|
+
<span class="agg-flowlabel">verified by</span>
|
|
570
|
+
<span class="agg-arrow"><svg viewBox="0 0 34 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M1 5h29m0 0-4-4m4 4-4 4"></path></svg></span>
|
|
454
571
|
</div>
|
|
455
|
-
<div class="
|
|
456
|
-
<label>
|
|
457
|
-
<strong data-
|
|
458
|
-
<p
|
|
572
|
+
<div class="agg-stop">
|
|
573
|
+
<span class="agg-stop-label">Combined evidence</span>
|
|
574
|
+
<span class="agg-stop-val"><strong data-aggregate-evidence>—</strong></span>
|
|
575
|
+
<p class="agg-stop-cap" data-aggregate-evidence-detail>The last integration run's verdict.</p>
|
|
459
576
|
</div>
|
|
460
|
-
<div class="
|
|
461
|
-
<
|
|
462
|
-
<
|
|
463
|
-
|
|
577
|
+
<div class="agg-link" aria-hidden="true">
|
|
578
|
+
<span class="agg-flowlabel">produces</span>
|
|
579
|
+
<span class="agg-arrow"><svg viewBox="0 0 34 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M1 5h29m0 0-4-4m4 4-4 4"></path></svg></span>
|
|
580
|
+
</div>
|
|
581
|
+
<div class="agg-stop">
|
|
582
|
+
<span class="agg-stop-label">Reviewed digest</span>
|
|
583
|
+
<span class="agg-stop-val"><strong class="publish-mono agg-digest" data-aggregate-digest>—</strong></span>
|
|
584
|
+
<p class="agg-stop-cap">The content digest that will be published — exactly what you reviewed.</p>
|
|
464
585
|
</div>
|
|
465
586
|
</div>
|
|
587
|
+
</details>
|
|
588
|
+
|
|
589
|
+
<!-- Included PRs — the batch this aggregate composes (b01 includedPrs). u14:
|
|
590
|
+
the "missing middle" — this is the STAGED delta vs live. The title + note
|
|
591
|
+
narrate how far ahead of the live site the staged preview is, and each row
|
|
592
|
+
carries an Undo (per-change revert) to roll the staged preview backward. -->
|
|
593
|
+
<div class="aggregate-prs agg-staged" data-aggregate-prs-wrap hidden>
|
|
594
|
+
<div class="agg-staged-head">
|
|
595
|
+
<b data-aggregate-prs-title>Included changes</b>
|
|
596
|
+
<!-- Scope-of-change risk measure across the whole staged batch (total
|
|
597
|
+
files + lines). Populated by renderAggregate from data.stagedScope;
|
|
598
|
+
hidden when no staged change carries size stats. -->
|
|
599
|
+
<span class="agg-scope" data-aggregate-scope hidden></span>
|
|
600
|
+
</div>
|
|
601
|
+
<!-- Staged → live delta strip: staged aggregate sha ahead of the live
|
|
602
|
+
pointer. Populated by renderAggregate; hidden when nothing is ahead. -->
|
|
603
|
+
<div class="agg-delta-strip" data-aggregate-delta-strip hidden>
|
|
604
|
+
<span class="agg-delta-node">
|
|
605
|
+
<span class="agg-stop-label">Staged preview</span>
|
|
606
|
+
<span class="sha-token" data-agg-delta-staged>—</span>
|
|
607
|
+
</span>
|
|
608
|
+
<span class="agg-delta-mid" aria-hidden="true">
|
|
609
|
+
<span class="agg-delta-count" data-agg-delta-count></span>
|
|
610
|
+
<svg viewBox="0 0 26 10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M1 5h21m0 0-4-4m4 4-4 4"></path></svg>
|
|
611
|
+
</span>
|
|
612
|
+
<span class="agg-delta-node">
|
|
613
|
+
<span class="agg-stop-label">Live site</span>
|
|
614
|
+
<span class="sha-token ghost" data-agg-delta-live>—</span>
|
|
615
|
+
</span>
|
|
616
|
+
</div>
|
|
617
|
+
<p class="note" data-aggregate-delta-note hidden></p>
|
|
618
|
+
<!-- ONE table, one row per change — merges what used to be two separate
|
|
619
|
+
"Included changes" lists (this staged-delta section + the Publish
|
|
620
|
+
plan's own duplicate) into a single source of truth. Populated by
|
|
621
|
+
renderAggregate, enriched with the plan's per-change title/description/
|
|
622
|
+
file+line counts when available. -->
|
|
623
|
+
<div class="table-wrap">
|
|
624
|
+
<table class="publish-table agg-included-table">
|
|
625
|
+
<thead>
|
|
626
|
+
<tr>
|
|
627
|
+
<th>Change</th>
|
|
628
|
+
<th>Head</th>
|
|
629
|
+
<th>Files</th>
|
|
630
|
+
<th>Lines</th>
|
|
631
|
+
<th>Added</th>
|
|
632
|
+
<th class="action">Actions</th>
|
|
633
|
+
</tr>
|
|
634
|
+
</thead>
|
|
635
|
+
<tbody data-aggregate-prs></tbody>
|
|
636
|
+
</table>
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
466
639
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
</div>
|
|
477
|
-
<ul class="domain-readiness-list" data-domain-readiness-list></ul>
|
|
478
|
-
<p class="note" data-domain-readiness-note></p>
|
|
640
|
+
<!-- The exact-digest Publish plan (b09 plan()): pinned SHA + digest +
|
|
641
|
+
rollback target + paywall verdict + run link. Shown inline (from
|
|
642
|
+
GET /api/changes `plan`) so the confirm never POSTs to preview.
|
|
643
|
+
Publish is refused until the plan is `ok`. Its own included-changes
|
|
644
|
+
list was merged into the single table above — see data-aggregate-prs. -->
|
|
645
|
+
<div class="aggregate-plan agg-manifest" data-aggregate-plan hidden>
|
|
646
|
+
<div class="aggregate-plan-head agg-manifest-head">
|
|
647
|
+
<b><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 11l3 3L22 4"></path><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path></svg>Publish plan</b>
|
|
648
|
+
<span class="chip" data-aggregate-plan-chip></span>
|
|
479
649
|
</div>
|
|
650
|
+
<details class="agg-chain-details">
|
|
651
|
+
<summary>Technical details</summary>
|
|
652
|
+
<dl class="aggregate-plan-grid" data-aggregate-plan-grid></dl>
|
|
653
|
+
</details>
|
|
480
654
|
|
|
481
|
-
<
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
</
|
|
655
|
+
<p class="note" data-aggregate-plan-note></p>
|
|
656
|
+
</div>
|
|
657
|
+
|
|
658
|
+
</div>
|
|
659
|
+
</details>
|
|
660
|
+
</div>
|
|
661
|
+
|
|
662
|
+
<!-- Branch retention panel removed entirely — old candidate branches now
|
|
663
|
+
clean themselves up automatically (auto-swept after each successful
|
|
664
|
+
accept/publish, see adminPublishTab.ts autoSweepBranches). There is
|
|
665
|
+
nothing here for anyone to look at or act on day-to-day, so there is no
|
|
666
|
+
panel at all, not even a collapsed one. -->
|
|
667
|
+
|
|
668
|
+
<!-- Publish next release — the collapsed destination/status panel below the
|
|
669
|
+
staged release. Its summary answers the operational question first: either
|
|
670
|
+
live already matches the staged release, or there is an action to advance it.
|
|
671
|
+
The disclosure contains destination links, freshness, rollback, and domain
|
|
672
|
+
controls for operators who need the detail. -->
|
|
673
|
+
<div class="panel" id="publish-live-site" data-publish-live-site>
|
|
674
|
+
<details class="live-site-details">
|
|
675
|
+
<summary class="panel-head">
|
|
676
|
+
<h3><svg><use href="#i-export"></use></svg>Publish next release</h3>
|
|
677
|
+
<span class="chip" data-live-release-state>Checking release…</span>
|
|
678
|
+
<button
|
|
679
|
+
class="btn primary btn-compact live-release-publish"
|
|
680
|
+
type="button"
|
|
681
|
+
data-live-release-publish
|
|
682
|
+
hidden
|
|
683
|
+
>Publish next release</button>
|
|
684
|
+
</summary>
|
|
685
|
+
<div class="panel-body grid">
|
|
686
|
+
<ul class="live-site-links">
|
|
687
|
+
<li>
|
|
688
|
+
<span class="live-site-link-label">Hosted store</span>
|
|
689
|
+
<a class="live-site-link-url publish-mono" href={liveHref} target="_blank" rel="noopener">{stripScheme(liveStoreUrl)}</a>
|
|
690
|
+
<span class="live-site-link-note">Updates immediately when you publish the reviewed release above.</span>
|
|
691
|
+
</li>
|
|
692
|
+
{publicSiteUrl && (
|
|
693
|
+
<li>
|
|
694
|
+
<span class="live-site-link-label">Public site</span>
|
|
695
|
+
<a class="live-site-link-url publish-mono" href={publicSiteUrl} target="_blank" rel="noopener">{stripScheme(publicSiteUrl)}</a>
|
|
696
|
+
<span class="live-site-link-note">Republishes automatically after the hosted release is published.</span>
|
|
697
|
+
</li>
|
|
698
|
+
)}
|
|
699
|
+
</ul>
|
|
700
|
+
|
|
701
|
+
<!-- Public-site freshness read-back (static-publish seam's staticLiveDigest):
|
|
702
|
+
what the external target (CloudFront/S3) is HONESTLY observed serving
|
|
703
|
+
right now, vs the control-plane's live pointer — the two can diverge on a
|
|
704
|
+
stale/failed republish. Answers "did going live actually reach the public
|
|
705
|
+
site?" without a manual curl. -->
|
|
706
|
+
{staticPublishEnabled && publicSiteDigest && publicSiteVerdict && (
|
|
707
|
+
<div
|
|
708
|
+
class={`risk-alert ${publicSiteVerdict.status === "current" ? "public-site-ok" : "amber"}`}
|
|
709
|
+
data-publish-public-site
|
|
710
|
+
>
|
|
711
|
+
<div class="risk-alert-main">
|
|
712
|
+
<svg><use href={publicSiteVerdict.status === "current" ? "#i-check" : "#i-alert"}></use></svg>
|
|
713
|
+
<div>
|
|
714
|
+
{publicSiteVerdict.status === "current" ? (
|
|
715
|
+
<>
|
|
716
|
+
<b>Public site matches the currently published release</b>
|
|
717
|
+
<span>
|
|
718
|
+
<code>{stripScheme(publicSiteUrl ?? tenant.appDomain).replace(/\/$/, "")}</code> is serving
|
|
719
|
+
(<code class="publish-mono">{liveVersion?.versionId?.slice(0, 12)}</code>)
|
|
720
|
+
— the same release as the hosted store. Staged preview changes are
|
|
721
|
+
a separate next release: use <b>Publish to both live sites</b> above
|
|
722
|
+
to advance the hosted store and republish this public site.
|
|
723
|
+
</span>
|
|
724
|
+
</>
|
|
725
|
+
) : publicSiteVerdict.status === "behind" ? (
|
|
726
|
+
<>
|
|
727
|
+
<b>Public site is behind the hosted store's live pointer</b>
|
|
728
|
+
<span>
|
|
729
|
+
<code>{tenant.appDomain}</code>'s public target is still serving{" "}
|
|
730
|
+
<code class="publish-mono">{publicSiteVerdict.publishedSha.slice(0, 12)}</code>, but
|
|
731
|
+
the hosted store's live pointer has since moved to{" "}
|
|
732
|
+
<code class="publish-mono">{publicSiteVerdict.livePointerSha.slice(0, 12)}</code>.
|
|
733
|
+
The republish job hasn't caught up yet (or a go-live happened before it was enabled) —
|
|
734
|
+
check the workflow run, or trigger a fresh go-live to re-dispatch it.
|
|
735
|
+
</span>
|
|
736
|
+
</>
|
|
737
|
+
) : (
|
|
738
|
+
<>
|
|
739
|
+
<b>Couldn't verify the public site is current</b>
|
|
740
|
+
<span>
|
|
741
|
+
Checked <code class="publish-mono">{publicSiteDigest.url}</code>: {publicSiteVerdict.reason}.
|
|
742
|
+
This does NOT mean the hosted store's own "Live version" above is wrong, only that the
|
|
743
|
+
separate public static target couldn't be confirmed current.
|
|
744
|
+
</span>
|
|
745
|
+
</>
|
|
746
|
+
)}
|
|
747
|
+
</div>
|
|
748
|
+
</div>
|
|
516
749
|
</div>
|
|
750
|
+
)}
|
|
751
|
+
|
|
752
|
+
<!-- Rollback options — CONTAINED within Live Site as a subsection (was a sibling
|
|
753
|
+
panel). The full published-version history + one-click rollback through the
|
|
754
|
+
single rollback orchestrator; collapsed by default (most people never open
|
|
755
|
+
it — the live version is already the metric up top). Keeps its
|
|
756
|
+
data-publish-versions + data-versions-* hooks verbatim. -->
|
|
757
|
+
<div class="live-site-subsection" data-publish-versions>
|
|
758
|
+
<details class="versions-details">
|
|
759
|
+
<summary class="live-site-subhead">
|
|
760
|
+
<h4><svg><use href="#i-refresh"></use></svg>Rollback options</h4>
|
|
761
|
+
<span class="chip" data-versions-chip>Loading</span>
|
|
762
|
+
</summary>
|
|
763
|
+
<div class="grid">
|
|
764
|
+
<p class="note publish-status" data-versions-status role="status" aria-live="polite"></p>
|
|
765
|
+
<p class="note" data-versions-rollback-note hidden></p>
|
|
766
|
+
<!-- u14: orients the owner between the staged aggregate above and live versions
|
|
767
|
+
below — "N staged change(s) not yet published; Publish live creates the next
|
|
768
|
+
version here." Owned by renderAggregate (its own element, so its timing never
|
|
769
|
+
races renderVersions). -->
|
|
770
|
+
<p class="note" data-versions-orientation hidden></p>
|
|
771
|
+
<div class="table-wrap">
|
|
772
|
+
<table class="publish-table versions-table">
|
|
773
|
+
<thead>
|
|
774
|
+
<tr>
|
|
775
|
+
<th>Version</th>
|
|
776
|
+
<th>Digest</th>
|
|
777
|
+
<th>Promoted</th>
|
|
778
|
+
<th>Preview</th>
|
|
779
|
+
<th>State</th>
|
|
780
|
+
<th class="action">Actions</th>
|
|
781
|
+
</tr>
|
|
782
|
+
</thead>
|
|
783
|
+
<tbody data-versions-body>
|
|
784
|
+
<tr data-versions-placeholder>
|
|
785
|
+
<td colspan="6"><span class="subtle">Loading published versions…</span></td>
|
|
786
|
+
</tr>
|
|
787
|
+
</tbody>
|
|
788
|
+
</table>
|
|
789
|
+
</div>
|
|
790
|
+
</div>
|
|
791
|
+
</details>
|
|
517
792
|
</div>
|
|
518
793
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
794
|
+
<!-- Public domain (apex connect / rollback) — CONTAINED within Live Site as a
|
|
795
|
+
subsection. Keeps its data-publish-domain + data-domain-* hooks verbatim so
|
|
796
|
+
the client controller is unchanged; only the wrapper/heading are lighter to
|
|
797
|
+
read as a nested section rather than a sibling panel. -->
|
|
798
|
+
{staticPublishEnabled && (
|
|
799
|
+
<div class="live-site-subsection" data-publish-domain>
|
|
800
|
+
<div class="live-site-subhead">
|
|
801
|
+
<h4><svg><use href="#i-shield"></use></svg>Public domain</h4>
|
|
802
|
+
<span class="chip" data-domain-state-chip>Loading</span>
|
|
803
|
+
</div>
|
|
804
|
+
<div class="grid">
|
|
805
|
+
<div class="grid cols-3">
|
|
806
|
+
<div class="field">
|
|
807
|
+
<label>Serving from</label>
|
|
808
|
+
<strong data-domain-state>—</strong>
|
|
809
|
+
<p data-domain-since>Current host for {tenant.appDomain}</p>
|
|
810
|
+
</div>
|
|
811
|
+
<div class="field">
|
|
812
|
+
<label>Rollback records</label>
|
|
813
|
+
<strong data-domain-capture>None captured</strong>
|
|
814
|
+
<p>DNS records captured before connect — rollback restores these.</p>
|
|
815
|
+
</div>
|
|
816
|
+
<div class="field">
|
|
817
|
+
<label>Last run</label>
|
|
818
|
+
<strong data-domain-run>None</strong>
|
|
819
|
+
<p data-domain-run-detail>Connect and rollback runs appear here.</p>
|
|
820
|
+
</div>
|
|
821
|
+
</div>
|
|
822
|
+
|
|
823
|
+
<p class="note publish-status" data-domain-status role="status" aria-live="polite"></p>
|
|
824
|
+
|
|
825
|
+
<!-- u9: apex-cutover readiness — the itemized preconditions the server gate
|
|
826
|
+
requires before a connect is permitted. Populated from
|
|
827
|
+
/api/domain/readiness (the SAME gate `tot go-live` reads). -->
|
|
828
|
+
<div class="domain-readiness" data-domain-readiness hidden>
|
|
829
|
+
<div class="domain-readiness-head">
|
|
830
|
+
<b>Cutover readiness</b>
|
|
831
|
+
<span class="chip" data-domain-readiness-chip>Checking…</span>
|
|
832
|
+
</div>
|
|
833
|
+
<ul class="domain-readiness-list" data-domain-readiness-list></ul>
|
|
834
|
+
<p class="note" data-domain-readiness-note></p>
|
|
835
|
+
</div>
|
|
836
|
+
|
|
837
|
+
<div class="check-row">
|
|
838
|
+
<div>
|
|
839
|
+
<b>Connect {tenant.appDomain} to the storefront</b>
|
|
840
|
+
<p>
|
|
841
|
+
Updates two DNS names — <code>{tenant.appDomain}</code> and <code>www.{tenant.appDomain}</code> —
|
|
842
|
+
to serve the published site. Takes effect in about a minute. Type the
|
|
843
|
+
domain to confirm.
|
|
844
|
+
</p>
|
|
845
|
+
{!viewerCanApexCutover && (
|
|
846
|
+
<p><span class="chip amber">Owner/admin only</span> Connecting the domain requires an owner or admin sign-in.</p>
|
|
847
|
+
)}
|
|
848
|
+
</div>
|
|
849
|
+
<div class="domain-connect-controls">
|
|
850
|
+
<input
|
|
851
|
+
type="text"
|
|
852
|
+
placeholder={tenant.appDomain}
|
|
853
|
+
autocomplete="off"
|
|
854
|
+
spellcheck="false"
|
|
855
|
+
aria-label="Type the domain to confirm"
|
|
856
|
+
data-domain-confirm
|
|
857
|
+
disabled={!viewerCanApexCutover}
|
|
858
|
+
/>
|
|
859
|
+
<label class="domain-rehearsal">
|
|
860
|
+
<input type="checkbox" data-domain-rehearsal disabled={!viewerCanApexCutover} />
|
|
861
|
+
Rehearsal (dry run, no DNS change)
|
|
862
|
+
</label>
|
|
863
|
+
<button
|
|
864
|
+
class="btn primary"
|
|
865
|
+
type="button"
|
|
866
|
+
data-domain-connect
|
|
867
|
+
data-owner-locked={!viewerCanApexCutover ? "true" : undefined}
|
|
868
|
+
disabled={!viewerCanApexCutover}
|
|
869
|
+
>
|
|
870
|
+
<svg><use href="#i-check"></use></svg>Connect domain
|
|
871
|
+
</button>
|
|
872
|
+
</div>
|
|
873
|
+
</div>
|
|
874
|
+
|
|
875
|
+
<div class="check-row">
|
|
876
|
+
<div>
|
|
877
|
+
<b>Roll back to the previous host</b>
|
|
878
|
+
<p data-domain-rollback-detail>
|
|
879
|
+
Restores the DNS records captured before connect. Available once records
|
|
880
|
+
have been captured.
|
|
881
|
+
</p>
|
|
882
|
+
</div>
|
|
883
|
+
<button class="btn" type="button" data-domain-rollback disabled={!viewerCanShip}>
|
|
884
|
+
<svg><use href="#i-refresh"></use></svg>Roll back
|
|
885
|
+
</button>
|
|
886
|
+
</div>
|
|
887
|
+
</div>
|
|
526
888
|
</div>
|
|
527
|
-
|
|
528
|
-
<svg><use href="#i-refresh"></use></svg>Roll back
|
|
529
|
-
</button>
|
|
530
|
-
</div>
|
|
889
|
+
)}
|
|
531
890
|
</div>
|
|
532
|
-
</
|
|
533
|
-
|
|
891
|
+
</details>
|
|
892
|
+
</div>
|
|
534
893
|
|
|
535
894
|
<!-- Accept-to-preview LIFTOFF — the plan confirm + live integration flight +
|
|
536
895
|
outcome ceremony. One dialog, three phases (plan → flight → outcome).
|
|
@@ -540,44 +899,45 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
540
899
|
<div class="liftoff-backdrop" data-liftoff hidden>
|
|
541
900
|
<div class="liftoff-card" role="dialog" aria-modal="true" aria-labelledby="liftoff-title" tabindex="-1">
|
|
542
901
|
<div class="liftoff-phase" data-liftoff-plan>
|
|
543
|
-
<p class="liftoff-kicker">
|
|
544
|
-
<h3 class="liftoff-title" id="liftoff-title" data-liftoff-title>
|
|
902
|
+
<p class="liftoff-kicker">Add to release</p>
|
|
903
|
+
<h3 class="liftoff-title" id="liftoff-title" data-liftoff-title>Add to your next release</h3>
|
|
545
904
|
<dl class="liftoff-grid" data-liftoff-grid></dl>
|
|
546
905
|
<p class="note">
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
906
|
+
Adds this change to your next release, then rebuilds the preview and re-runs your
|
|
907
|
+
store's checks. <b>Nothing goes live</b> — publishing your release stays a
|
|
908
|
+
separate, explicit step.
|
|
550
909
|
</p>
|
|
551
910
|
<div class="liftoff-actions">
|
|
552
911
|
<button class="btn" type="button" data-liftoff-cancel>Cancel</button>
|
|
553
912
|
<button class="btn primary" type="button" data-liftoff-proceed>
|
|
554
|
-
<svg><use href="#i-check"></use></svg>
|
|
913
|
+
<svg><use href="#i-check"></use></svg>Add to release
|
|
555
914
|
</button>
|
|
556
915
|
</div>
|
|
557
916
|
</div>
|
|
558
917
|
|
|
559
918
|
<div class="liftoff-phase" data-liftoff-flight hidden>
|
|
560
|
-
<p class="liftoff-kicker">
|
|
561
|
-
<h3 class="liftoff-title" data-liftoff-flight-title>
|
|
919
|
+
<p class="liftoff-kicker">Adding to your release</p>
|
|
920
|
+
<h3 class="liftoff-title" data-liftoff-flight-title>Adding…</h3>
|
|
562
921
|
<ol class="liftoff-stages">
|
|
563
922
|
<li data-liftoff-stage="merge">
|
|
564
923
|
<span class="liftoff-dot"></span>
|
|
565
|
-
<div><b>
|
|
924
|
+
<div><b>Add</b><span>add this change to your next release</span></div>
|
|
566
925
|
</li>
|
|
567
926
|
<li data-liftoff-stage="build">
|
|
568
927
|
<span class="liftoff-dot"></span>
|
|
569
|
-
<div><b>
|
|
928
|
+
<div><b>Build</b><span>rebuild the preview with your change in it</span></div>
|
|
570
929
|
</li>
|
|
571
930
|
<li data-liftoff-stage="evidence">
|
|
572
931
|
<span class="liftoff-dot"></span>
|
|
573
|
-
<div><b>
|
|
932
|
+
<div><b>Check</b><span>re-run your store's checks on the preview</span></div>
|
|
574
933
|
</li>
|
|
575
934
|
<li data-liftoff-stage="green">
|
|
576
935
|
<span class="liftoff-dot"></span>
|
|
577
|
-
<div><b>
|
|
936
|
+
<div><b>Ready</b><span>your next release is ready to review and publish</span></div>
|
|
578
937
|
</li>
|
|
579
938
|
</ol>
|
|
580
939
|
<p class="liftoff-live" data-liftoff-live role="status" aria-live="polite">Working…</p>
|
|
940
|
+
<div class="stream-log" data-liftoff-log role="log" aria-live="polite"></div>
|
|
581
941
|
<div class="liftoff-meta">
|
|
582
942
|
<span class="publish-mono" data-liftoff-elapsed>0:00</span>
|
|
583
943
|
<button class="btn" type="button" data-liftoff-hide>Continue in background</button>
|
|
@@ -588,8 +948,81 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
588
948
|
<div class="liftoff-seal" data-liftoff-seal aria-hidden="true"></div>
|
|
589
949
|
<h3 class="liftoff-title" data-liftoff-outcome-title></h3>
|
|
590
950
|
<p class="note" data-liftoff-outcome-detail></p>
|
|
951
|
+
<!-- Merge-conflict recovery (shown ONLY for reason "merge_failed") — decision
|
|
952
|
+
`automate-first-conflict-recovery` (u4): the refusal LEADS with one primary
|
|
953
|
+
action ("Update this change automatically" → /resolve strategy:"merge",
|
|
954
|
+
then auto-re-integrate). The three option cards appear only when diff3 hits
|
|
955
|
+
a genuine same-line overlap (RefreshResult.unresolved); the old terminal +
|
|
956
|
+
AI-agent guidance is demoted VERBATIM into the collapsed "Do it manually
|
|
957
|
+
instead" expert section (nothing removed). Decision logic lives in the
|
|
958
|
+
tested `resolveConflict.ts`; this glue mirrors it (verified on preview). -->
|
|
959
|
+
<div class="liftoff-conflict" data-liftoff-conflict hidden>
|
|
960
|
+
<p class="note liftoff-conflict-summary" data-conflict-summary hidden></p>
|
|
961
|
+
|
|
962
|
+
<!-- PRIMARY ACTION — always first when a candidate is refused not-mergeable. -->
|
|
963
|
+
<div class="liftoff-conflict-primary" data-conflict-primary>
|
|
964
|
+
<p class="note" data-conflict-primary-note>
|
|
965
|
+
We can rebuild your change on top of the store's current version and
|
|
966
|
+
re-integrate it for you — no terminal, no re-submit.
|
|
967
|
+
</p>
|
|
968
|
+
<button class="btn primary" type="button" data-conflict-auto>Update this change automatically</button>
|
|
969
|
+
<p class="note liftoff-conflict-primary-status" data-conflict-primary-status role="status" aria-live="polite" hidden></p>
|
|
970
|
+
</div>
|
|
971
|
+
|
|
972
|
+
<!-- OPTION CARDS — revealed only on a genuine same-line overlap diff3 can't
|
|
973
|
+
safely auto-pick. Static nodes mirror CONFLICT_OPTION_CARDS. -->
|
|
974
|
+
<div class="liftoff-conflict-cards" data-conflict-cards hidden>
|
|
975
|
+
<p class="note" data-conflict-cards-intro>
|
|
976
|
+
Some of the same lines were changed on both sides, so there's no single safe
|
|
977
|
+
answer. Pick how to resolve the overlap:
|
|
978
|
+
</p>
|
|
979
|
+
<ul class="liftoff-conflict-files publish-mono" data-conflict-files></ul>
|
|
980
|
+
<div class="liftoff-conflict-card-grid" role="group" aria-label="Resolve the overlap">
|
|
981
|
+
<button class="liftoff-option-card" type="button" data-conflict-card data-strategy="merge" data-recommended="1">
|
|
982
|
+
<b>Combine both</b>
|
|
983
|
+
<span>Recommended — weave both sets of edits together</span>
|
|
984
|
+
</button>
|
|
985
|
+
<button class="liftoff-option-card" type="button" data-conflict-card data-strategy="ours">
|
|
986
|
+
<b>Keep my change</b>
|
|
987
|
+
<span>Use your edits on the conflicting lines</span>
|
|
988
|
+
</button>
|
|
989
|
+
<button class="liftoff-option-card" type="button" data-conflict-card data-strategy="theirs">
|
|
990
|
+
<b>Keep the store's current version</b>
|
|
991
|
+
<span>Use the store's edits on the conflicting lines</span>
|
|
992
|
+
</button>
|
|
993
|
+
</div>
|
|
994
|
+
<p class="note liftoff-conflict-cards-status" data-conflict-cards-status role="status" aria-live="polite" hidden></p>
|
|
995
|
+
</div>
|
|
996
|
+
|
|
997
|
+
<!-- EXPERT FALLBACK — the ORIGINAL terminal + AI-agent guidance, verbatim,
|
|
998
|
+
collapsed. Nothing deleted; just demoted from primary to fallback. -->
|
|
999
|
+
<details class="liftoff-conflict-expert" data-conflict-expert>
|
|
1000
|
+
<summary>Do it manually instead</summary>
|
|
1001
|
+
<p class="note" data-conflict-intro>
|
|
1002
|
+
From the store checkout that submitted this change, bring it up to date and
|
|
1003
|
+
re-submit — the open PR updates in place. Once the new build is ready,
|
|
1004
|
+
finish here with <b>Add to release</b>, or from the terminal with
|
|
1005
|
+
<code>tot accept --tenant <store> --pr <N></code>.
|
|
1006
|
+
Check <code>git status</code> first — abort any half-finished rebase from an
|
|
1007
|
+
earlier attempt. Then <code>tot pr</code>: this change should be marked
|
|
1008
|
+
<code>(active)</code>, proving you're in the right checkout and branch.
|
|
1009
|
+
If git reports an authentication failure, run <code>tot login</code> and retry.
|
|
1010
|
+
</p>
|
|
1011
|
+
<pre class="liftoff-commands publish-mono" data-conflict-commands></pre>
|
|
1012
|
+
<div class="liftoff-conflict-actions">
|
|
1013
|
+
<button class="btn" type="button" data-conflict-copy-prompt>Copy prompt for your AI agent</button>
|
|
1014
|
+
<button class="btn" type="button" data-conflict-copy>Copy commands</button>
|
|
1015
|
+
</div>
|
|
1016
|
+
<p class="liftoff-conflict-raw publish-mono" data-conflict-raw></p>
|
|
1017
|
+
</details>
|
|
1018
|
+
</div>
|
|
591
1019
|
<div class="liftoff-actions">
|
|
592
1020
|
<a class="btn" data-liftoff-review target="_blank" rel="noopener" hidden>Review the preview build</a>
|
|
1021
|
+
<!-- Fail-visible next step: when the merge LANDED but the preview build
|
|
1022
|
+
failed, this offers Rebuild (not a dead "did not land"); on an
|
|
1023
|
+
indeterminate outcome it offers Re-check. Hidden on success/conflict
|
|
1024
|
+
(conflict recovery owns its own primary action). -->
|
|
1025
|
+
<button class="btn primary" type="button" data-liftoff-rebuild hidden>Rebuild the preview build</button>
|
|
593
1026
|
<button class="btn primary" type="button" data-liftoff-done>Done</button>
|
|
594
1027
|
</div>
|
|
595
1028
|
</div>
|
|
@@ -628,8 +1061,10 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
628
1061
|
<p class="liftoff-live" data-shipoff-live role="status" aria-live="polite">
|
|
629
1062
|
Pinning the exact reviewed build to your live channel…
|
|
630
1063
|
</p>
|
|
1064
|
+
<div class="stream-log" data-shipoff-log role="log" aria-live="polite"></div>
|
|
631
1065
|
<div class="liftoff-meta">
|
|
632
1066
|
<span class="publish-mono" data-shipoff-elapsed>0:00</span>
|
|
1067
|
+
<button class="btn" type="button" data-shipoff-hide>Continue in background</button>
|
|
633
1068
|
</div>
|
|
634
1069
|
</div>
|
|
635
1070
|
|
|
@@ -637,8 +1072,23 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
637
1072
|
<div class="liftoff-seal" data-shipoff-seal aria-hidden="true"></div>
|
|
638
1073
|
<h3 class="liftoff-title" data-shipoff-outcome-title></h3>
|
|
639
1074
|
<p class="note" data-shipoff-outcome-detail></p>
|
|
1075
|
+
<!-- Failure detail + "Copy for your AI agent" — shown ONLY when the publish did
|
|
1076
|
+
not complete. Mirrors the build-flight failure affordance. -->
|
|
1077
|
+
<div class="liftoff-conflict" data-shipoff-failure hidden>
|
|
1078
|
+
<pre class="liftoff-commands publish-mono" data-shipoff-failure-detail></pre>
|
|
1079
|
+
<div class="liftoff-conflict-actions">
|
|
1080
|
+
<button class="btn" type="button" data-shipoff-copy>Copy for your AI agent</button>
|
|
1081
|
+
</div>
|
|
1082
|
+
</div>
|
|
640
1083
|
<div class="liftoff-actions">
|
|
641
|
-
<a class="btn primary" data-shipoff-view target="_blank" rel="noopener" hidden>View
|
|
1084
|
+
<a class="btn primary" data-shipoff-view target="_blank" rel="noopener" hidden>View live</a>
|
|
1085
|
+
<!-- r10 — right at the post-publish moment: jump straight to the rollback
|
|
1086
|
+
row for the version this release just replaced, pre-targeted, so a
|
|
1087
|
+
publish that looked wrong is one click from being undone. Reuses the
|
|
1088
|
+
existing Live Site rollback action; shown only on a successful ship. -->
|
|
1089
|
+
<button class="btn" type="button" data-shipoff-undo hidden>
|
|
1090
|
+
<svg><use href="#i-undo"></use></svg>Undo this release
|
|
1091
|
+
</button>
|
|
642
1092
|
<button class="btn" type="button" data-shipoff-done>Done</button>
|
|
643
1093
|
</div>
|
|
644
1094
|
</div>
|
|
@@ -646,2312 +1096,108 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
646
1096
|
<div class="liftoff-confetti" data-shipoff-confetti aria-hidden="true"></div>
|
|
647
1097
|
</div>
|
|
648
1098
|
</div>
|
|
1099
|
+
|
|
1100
|
+
<!-- Build/check flight — the real-time counterpart of the accept liftoff for the
|
|
1101
|
+
"Build preview" / "Run checks" action. `POST /api/preview/build` streams its
|
|
1102
|
+
lifecycle phases over SSE (Accept: text/event-stream); this dialog reflects them
|
|
1103
|
+
live on the SAME stage-rail + "Continue in background" pattern as the liftoff.
|
|
1104
|
+
On a FAILURE the outcome shows the full detail the build reported plus a "Copy
|
|
1105
|
+
for your AI agent" button (mirrors the merge-conflict copy affordance). Progress
|
|
1106
|
+
reduction + copy assembly live in the tested buildStream.ts; this glue mirrors
|
|
1107
|
+
it (verified on preview). -->
|
|
1108
|
+
<div class="liftoff-backdrop" data-buildoff hidden>
|
|
1109
|
+
<div class="liftoff-card" role="dialog" aria-modal="true" aria-labelledby="buildoff-title" tabindex="-1">
|
|
1110
|
+
<div class="liftoff-phase" data-buildoff-flight>
|
|
1111
|
+
<p class="liftoff-kicker">Building preview</p>
|
|
1112
|
+
<h3 class="liftoff-title" id="buildoff-title" data-buildoff-flight-title>Building…</h3>
|
|
1113
|
+
<ol class="liftoff-stages">
|
|
1114
|
+
<li data-buildoff-stage="resolve">
|
|
1115
|
+
<span class="liftoff-dot"></span>
|
|
1116
|
+
<div><b>Locate commit</b><span>find the pull request's latest commit</span></div>
|
|
1117
|
+
</li>
|
|
1118
|
+
<li data-buildoff-stage="read">
|
|
1119
|
+
<span class="liftoff-dot"></span>
|
|
1120
|
+
<div><b>Read & validate</b><span>read your store's files and check them</span></div>
|
|
1121
|
+
</li>
|
|
1122
|
+
<li data-buildoff-stage="publish">
|
|
1123
|
+
<span class="liftoff-dot"></span>
|
|
1124
|
+
<div><b>Publish version</b><span>materialize this version's hosted preview</span></div>
|
|
1125
|
+
</li>
|
|
1126
|
+
<li data-buildoff-stage="check">
|
|
1127
|
+
<span class="liftoff-dot"></span>
|
|
1128
|
+
<div><b>Run checks</b><span>compute the evidence for this version</span></div>
|
|
1129
|
+
</li>
|
|
1130
|
+
</ol>
|
|
1131
|
+
<p class="liftoff-live" data-buildoff-live role="status" aria-live="polite">Starting…</p>
|
|
1132
|
+
<div class="stream-log" data-buildoff-log role="log" aria-live="polite"></div>
|
|
1133
|
+
<div class="liftoff-meta">
|
|
1134
|
+
<span class="publish-mono" data-buildoff-elapsed>0:00</span>
|
|
1135
|
+
<button class="btn" type="button" data-buildoff-hide>Continue in background</button>
|
|
1136
|
+
</div>
|
|
1137
|
+
</div>
|
|
1138
|
+
|
|
1139
|
+
<div class="liftoff-phase liftoff-outcome-phase" data-buildoff-outcome hidden>
|
|
1140
|
+
<div class="liftoff-seal" data-buildoff-seal aria-hidden="true"></div>
|
|
1141
|
+
<h3 class="liftoff-title" data-buildoff-outcome-title></h3>
|
|
1142
|
+
<p class="note" data-buildoff-outcome-detail></p>
|
|
1143
|
+
<!-- Failure detail + "Copy for your AI agent" — shown ONLY when the build did
|
|
1144
|
+
not succeed. The detail is the honest reason the build reported (errors +
|
|
1145
|
+
blocking findings); the copy button hands a framed brief to a coding agent. -->
|
|
1146
|
+
<div class="liftoff-conflict" data-buildoff-failure hidden>
|
|
1147
|
+
<pre class="liftoff-commands publish-mono" data-buildoff-failure-detail></pre>
|
|
1148
|
+
<div class="liftoff-conflict-actions">
|
|
1149
|
+
<button class="btn" type="button" data-buildoff-copy>Copy for your AI agent</button>
|
|
1150
|
+
</div>
|
|
1151
|
+
</div>
|
|
1152
|
+
<div class="liftoff-actions">
|
|
1153
|
+
<a class="btn" data-buildoff-review target="_blank" rel="noopener" hidden>Open the preview</a>
|
|
1154
|
+
<button class="btn primary" type="button" data-buildoff-done>Done</button>
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
</div>
|
|
1158
|
+
</div>
|
|
1159
|
+
|
|
1160
|
+
<!-- Retry-build flight — the real-time counterpart for the aggregate "Retry build"
|
|
1161
|
+
action (`POST /api/changes/rebuild`), which re-runs ONLY the aggregate rebuild +
|
|
1162
|
+
combined evidence against the last merged commit (no new merge). It streams its
|
|
1163
|
+
reading/publishing/checking phases over SSE into the SAME shared scrolling
|
|
1164
|
+
live-log console; "Continue in background" hides it while the stream keeps
|
|
1165
|
+
running, and on a FAILURE the outcome shows the honest detail + a "Copy for your
|
|
1166
|
+
AI agent" button — the same affordances as the build/ship flights. -->
|
|
1167
|
+
<div class="liftoff-backdrop" data-rebuildoff hidden>
|
|
1168
|
+
<div class="liftoff-card" role="dialog" aria-modal="true" aria-labelledby="rebuildoff-title" tabindex="-1">
|
|
1169
|
+
<div class="liftoff-phase" data-rebuildoff-flight>
|
|
1170
|
+
<p class="liftoff-kicker">Retrying the build</p>
|
|
1171
|
+
<h3 class="liftoff-title" id="rebuildoff-title">Rebuilding your next release preview…</h3>
|
|
1172
|
+
<p class="liftoff-live" data-rebuildoff-live role="status" aria-live="polite">Starting…</p>
|
|
1173
|
+
<div class="stream-log" data-rebuildoff-log role="log" aria-live="polite"></div>
|
|
1174
|
+
<div class="liftoff-meta">
|
|
1175
|
+
<span class="publish-mono" data-rebuildoff-elapsed>0:00</span>
|
|
1176
|
+
<button class="btn" type="button" data-rebuildoff-hide>Continue in background</button>
|
|
1177
|
+
</div>
|
|
1178
|
+
</div>
|
|
1179
|
+
|
|
1180
|
+
<div class="liftoff-phase liftoff-outcome-phase" data-rebuildoff-outcome hidden>
|
|
1181
|
+
<div class="liftoff-seal" data-rebuildoff-seal aria-hidden="true"></div>
|
|
1182
|
+
<h3 class="liftoff-title" data-rebuildoff-outcome-title></h3>
|
|
1183
|
+
<p class="note" data-rebuildoff-outcome-detail></p>
|
|
1184
|
+
<div class="liftoff-conflict" data-rebuildoff-failure hidden>
|
|
1185
|
+
<pre class="liftoff-commands publish-mono" data-rebuildoff-failure-detail></pre>
|
|
1186
|
+
<div class="liftoff-conflict-actions">
|
|
1187
|
+
<button class="btn" type="button" data-rebuildoff-copy>Copy for your AI agent</button>
|
|
1188
|
+
</div>
|
|
1189
|
+
</div>
|
|
1190
|
+
<div class="liftoff-actions">
|
|
1191
|
+
<button class="btn primary" type="button" data-rebuildoff-done>Done</button>
|
|
1192
|
+
</div>
|
|
1193
|
+
</div>
|
|
1194
|
+
</div>
|
|
1195
|
+
</div>
|
|
649
1196
|
</section>
|
|
650
1197
|
|
|
651
|
-
<script
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (!root) return;
|
|
655
|
-
const ds = root.dataset;
|
|
656
|
-
const canShip = ds.canShip === "true";
|
|
657
|
-
const repo = ds.repo || "";
|
|
658
|
-
const appDomain = ds.appDomain || "";
|
|
659
|
-
|
|
660
|
-
const statusEl = root.querySelector("[data-publish-status]");
|
|
661
|
-
const queueBody = root.querySelector("[data-publish-queue-body]");
|
|
662
|
-
const queueChip = root.querySelector("[data-publish-queue-chip]");
|
|
663
|
-
const queueCount = root.querySelector("[data-publish-queue-count]");
|
|
664
|
-
const degradedStrip = root.querySelector("[data-publish-degraded]");
|
|
665
|
-
|
|
666
|
-
// rux7 — in-flight UI state the auto-refresh poll must preserve across a
|
|
667
|
-
// re-render. These are the AUTHORITATIVE source of truth (never scraped
|
|
668
|
-
// back out of the DOM): an action's async closure holds a reference to the
|
|
669
|
-
// OLD button, so after a poll swaps the row the completion signal lands on a
|
|
670
|
-
// detached node — reading `disabled`/`data-building` off the fresh DOM would
|
|
671
|
-
// wedge forever. buildPreview/integrate/reject/retire add on start and
|
|
672
|
-
// delete in `finally` (before their own loadQueue), so the next render
|
|
673
|
-
// reflects the true busy set. Open diff panes are the exception: the DOM IS
|
|
674
|
-
// their source of truth (toggleDiff owns open/close), so renderQueue detaches
|
|
675
|
-
// and re-attaches the actual pane node — see renderQueue.
|
|
676
|
-
const inflightBuilds = new Set(); // prNumber currently building
|
|
677
|
-
const busyChanges = new Set(); // changeId with an in-flight integrate/reject/retire
|
|
678
|
-
let aggregatePublishing = false; // the aggregate Publish POST is in flight
|
|
679
|
-
|
|
680
|
-
const setStatus = (el, message, state) => {
|
|
681
|
-
if (!el) return;
|
|
682
|
-
el.textContent = message || "";
|
|
683
|
-
if (state) el.dataset.state = state;
|
|
684
|
-
else delete el.dataset.state;
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
const shortSha = (sha) => (sha ? String(sha).slice(0, 8) : "—");
|
|
688
|
-
|
|
689
|
-
// Inline mirror of `candidatePrUrl` (lib/preview/candidateDashboard.ts) — an
|
|
690
|
-
// is:inline script can't import a module, so the pure URL shape is mirrored
|
|
691
|
-
// here (same pattern as `shortSha`). The friendly `/preview/<tenant>/pr/<N>`
|
|
692
|
-
// route handles every state (ready, not-built, …), so a row with a PR number
|
|
693
|
-
// always has a working preview link even when `previewUrl` is still null.
|
|
694
|
-
const candidatePrUrl = (prNumber) =>
|
|
695
|
-
prNumber != null ? "/preview/" + encodeURIComponent(appDomain) + "/pr/" + prNumber : null;
|
|
696
|
-
|
|
697
|
-
const age = (iso) => {
|
|
698
|
-
if (!iso) return "—";
|
|
699
|
-
const ms = Date.now() - new Date(iso).getTime();
|
|
700
|
-
if (!Number.isFinite(ms) || ms < 0) return "—";
|
|
701
|
-
const mins = Math.floor(ms / 60000);
|
|
702
|
-
if (mins < 1) return "just now";
|
|
703
|
-
if (mins < 60) return mins + "m ago";
|
|
704
|
-
const hours = Math.floor(mins / 60);
|
|
705
|
-
if (hours < 48) return hours + "h ago";
|
|
706
|
-
return Math.floor(hours / 24) + "d ago";
|
|
707
|
-
};
|
|
708
|
-
|
|
709
|
-
const STATUS_CHIP = {
|
|
710
|
-
queued: "",
|
|
711
|
-
reconciling: "blue",
|
|
712
|
-
ready: "green",
|
|
713
|
-
blocked: "red",
|
|
714
|
-
};
|
|
715
|
-
|
|
716
|
-
const chip = (text, tone) => {
|
|
717
|
-
const el = document.createElement("span");
|
|
718
|
-
el.className = "chip" + (tone ? " " + tone : "");
|
|
719
|
-
el.textContent = text;
|
|
720
|
-
return el;
|
|
721
|
-
};
|
|
722
|
-
|
|
723
|
-
const cell = (...children) => {
|
|
724
|
-
const td = document.createElement("td");
|
|
725
|
-
for (const child of children) {
|
|
726
|
-
if (child == null) continue;
|
|
727
|
-
td.append(typeof child === "string" ? document.createTextNode(child) : child);
|
|
728
|
-
}
|
|
729
|
-
return td;
|
|
730
|
-
};
|
|
731
|
-
|
|
732
|
-
const subtle = (text) => {
|
|
733
|
-
const el = document.createElement("span");
|
|
734
|
-
el.className = "subtle";
|
|
735
|
-
el.textContent = text;
|
|
736
|
-
return el;
|
|
737
|
-
};
|
|
738
|
-
|
|
739
|
-
// ---- ?pr=N deep link ---------------------------------------------------
|
|
740
|
-
// A deep link like /admin#publish?pr=42 asks us to surface a specific PR's
|
|
741
|
-
// row once the queue has loaded. The param rides in the hash fragment
|
|
742
|
-
// (after the tab anchor) because that's how the admin shell routes tabs;
|
|
743
|
-
// fall back to a real query string too. Resolve exactly once — so the
|
|
744
|
-
// scroll/highlight never re-fires on the reloads that follow every ship
|
|
745
|
-
// action — and until it resolves, keep an honest "not in the queue yet"
|
|
746
|
-
// callout up rather than silently doing nothing.
|
|
747
|
-
|
|
748
|
-
const readRequestedPr = () => {
|
|
749
|
-
const hash = window.location.hash || "";
|
|
750
|
-
const q = hash.indexOf("?");
|
|
751
|
-
let raw = new URLSearchParams(q >= 0 ? hash.slice(q + 1) : "").get("pr");
|
|
752
|
-
if (raw == null) raw = new URLSearchParams(window.location.search).get("pr");
|
|
753
|
-
if (raw == null) return null;
|
|
754
|
-
const n = Number.parseInt(raw, 10);
|
|
755
|
-
return Number.isInteger(n) && n > 0 ? n : null;
|
|
756
|
-
};
|
|
757
|
-
|
|
758
|
-
const requestedPr = readRequestedPr();
|
|
759
|
-
let prDeeplinkResolved = false;
|
|
760
|
-
|
|
761
|
-
const applyPrDeeplink = () => {
|
|
762
|
-
if (requestedPr == null || prDeeplinkResolved) return;
|
|
763
|
-
const callout = root.querySelector("[data-publish-pr-callout]");
|
|
764
|
-
const row = queueBody.querySelector('tr[data-pr-number="' + requestedPr + '"]');
|
|
765
|
-
if (!row) {
|
|
766
|
-
if (callout) {
|
|
767
|
-
callout.hidden = false;
|
|
768
|
-
callout.textContent = "PR #" + requestedPr + " isn't in the queue yet.";
|
|
769
|
-
}
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
prDeeplinkResolved = true;
|
|
773
|
-
if (callout) {
|
|
774
|
-
callout.hidden = true;
|
|
775
|
-
callout.textContent = "";
|
|
776
|
-
}
|
|
777
|
-
row.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
778
|
-
row.classList.remove("publish-row-highlight");
|
|
779
|
-
void row.offsetWidth; // restart the flash animation if re-applied
|
|
780
|
-
row.classList.add("publish-row-highlight");
|
|
781
|
-
window.setTimeout(() => row.classList.remove("publish-row-highlight"), 2600);
|
|
782
|
-
};
|
|
783
|
-
|
|
784
|
-
// ---- Open change queue -------------------------------------------------
|
|
785
|
-
|
|
786
|
-
const renderQueue = (payload) => {
|
|
787
|
-
const changes = Array.isArray(payload.changes) ? payload.changes : [];
|
|
788
|
-
// rux1: surface a forge-listing failure as a visible strip — never let a
|
|
789
|
-
// degraded (short) list read as a genuine empty queue.
|
|
790
|
-
if (degradedStrip) degradedStrip.hidden = payload.degraded !== true;
|
|
791
|
-
// rux7: capture any open diff panes BEFORE wiping so a poll re-render
|
|
792
|
-
// re-attaches the exact node (with its already-fetched content) under the
|
|
793
|
-
// matching new row, instead of blowing the expanded pane away. Keyed by
|
|
794
|
-
// changeId (== the diff row's data-publish-diff-row). Wiping detaches them
|
|
795
|
-
// from the DOM but the Map keeps them alive for re-attachment.
|
|
796
|
-
const openDiffNodes = new Map();
|
|
797
|
-
queueBody.querySelectorAll("tr[data-publish-diff-row]").forEach((node) => {
|
|
798
|
-
openDiffNodes.set(node.dataset.publishDiffRow, node);
|
|
799
|
-
});
|
|
800
|
-
queueBody.textContent = "";
|
|
801
|
-
if (queueCount) queueCount.textContent = String(changes.length);
|
|
802
|
-
if (queueChip) {
|
|
803
|
-
queueChip.textContent = changes.length === 1 ? "1 open" : changes.length + " open";
|
|
804
|
-
queueChip.className = "chip" + (changes.length ? " blue" : "");
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
if (!changes.length) {
|
|
808
|
-
const tr = document.createElement("tr");
|
|
809
|
-
const td = cell(
|
|
810
|
-
subtle(
|
|
811
|
-
"No open changes. New store changes appear here when a developer submits them for review.",
|
|
812
|
-
),
|
|
813
|
-
);
|
|
814
|
-
td.colSpan = 6;
|
|
815
|
-
tr.append(td);
|
|
816
|
-
queueBody.append(tr);
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
for (const change of changes) {
|
|
821
|
-
const tr = document.createElement("tr");
|
|
822
|
-
if (change.prNumber != null) tr.dataset.prNumber = String(change.prNumber);
|
|
823
|
-
|
|
824
|
-
// Change id + branch / PR + preview link.
|
|
825
|
-
const idCell = document.createElement("td");
|
|
826
|
-
const idStrong = document.createElement("strong");
|
|
827
|
-
idStrong.textContent = change.changeId || "—";
|
|
828
|
-
idCell.append(idStrong);
|
|
829
|
-
const meta = [];
|
|
830
|
-
if (change.branch) meta.push(change.branch);
|
|
831
|
-
if (change.prNumber != null) meta.push("PR #" + change.prNumber);
|
|
832
|
-
if (meta.length) idCell.append(subtle(meta.join(" · ")));
|
|
833
|
-
// Owner-facing "who did this" — accept takes priority over build (a
|
|
834
|
-
// candidate that's been accepted-to-preview is more meaningfully
|
|
835
|
-
// "acted on" than merely built). Reject never shows here: a rejected
|
|
836
|
-
// candidate is closed and has already left the open queue.
|
|
837
|
-
const actorRecord = change.actors && (change.actors.accept || change.actors.build);
|
|
838
|
-
if (actorRecord) {
|
|
839
|
-
const label = change.actors.accept ? "Accepted by " : "Built by ";
|
|
840
|
-
idCell.append(subtle(label + (actorRecord.name || actorRecord.email)));
|
|
841
|
-
}
|
|
842
|
-
// Prefer the synthesized friendly per-PR route (handles every state);
|
|
843
|
-
// fall back to any server-provided previewUrl for change-first rows
|
|
844
|
-
// that have no PR number yet.
|
|
845
|
-
const previewUrl = candidatePrUrl(change.prNumber) || change.previewUrl;
|
|
846
|
-
if (previewUrl) {
|
|
847
|
-
const preview = document.createElement("a");
|
|
848
|
-
preview.href = previewUrl;
|
|
849
|
-
preview.target = "_blank";
|
|
850
|
-
preview.rel = "noopener";
|
|
851
|
-
preview.textContent = "Open preview";
|
|
852
|
-
const wrap = document.createElement("span");
|
|
853
|
-
wrap.className = "subtle";
|
|
854
|
-
wrap.append(preview);
|
|
855
|
-
idCell.append(wrap);
|
|
856
|
-
}
|
|
857
|
-
tr.append(idCell);
|
|
858
|
-
|
|
859
|
-
// Reconcile status.
|
|
860
|
-
const statusCell = cell(
|
|
861
|
-
chip(change.status || "unknown", STATUS_CHIP[change.status] ?? "amber"),
|
|
862
|
-
);
|
|
863
|
-
if (change.lastError) statusCell.append(subtle(change.lastError));
|
|
864
|
-
tr.append(statusCell);
|
|
865
|
-
|
|
866
|
-
// Evidence / compliance verdict.
|
|
867
|
-
const checks = (change.evidence && change.evidence.checks) || [];
|
|
868
|
-
const failing = checks.filter((c) => c.status === "fail");
|
|
869
|
-
const promotable = Boolean(change.evidence && change.evidence.promotable);
|
|
870
|
-
const checksCell = cell(
|
|
871
|
-
promotable ? chip("Checks passed", "green") : chip("Checks blocked", "amber"),
|
|
872
|
-
);
|
|
873
|
-
if (failing.length) {
|
|
874
|
-
checksCell.append(subtle("Failing: " + failing.map((c) => c.label || c.id).join(", ")));
|
|
875
|
-
} else if (!checks.length) {
|
|
876
|
-
checksCell.append(subtle("No checks reported yet"));
|
|
877
|
-
}
|
|
878
|
-
tr.append(checksCell);
|
|
879
|
-
|
|
880
|
-
// Head sha + age.
|
|
881
|
-
const headCell = cell(shortSha(change.headSha));
|
|
882
|
-
headCell.classList.add("publish-mono");
|
|
883
|
-
tr.append(headCell);
|
|
884
|
-
tr.append(cell(age(change.updatedAt)));
|
|
885
|
-
|
|
886
|
-
// Actions.
|
|
887
|
-
const actions = document.createElement("td");
|
|
888
|
-
actions.className = "action publish-actions";
|
|
889
|
-
|
|
890
|
-
// rux5 — a synthetic not-built forge PR row (built:false / status
|
|
891
|
-
// "not-built") has NO built candidate: changeId is null, so the built-only
|
|
892
|
-
// actions below (Diff/Accept/Reject/Retire, all keyed on changeId) do not
|
|
893
|
-
// apply. Its one action is Build preview — materialize the PR's hosted
|
|
894
|
-
// preview on demand. Capability-gated: the button is SHOWN only to a
|
|
895
|
-
// ship-capable operator, and the server (buildGate) re-authorizes — the UI
|
|
896
|
-
// check is UX, not the real gate.
|
|
897
|
-
if (change.built === false || change.status === "not-built") {
|
|
898
|
-
if (canShip) {
|
|
899
|
-
const build = document.createElement("button");
|
|
900
|
-
build.type = "button";
|
|
901
|
-
build.className = "btn primary";
|
|
902
|
-
build.title = "Materialize this PR's hosted preview on demand (does not go live)";
|
|
903
|
-
// rux7: if a build for this PR is mid-flight, a poll re-render must
|
|
904
|
-
// reflect the spinner/disabled state (not a fresh clickable button
|
|
905
|
-
// that would let the operator double-submit). The in-flight closure
|
|
906
|
-
// re-renders on completion, clearing this.
|
|
907
|
-
if (change.prNumber != null && inflightBuilds.has(change.prNumber)) {
|
|
908
|
-
build.textContent = "Building…";
|
|
909
|
-
build.disabled = true;
|
|
910
|
-
build.dataset.building = "true";
|
|
911
|
-
} else {
|
|
912
|
-
build.textContent = "Build preview";
|
|
913
|
-
}
|
|
914
|
-
build.addEventListener("click", () => buildPreview(change, build));
|
|
915
|
-
actions.append(build);
|
|
916
|
-
} else {
|
|
917
|
-
actions.append(subtle("Owner/admin sign-in required to build"));
|
|
918
|
-
}
|
|
919
|
-
tr.append(actions);
|
|
920
|
-
queueBody.append(tr);
|
|
921
|
-
continue;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// rux7: while an integrate/reject/retire for this change is in flight,
|
|
925
|
-
// keep its mutating actions disabled across a poll re-render so the
|
|
926
|
-
// operator can't double-submit against a freshly-minted button.
|
|
927
|
-
const changeBusy = busyChanges.has(change.changeId);
|
|
928
|
-
|
|
929
|
-
// rux6 Review: expand the row into a single "review moment" pane —
|
|
930
|
-
// preview iframe + evidence chips + diff + Accept/Reject — so a reviewer
|
|
931
|
-
// never leaves the queue. Supersedes the standalone Diff button (the dg7
|
|
932
|
-
// diff now lives inside the pane, reusing renderDiff). The row's own
|
|
933
|
-
// Accept/Reject/Retire buttons below are unchanged.
|
|
934
|
-
const reviewBtn = document.createElement("button");
|
|
935
|
-
reviewBtn.type = "button";
|
|
936
|
-
reviewBtn.className = "btn";
|
|
937
|
-
reviewBtn.textContent = "Review";
|
|
938
|
-
reviewBtn.setAttribute("aria-expanded", "false");
|
|
939
|
-
reviewBtn.title = "Open one pane to preview, diff, and act on this change";
|
|
940
|
-
reviewBtn.addEventListener("click", () => toggleReview(change, reviewBtn, tr));
|
|
941
|
-
actions.append(reviewBtn);
|
|
942
|
-
// b11 SPLIT: the candidate-queue primary action is now ACCEPT TO PREVIEW
|
|
943
|
-
// (b08 integrate = advance the shared preview aggregate), NOT go-live.
|
|
944
|
-
// Integrate flips NO live channel, so it has NO paywall (entitled) gate —
|
|
945
|
-
// it holds only the ship-on-behalf capability floor. Going live is the
|
|
946
|
-
// aggregate card's Publish action, once the aggregate is green.
|
|
947
|
-
const accept = document.createElement("button");
|
|
948
|
-
accept.type = "button";
|
|
949
|
-
accept.className = "btn primary";
|
|
950
|
-
accept.textContent = "Accept to preview";
|
|
951
|
-
accept.disabled = !canShip || !change.mergeable || changeBusy;
|
|
952
|
-
accept.title = !canShip
|
|
953
|
-
? "Requires an owner or admin sign-in"
|
|
954
|
-
: !change.mergeable
|
|
955
|
-
? "This change is not ready to integrate"
|
|
956
|
-
: "Integrate this change into the preview aggregate (does not go live)";
|
|
957
|
-
accept.addEventListener("click", () => integrateCandidate(change, accept));
|
|
958
|
-
actions.append(accept);
|
|
959
|
-
const reject = document.createElement("button");
|
|
960
|
-
reject.type = "button";
|
|
961
|
-
reject.className = "btn";
|
|
962
|
-
reject.textContent = "Reject";
|
|
963
|
-
reject.disabled = !canShip || changeBusy;
|
|
964
|
-
reject.title = canShip ? "Close this change without publishing" : "Requires an owner or admin sign-in";
|
|
965
|
-
reject.addEventListener("click", () => rejectChange(change, reject));
|
|
966
|
-
actions.append(reject);
|
|
967
|
-
|
|
968
|
-
// U7 Retire/GC: evict this candidate's preview environment + version to
|
|
969
|
-
// reclaim space. DISTINCT from Reject — retire is reversible-by-rebuild
|
|
970
|
-
// (U1 build-on-demand + the U2 fallback page rematerialize it), so a
|
|
971
|
-
// retired PR degrades to "not built yet", not a dead 404.
|
|
972
|
-
const retire = document.createElement("button");
|
|
973
|
-
retire.type = "button";
|
|
974
|
-
retire.className = "btn";
|
|
975
|
-
retire.textContent = "Retire";
|
|
976
|
-
retire.disabled = !canShip || changeBusy;
|
|
977
|
-
retire.title = canShip
|
|
978
|
-
? "Evict this preview to reclaim space — rebuildable on demand (not the same as Reject)"
|
|
979
|
-
: "Requires an owner or admin sign-in";
|
|
980
|
-
retire.addEventListener("click", () => retireChange(change, retire));
|
|
981
|
-
actions.append(retire);
|
|
982
|
-
tr.append(actions);
|
|
983
|
-
|
|
984
|
-
queueBody.append(tr);
|
|
985
|
-
|
|
986
|
-
// rux7: re-attach a diff pane that was open before this re-render. The
|
|
987
|
-
// pane node carries its already-fetched content; restore the expanded
|
|
988
|
-
// state + the "Hide diff" toggle affordance so a poll never collapses it.
|
|
989
|
-
const openDiff = openDiffNodes.get(change.changeId);
|
|
990
|
-
if (openDiff) {
|
|
991
|
-
tr.dataset.changeRowId = change.changeId;
|
|
992
|
-
diffBtn.textContent = "Hide diff";
|
|
993
|
-
tr.after(openDiff);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
};
|
|
997
|
-
|
|
998
|
-
const renderCurrent = (current, shippedBy) => {
|
|
999
|
-
const versionEl = root.querySelector("[data-publish-live-version]");
|
|
1000
|
-
const sourceEl = root.querySelector("[data-publish-live-source]");
|
|
1001
|
-
const atEl = root.querySelector("[data-publish-live-at]");
|
|
1002
|
-
const shippedByEl = root.querySelector("[data-publish-live-shipped-by]");
|
|
1003
|
-
if (current && current.versionId) {
|
|
1004
|
-
if (versionEl) versionEl.textContent = shortSha(current.versionId);
|
|
1005
|
-
if (sourceEl) {
|
|
1006
|
-
sourceEl.textContent =
|
|
1007
|
-
(current.source === "commit" ? "Published from a change" : "Published version") +
|
|
1008
|
-
(current.artifactCount != null ? " · " + current.artifactCount + " files" : "");
|
|
1009
|
-
}
|
|
1010
|
-
if (atEl) atEl.textContent = current.createdAt ? age(current.createdAt) : "—";
|
|
1011
|
-
// Owner-facing "who did this" — see changeActorAttribution.ts. Absent for
|
|
1012
|
-
// any version published before this attribution existed, which is honest,
|
|
1013
|
-
// not a bug.
|
|
1014
|
-
if (shippedByEl) {
|
|
1015
|
-
shippedByEl.textContent = shippedBy ? "Shipped by " + (shippedBy.name || shippedBy.email) : "";
|
|
1016
|
-
}
|
|
1017
|
-
} else {
|
|
1018
|
-
if (versionEl) versionEl.textContent = "Not live yet";
|
|
1019
|
-
if (sourceEl) sourceEl.textContent = "Nothing has been published for this store";
|
|
1020
|
-
if (atEl) atEl.textContent = "—";
|
|
1021
|
-
if (shippedByEl) shippedByEl.textContent = "";
|
|
1022
|
-
}
|
|
1023
|
-
};
|
|
1024
|
-
|
|
1025
|
-
// ---- rux7: auto-refresh (poll GET /api/changes) ------------------------
|
|
1026
|
-
// Keep the queue + aggregate live without a manual reload: poll on an
|
|
1027
|
-
// interval that BACKS OFF while nothing changes (grow the delay up to a cap)
|
|
1028
|
-
// and SNAPS BACK to the base the moment something does. Pause entirely while
|
|
1029
|
-
// the tab is hidden (document.hidden) — a backgrounded admin tab must not
|
|
1030
|
-
// hammer the endpoint — and refresh promptly on return. A poll re-render
|
|
1031
|
-
// reuses renderQueue/renderAggregate, which already preserve open diff panes
|
|
1032
|
-
// and in-flight actions (inflightBuilds / busyChanges / aggregatePublishing),
|
|
1033
|
-
// and applyPrDeeplink stays a one-time effect (prDeeplinkResolved), so the
|
|
1034
|
-
// ?pr scroll never re-fires on a tick.
|
|
1035
|
-
const POLL_BASE_MS = 8000;
|
|
1036
|
-
const POLL_MAX_MS = 60000;
|
|
1037
|
-
const POLL_GROWTH = 1.6;
|
|
1038
|
-
let pollDelay = POLL_BASE_MS;
|
|
1039
|
-
let pollTimer = null;
|
|
1040
|
-
let pollBusy = false;
|
|
1041
|
-
let lastChangesSignature = null;
|
|
1042
|
-
|
|
1043
|
-
// A compact projection of everything the queue/aggregate render VISIBLY off,
|
|
1044
|
-
// so a no-op poll (identical data) neither re-renders nor interrupts a text
|
|
1045
|
-
// selection — it just backs off. Relative ages are intentionally excluded;
|
|
1046
|
-
// they drift at most one backoff interval and refresh on any real change.
|
|
1047
|
-
const changesSignature = (data) => {
|
|
1048
|
-
const cur = data.current || {};
|
|
1049
|
-
const agg = data.aggregate || {};
|
|
1050
|
-
const run = agg.activeRun || agg.lastRun || {};
|
|
1051
|
-
const plan = data.plan || {};
|
|
1052
|
-
const rows = (Array.isArray(data.changes) ? data.changes : []).map((c) => [
|
|
1053
|
-
c.changeId,
|
|
1054
|
-
c.prNumber,
|
|
1055
|
-
c.status,
|
|
1056
|
-
c.headSha,
|
|
1057
|
-
c.mergeable,
|
|
1058
|
-
c.built,
|
|
1059
|
-
c.lastError || null,
|
|
1060
|
-
!!(c.evidence && c.evidence.promotable),
|
|
1061
|
-
c.actors ? Object.keys(c.actors).sort().join(",") : "",
|
|
1062
|
-
]);
|
|
1063
|
-
return JSON.stringify({
|
|
1064
|
-
degraded: data.degraded === true,
|
|
1065
|
-
current: [cur.versionId || null, cur.createdAt || null, cur.artifactCount ?? null],
|
|
1066
|
-
shippedBy: (data.shippedBy && data.shippedBy.email) || null,
|
|
1067
|
-
aggregate: [
|
|
1068
|
-
agg.queueState || null,
|
|
1069
|
-
agg.aggregateSha || null,
|
|
1070
|
-
run.state || null,
|
|
1071
|
-
run.finishedAt || null,
|
|
1072
|
-
(agg.includedPrs || []).length,
|
|
1073
|
-
],
|
|
1074
|
-
plan: [plan.ok === true, plan.pinnedSha || null, plan.artifactDigest || null, plan.alreadyShipped === true],
|
|
1075
|
-
rows,
|
|
1076
|
-
});
|
|
1077
|
-
};
|
|
1078
|
-
|
|
1079
|
-
const clearPoll = () => {
|
|
1080
|
-
if (pollTimer) {
|
|
1081
|
-
clearTimeout(pollTimer);
|
|
1082
|
-
pollTimer = null;
|
|
1083
|
-
}
|
|
1084
|
-
};
|
|
1085
|
-
|
|
1086
|
-
const schedulePoll = () => {
|
|
1087
|
-
clearPoll();
|
|
1088
|
-
if (document.hidden) return; // paused while hidden; visibilitychange resumes
|
|
1089
|
-
pollTimer = setTimeout(runPoll, pollDelay);
|
|
1090
|
-
};
|
|
1091
|
-
|
|
1092
|
-
// One poll tick: fetch, and only re-render when the projection actually
|
|
1093
|
-
// changed. Returns an outcome the caller uses to size the next interval.
|
|
1094
|
-
const pollChanges = async () => {
|
|
1095
|
-
let res;
|
|
1096
|
-
try {
|
|
1097
|
-
res = await fetch(ds.changesEndpoint, { headers: { accept: "application/json" } });
|
|
1098
|
-
} catch {
|
|
1099
|
-
return "error"; // network blip — keep the last good view, just back off
|
|
1100
|
-
}
|
|
1101
|
-
if (res.status === 401 || res.status === 403) return "auth"; // session gone
|
|
1102
|
-
const data = await res.json().catch(() => null);
|
|
1103
|
-
if (!res.ok || !data) return "error";
|
|
1104
|
-
const sig = changesSignature(data);
|
|
1105
|
-
if (sig === lastChangesSignature) return "unchanged";
|
|
1106
|
-
lastChangesSignature = sig;
|
|
1107
|
-
renderCurrent(data.current, data.shippedBy);
|
|
1108
|
-
renderQueue(data);
|
|
1109
|
-
applyPrDeeplink();
|
|
1110
|
-
renderAggregate(data);
|
|
1111
|
-
return "changed";
|
|
1112
|
-
};
|
|
1113
|
-
|
|
1114
|
-
const runPoll = async () => {
|
|
1115
|
-
pollTimer = null;
|
|
1116
|
-
if (document.hidden || pollBusy) {
|
|
1117
|
-
schedulePoll();
|
|
1118
|
-
return;
|
|
1119
|
-
}
|
|
1120
|
-
pollBusy = true;
|
|
1121
|
-
let outcome;
|
|
1122
|
-
try {
|
|
1123
|
-
outcome = await pollChanges();
|
|
1124
|
-
} finally {
|
|
1125
|
-
pollBusy = false;
|
|
1126
|
-
}
|
|
1127
|
-
if (outcome === "auth") {
|
|
1128
|
-
clearPoll(); // stop polling — a sign-in is required; a manual refresh retries
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
pollDelay =
|
|
1132
|
-
outcome === "changed"
|
|
1133
|
-
? POLL_BASE_MS
|
|
1134
|
-
: Math.min(POLL_MAX_MS, Math.round(pollDelay * POLL_GROWTH));
|
|
1135
|
-
schedulePoll();
|
|
1136
|
-
};
|
|
1137
|
-
|
|
1138
|
-
const loadQueue = async () => {
|
|
1139
|
-
try {
|
|
1140
|
-
const res = await fetch(ds.changesEndpoint, { headers: { accept: "application/json" } });
|
|
1141
|
-
if (res.status === 401 || res.status === 403) {
|
|
1142
|
-
if (queueChip) queueChip.textContent = "Sign-in required";
|
|
1143
|
-
if (degradedStrip) degradedStrip.hidden = true;
|
|
1144
|
-
queueBody.textContent = "";
|
|
1145
|
-
const tr = document.createElement("tr");
|
|
1146
|
-
const td = cell(subtle("Sign in as a store owner or admin to view the publish queue."));
|
|
1147
|
-
td.colSpan = 6;
|
|
1148
|
-
tr.append(td);
|
|
1149
|
-
queueBody.append(tr);
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
const data = await res.json();
|
|
1153
|
-
if (!res.ok) {
|
|
1154
|
-
setStatus(statusEl, data.error || "Could not load the change queue (" + res.status + ")", "err");
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
|
-
renderCurrent(data.current, data.shippedBy);
|
|
1158
|
-
renderQueue(data);
|
|
1159
|
-
applyPrDeeplink();
|
|
1160
|
-
renderAggregate(data);
|
|
1161
|
-
// rux7: this is fresh authoritative data — resync the poll clock so the
|
|
1162
|
-
// next tick dedupes against it, and reset the cadence to responsive
|
|
1163
|
-
// (a manual refresh / a just-completed action likely means more coming).
|
|
1164
|
-
lastChangesSignature = changesSignature(data);
|
|
1165
|
-
pollDelay = POLL_BASE_MS;
|
|
1166
|
-
schedulePoll();
|
|
1167
|
-
} catch {
|
|
1168
|
-
setStatus(statusEl, "Could not load the change queue — network error", "err");
|
|
1169
|
-
}
|
|
1170
|
-
};
|
|
1171
|
-
|
|
1172
|
-
// ---- b11: the aggregate card + exact-digest Publish ---------------------
|
|
1173
|
-
// Reads the aggregate/runs/plan projection GET /api/changes now returns
|
|
1174
|
-
// (AggregateCardView + AggregateShipPlanView, built by aggregateView.ts):
|
|
1175
|
-
// - the aggregate's queueState + build sha + reviewed digest,
|
|
1176
|
-
// - the COMBINED EVIDENCE = the last integration run's honest state +
|
|
1177
|
-
// statusMessage (never re-derived),
|
|
1178
|
-
// - the included-PR batch,
|
|
1179
|
-
// - the b09 ship PLAN (pinned SHA/digest/PRs/rollback/paywall + run link),
|
|
1180
|
-
// shown inline so the confirm never has to POST to preview it.
|
|
1181
|
-
|
|
1182
|
-
const aggPanel = root.querySelector("[data-publish-aggregate]");
|
|
1183
|
-
const AGG_STATE_CHIP = {
|
|
1184
|
-
empty: "",
|
|
1185
|
-
queued: "amber",
|
|
1186
|
-
integrating: "blue",
|
|
1187
|
-
green: "green",
|
|
1188
|
-
red: "red",
|
|
1189
|
-
shipped: "green",
|
|
1190
|
-
};
|
|
1191
|
-
// The plan we last rendered — the exact-digest guard the Publish POST sends
|
|
1192
|
-
// back so a stale plan (queue moved / artifact changed) is refused, never
|
|
1193
|
-
// shipped blind.
|
|
1194
|
-
let currentPlan = null;
|
|
1195
|
-
|
|
1196
|
-
const runLink = (revHref, label) => {
|
|
1197
|
-
if (!revHref) return null;
|
|
1198
|
-
const a = document.createElement("a");
|
|
1199
|
-
a.href = revHref;
|
|
1200
|
-
a.target = "_blank";
|
|
1201
|
-
a.rel = "noopener";
|
|
1202
|
-
a.textContent = label || "Open build";
|
|
1203
|
-
return a;
|
|
1204
|
-
};
|
|
1205
|
-
|
|
1206
|
-
const renderAggregate = (data) => {
|
|
1207
|
-
if (!aggPanel) return;
|
|
1208
|
-
const agg = data.aggregate || null;
|
|
1209
|
-
const plan = data.plan || null;
|
|
1210
|
-
currentPlan = plan && plan.ok ? plan : null;
|
|
1211
|
-
|
|
1212
|
-
const chipEl = aggPanel.querySelector("[data-aggregate-chip]");
|
|
1213
|
-
const shaEl = aggPanel.querySelector("[data-aggregate-sha]");
|
|
1214
|
-
const runLinkEl = aggPanel.querySelector("[data-aggregate-run-link]");
|
|
1215
|
-
const evEl = aggPanel.querySelector("[data-aggregate-evidence]");
|
|
1216
|
-
const evDetailEl = aggPanel.querySelector("[data-aggregate-evidence-detail]");
|
|
1217
|
-
const digestEl = aggPanel.querySelector("[data-aggregate-digest]");
|
|
1218
|
-
const prsWrap = aggPanel.querySelector("[data-aggregate-prs-wrap]");
|
|
1219
|
-
const prsList = aggPanel.querySelector("[data-aggregate-prs]");
|
|
1220
|
-
const publishBtn = aggPanel.querySelector("[data-aggregate-publish]");
|
|
1221
|
-
const publishDetail = aggPanel.querySelector("[data-aggregate-publish-detail]");
|
|
1222
|
-
|
|
1223
|
-
const state = (agg && agg.queueState) || "empty";
|
|
1224
|
-
if (chipEl) {
|
|
1225
|
-
chipEl.textContent = state;
|
|
1226
|
-
chipEl.className = "chip " + (AGG_STATE_CHIP[state] || "");
|
|
1227
|
-
}
|
|
1228
|
-
if (shaEl) shaEl.textContent = agg && agg.aggregateSha ? shortSha(agg.aggregateSha) : "—";
|
|
1229
|
-
if (runLinkEl) {
|
|
1230
|
-
runLinkEl.textContent = "";
|
|
1231
|
-
const link = agg && runLink(agg.revHref, "Open build");
|
|
1232
|
-
if (link) runLinkEl.append(link);
|
|
1233
|
-
else runLinkEl.append(document.createTextNode("The current green preview build for this store."));
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// Combined evidence = the last (or in-flight) integration run's honest state.
|
|
1237
|
-
const lastRun = agg && (agg.activeRun || agg.lastRun);
|
|
1238
|
-
if (evEl) {
|
|
1239
|
-
if (!lastRun) {
|
|
1240
|
-
evEl.textContent = "No runs yet";
|
|
1241
|
-
} else if (lastRun.state === "passed") {
|
|
1242
|
-
evEl.textContent = "Passed";
|
|
1243
|
-
} else if (lastRun.state === "failed") {
|
|
1244
|
-
evEl.textContent = "Failed";
|
|
1245
|
-
} else {
|
|
1246
|
-
evEl.textContent = "Integrating…";
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
if (evDetailEl) {
|
|
1250
|
-
evDetailEl.textContent = "";
|
|
1251
|
-
if (lastRun) {
|
|
1252
|
-
const parts = [];
|
|
1253
|
-
if (lastRun.statusMessage) parts.push(lastRun.statusMessage);
|
|
1254
|
-
else if (lastRun.state === "passed") parts.push("Combined evidence passed.");
|
|
1255
|
-
else if (lastRun.state === "failed") parts.push("Combined evidence did not pass.");
|
|
1256
|
-
if (lastRun.finishedAt) parts.push(age(lastRun.finishedAt));
|
|
1257
|
-
evDetailEl.append(document.createTextNode(parts.join(" · ")));
|
|
1258
|
-
const rl = runLink(lastRun.revHref, "Run build");
|
|
1259
|
-
if (rl) {
|
|
1260
|
-
evDetailEl.append(document.createTextNode(" · "));
|
|
1261
|
-
evDetailEl.append(rl);
|
|
1262
|
-
}
|
|
1263
|
-
} else {
|
|
1264
|
-
evDetailEl.append(document.createTextNode("The last integration run's verdict."));
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// Reviewed digest: only known once the plan pins it (b09). Never fabricated.
|
|
1269
|
-
if (digestEl) {
|
|
1270
|
-
digestEl.textContent = currentPlan ? shortSha(currentPlan.artifactDigest.replace(/^sha256:/, "")) : "—";
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
// Included PRs.
|
|
1274
|
-
const prs = (agg && agg.includedPrs) || [];
|
|
1275
|
-
if (prsWrap) prsWrap.hidden = prs.length === 0;
|
|
1276
|
-
if (prsList) {
|
|
1277
|
-
prsList.textContent = "";
|
|
1278
|
-
for (const pr of prs) {
|
|
1279
|
-
const li = document.createElement("li");
|
|
1280
|
-
const label = document.createElement("span");
|
|
1281
|
-
label.textContent = pr.prNumber != null ? "PR #" + pr.prNumber : pr.changeId;
|
|
1282
|
-
const sha = document.createElement("span");
|
|
1283
|
-
sha.className = "publish-mono subtle";
|
|
1284
|
-
sha.textContent = shortSha(pr.headSha);
|
|
1285
|
-
li.append(label, sha);
|
|
1286
|
-
prsList.append(li);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
renderPlan(data.plan);
|
|
1291
|
-
|
|
1292
|
-
// The Publish button: enabled only for a shippable, paywall-cleared plan and
|
|
1293
|
-
// a viewer who can ship. A paywalled plan is SHOWN (upsell), never hidden.
|
|
1294
|
-
if (publishBtn) {
|
|
1295
|
-
const shippable = Boolean(currentPlan);
|
|
1296
|
-
const paywalled = currentPlan && currentPlan.paywall && !currentPlan.paywall.allowed;
|
|
1297
|
-
// rux7: never re-enable the Publish button while its POST is in flight —
|
|
1298
|
-
// a poll refresh landing mid-publish must not open a double-submit.
|
|
1299
|
-
publishBtn.disabled =
|
|
1300
|
-
aggregatePublishing || !canShip || !shippable || paywalled || currentPlan.alreadyShipped;
|
|
1301
|
-
publishBtn.title = !canShip
|
|
1302
|
-
? "Requires an owner or admin sign-in"
|
|
1303
|
-
: !shippable
|
|
1304
|
-
? "The aggregate is not green / has no passed run to publish yet"
|
|
1305
|
-
: paywalled
|
|
1306
|
-
? "Publishing to your live store is part of the paid plan"
|
|
1307
|
-
: currentPlan.alreadyShipped
|
|
1308
|
-
? "This reviewed build is already live"
|
|
1309
|
-
: "Publish the exact reviewed digest to your live store";
|
|
1310
|
-
}
|
|
1311
|
-
if (publishDetail) {
|
|
1312
|
-
if (currentPlan && currentPlan.alreadyShipped) {
|
|
1313
|
-
publishDetail.textContent =
|
|
1314
|
-
"This reviewed build (" + shortSha(currentPlan.pinnedSha) + ") is already live. ";
|
|
1315
|
-
const view = document.createElement("a");
|
|
1316
|
-
view.href = ds.liveHref || "/";
|
|
1317
|
-
view.target = "_blank";
|
|
1318
|
-
view.rel = "noopener";
|
|
1319
|
-
view.textContent = "View your live store →";
|
|
1320
|
-
publishDetail.append(view);
|
|
1321
|
-
} else if (currentPlan) {
|
|
1322
|
-
publishDetail.textContent =
|
|
1323
|
-
"Publishes the exact reviewed digest " + shortSha(currentPlan.artifactDigest.replace(/^sha256:/, "")) +
|
|
1324
|
-
" (build " + shortSha(currentPlan.pinnedSha) + ") — no rebuild.";
|
|
1325
|
-
} else {
|
|
1326
|
-
publishDetail.textContent =
|
|
1327
|
-
"Publishes the exact reviewed digest — no rebuild. Available once the aggregate is green and its integration run passed.";
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
};
|
|
1331
|
-
|
|
1332
|
-
const renderPlan = (plan) => {
|
|
1333
|
-
const planBox = aggPanel && aggPanel.querySelector("[data-aggregate-plan]");
|
|
1334
|
-
if (!planBox) return;
|
|
1335
|
-
const chipEl = planBox.querySelector("[data-aggregate-plan-chip]");
|
|
1336
|
-
const grid = planBox.querySelector("[data-aggregate-plan-grid]");
|
|
1337
|
-
const note = planBox.querySelector("[data-aggregate-plan-note]");
|
|
1338
|
-
if (grid) grid.textContent = "";
|
|
1339
|
-
|
|
1340
|
-
// No plan at all (degenerate: no store bound) — hide the box.
|
|
1341
|
-
if (!plan) {
|
|
1342
|
-
planBox.hidden = true;
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
planBox.hidden = false;
|
|
1346
|
-
|
|
1347
|
-
if (!plan.ok) {
|
|
1348
|
-
// An honest refusal — show WHY it isn't shippable, never a fake plan.
|
|
1349
|
-
if (chipEl) {
|
|
1350
|
-
chipEl.textContent = "Not shippable";
|
|
1351
|
-
chipEl.className = "chip amber";
|
|
1352
|
-
}
|
|
1353
|
-
if (note) note.textContent = plan.message || ("Not shippable (" + (plan.reason || "refused") + ").");
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
const paywalled = plan.paywall && !plan.paywall.allowed;
|
|
1358
|
-
if (chipEl) {
|
|
1359
|
-
chipEl.textContent = plan.alreadyShipped ? "Already live" : paywalled ? "Upgrade required" : "Ready to publish";
|
|
1360
|
-
chipEl.className = "chip " + (plan.alreadyShipped ? "green" : paywalled ? "amber" : "green");
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
const row = (term, value, mono) => {
|
|
1364
|
-
const dt = document.createElement("dt");
|
|
1365
|
-
dt.textContent = term;
|
|
1366
|
-
const dd = document.createElement("dd");
|
|
1367
|
-
if (value instanceof Node) dd.append(value);
|
|
1368
|
-
else {
|
|
1369
|
-
dd.textContent = value;
|
|
1370
|
-
if (mono) dd.className = "publish-mono";
|
|
1371
|
-
}
|
|
1372
|
-
grid.append(dt, dd);
|
|
1373
|
-
};
|
|
1374
|
-
|
|
1375
|
-
row("Reviewed build", shortSha(plan.pinnedSha), true);
|
|
1376
|
-
row("Reviewed digest", shortSha(plan.artifactDigest.replace(/^sha256:/, "")), true);
|
|
1377
|
-
row("Included changes", String((plan.includedPrs || []).length) + " change" + ((plan.includedPrs || []).length === 1 ? "" : "s"));
|
|
1378
|
-
const runCell = document.createElement("span");
|
|
1379
|
-
runCell.append(document.createTextNode(plan.integrationRunId));
|
|
1380
|
-
const planRunLink = runLink(plan.pinnedRevHref, "Open build");
|
|
1381
|
-
if (planRunLink) {
|
|
1382
|
-
runCell.append(document.createTextNode(" · "));
|
|
1383
|
-
runCell.append(planRunLink);
|
|
1384
|
-
}
|
|
1385
|
-
row("Integration run", runCell);
|
|
1386
|
-
if (plan.rollbackTarget) {
|
|
1387
|
-
row("Rollback to", shortSha(plan.rollbackTarget.aggregateSha), true);
|
|
1388
|
-
} else {
|
|
1389
|
-
row("Rollback to", "None (first publish)");
|
|
1390
|
-
}
|
|
1391
|
-
row("Subscription", plan.paywall && plan.paywall.allowed ? "Enabled" : "Not enabled");
|
|
1392
|
-
|
|
1393
|
-
if (note) {
|
|
1394
|
-
note.textContent = paywalled
|
|
1395
|
-
? (plan.paywall.message || "Publishing to your live store requires the paid plan.")
|
|
1396
|
-
: plan.alreadyShipped
|
|
1397
|
-
? "This reviewed build is already live — nothing to publish."
|
|
1398
|
-
: "Publish sends the exact reviewed digest above to your live store. No rebuild.";
|
|
1399
|
-
}
|
|
1400
|
-
};
|
|
1401
|
-
|
|
1402
|
-
// The exact-digest Publish (b10 ship = b09 orchestrator.ship). Confirms the
|
|
1403
|
-
// pinned plan, then POSTs { confirmGoLive, expectedAggregateSha,
|
|
1404
|
-
// expectedArtifactDigest } — the reviewed-digest guard: a stale plan (queue
|
|
1405
|
-
// moved / artifact changed) is refused by the server, never shipped blind.
|
|
1406
|
-
// ── Publish-live ceremony (shipoff) ──────────────────────────────────────
|
|
1407
|
-
// The go-live counterpart of the accept liftoff below (shares its shell,
|
|
1408
|
-
// styles, planRow + celebrate helpers — referenced at runtime, defined a few
|
|
1409
|
-
// sections down). One honest in-flight state: ship is a single POST that
|
|
1410
|
-
// promotes the pinned digest, so no staged theater — just plan → publishing →
|
|
1411
|
-
// outcome with a real "View your live store" link.
|
|
1412
|
-
const shipoff = root.querySelector("[data-shipoff]");
|
|
1413
|
-
const shipoffCard = shipoff && shipoff.querySelector(".liftoff-card");
|
|
1414
|
-
const shipoffEls = shipoff
|
|
1415
|
-
? {
|
|
1416
|
-
plan: shipoff.querySelector("[data-shipoff-plan]"),
|
|
1417
|
-
flight: shipoff.querySelector("[data-shipoff-flight]"),
|
|
1418
|
-
outcome: shipoff.querySelector("[data-shipoff-outcome]"),
|
|
1419
|
-
grid: shipoff.querySelector("[data-shipoff-grid]"),
|
|
1420
|
-
proceed: shipoff.querySelector("[data-shipoff-proceed]"),
|
|
1421
|
-
cancel: shipoff.querySelector("[data-shipoff-cancel]"),
|
|
1422
|
-
live: shipoff.querySelector("[data-shipoff-live]"),
|
|
1423
|
-
elapsed: shipoff.querySelector("[data-shipoff-elapsed]"),
|
|
1424
|
-
seal: shipoff.querySelector("[data-shipoff-seal]"),
|
|
1425
|
-
outcomeTitle: shipoff.querySelector("[data-shipoff-outcome-title]"),
|
|
1426
|
-
outcomeDetail: shipoff.querySelector("[data-shipoff-outcome-detail]"),
|
|
1427
|
-
view: shipoff.querySelector("[data-shipoff-view]"),
|
|
1428
|
-
done: shipoff.querySelector("[data-shipoff-done]"),
|
|
1429
|
-
confetti: shipoff.querySelector("[data-shipoff-confetti]"),
|
|
1430
|
-
}
|
|
1431
|
-
: null;
|
|
1432
|
-
let shipoffTimer = null;
|
|
1433
|
-
const closeShipoff = () => {
|
|
1434
|
-
if (shipoffTimer) { clearInterval(shipoffTimer); shipoffTimer = null; }
|
|
1435
|
-
if (shipoff) shipoff.hidden = true;
|
|
1436
|
-
if (shipoffEls) shipoffEls.confetti.textContent = "";
|
|
1437
|
-
};
|
|
1438
|
-
const showShipoffPhase = (phase) => {
|
|
1439
|
-
if (!shipoffEls) return;
|
|
1440
|
-
shipoffEls.plan.hidden = phase !== "plan";
|
|
1441
|
-
shipoffEls.flight.hidden = phase !== "flight";
|
|
1442
|
-
shipoffEls.outcome.hidden = phase !== "outcome";
|
|
1443
|
-
if (shipoffCard) shipoffCard.dataset.mode = phase;
|
|
1444
|
-
};
|
|
1445
|
-
|
|
1446
|
-
const buildPublishPlanText = (plan) => {
|
|
1447
|
-
const lines = ["Publish plan (exact reviewed digest — no rebuild):"];
|
|
1448
|
-
lines.push(" tenant: " + (appDomain || repo));
|
|
1449
|
-
lines.push(" build: " + shortSha(plan.pinnedSha));
|
|
1450
|
-
lines.push(" digest: " + plan.artifactDigest);
|
|
1451
|
-
lines.push(" changes: " + (plan.includedPrs || []).length + " included");
|
|
1452
|
-
if (plan.rollbackTarget) lines.push(" rollback: " + shortSha(plan.rollbackTarget.aggregateSha));
|
|
1453
|
-
lines.push(" effect: publish this exact digest to your LIVE store.");
|
|
1454
|
-
return lines.join("\n");
|
|
1455
|
-
};
|
|
1456
|
-
|
|
1457
|
-
const shortDigest = (digest) => {
|
|
1458
|
-
const d = String(digest || "").replace(/^sha256:/, "");
|
|
1459
|
-
return d ? "sha256:" + d.slice(0, 12) + "…" : "—";
|
|
1460
|
-
};
|
|
1461
|
-
|
|
1462
|
-
const publishAggregate = (button) => {
|
|
1463
|
-
if (!currentPlan) return;
|
|
1464
|
-
const plan = currentPlan;
|
|
1465
|
-
if (!shipoff || !shipoffEls) {
|
|
1466
|
-
// No-markup fallback only — the ceremony IS the confirm now.
|
|
1467
|
-
if (!window.confirm(buildPublishPlanText(plan) + "\n\nPublish live?")) return;
|
|
1468
|
-
runShip(plan, button);
|
|
1469
|
-
return;
|
|
1470
|
-
}
|
|
1471
|
-
shipoffEls.grid.textContent = "";
|
|
1472
|
-
shipoffEls.grid.append(
|
|
1473
|
-
...planRow("Store", appDomain || repo),
|
|
1474
|
-
...planRow("Build", shortSha(plan.pinnedSha), true),
|
|
1475
|
-
...planRow("Digest", shortDigest(plan.artifactDigest), true),
|
|
1476
|
-
...planRow("Changes", (plan.includedPrs || []).length + " included"),
|
|
1477
|
-
...(plan.rollbackTarget
|
|
1478
|
-
? planRow("Rollback to", shortSha(plan.rollbackTarget.aggregateSha), true)
|
|
1479
|
-
: []),
|
|
1480
|
-
);
|
|
1481
|
-
showShipoffPhase("plan");
|
|
1482
|
-
shipoff.hidden = false;
|
|
1483
|
-
shipoffEls.proceed.focus();
|
|
1484
|
-
shipoffEls.cancel.onclick = () => closeShipoff();
|
|
1485
|
-
shipoffEls.done.onclick = () => closeShipoff();
|
|
1486
|
-
shipoffEls.proceed.onclick = () => runShip(plan, button);
|
|
1487
|
-
};
|
|
1488
|
-
|
|
1489
|
-
const runShip = async (plan, button) => {
|
|
1490
|
-
if (button) button.disabled = true;
|
|
1491
|
-
aggregatePublishing = true; // rux7: hold the button disabled across polls
|
|
1492
|
-
setStatus(aggStatusEl, "Publishing the reviewed build (" + shortSha(plan.pinnedSha) + ") live…");
|
|
1493
|
-
const startedAt = Date.now();
|
|
1494
|
-
if (shipoffEls) {
|
|
1495
|
-
showShipoffPhase("flight");
|
|
1496
|
-
shipoffTimer = setInterval(() => {
|
|
1497
|
-
const s = Math.floor((Date.now() - startedAt) / 1000);
|
|
1498
|
-
shipoffEls.elapsed.textContent = Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0");
|
|
1499
|
-
}, 1000);
|
|
1500
|
-
}
|
|
1501
|
-
const outcome = (state, title, detail, showView) => {
|
|
1502
|
-
if (!shipoffEls) return;
|
|
1503
|
-
if (shipoffTimer) { clearInterval(shipoffTimer); shipoffTimer = null; }
|
|
1504
|
-
shipoffEls.seal.dataset.state = state;
|
|
1505
|
-
shipoffEls.outcomeTitle.textContent = title;
|
|
1506
|
-
shipoffEls.outcomeDetail.textContent = detail;
|
|
1507
|
-
shipoffEls.view.hidden = !showView;
|
|
1508
|
-
if (showView) shipoffEls.view.href = ds.liveHref || "/";
|
|
1509
|
-
showShipoffPhase("outcome");
|
|
1510
|
-
shipoffEls.done.focus();
|
|
1511
|
-
if (state === "ok") celebrate(shipoffEls.confetti);
|
|
1512
|
-
};
|
|
1513
|
-
try {
|
|
1514
|
-
const res = await fetch(ds.shipEndpoint, {
|
|
1515
|
-
method: "POST",
|
|
1516
|
-
headers: { "content-type": "application/json", "x-tot-capability": "ship-on-behalf" },
|
|
1517
|
-
body: JSON.stringify({
|
|
1518
|
-
aggregateId: plan.aggregateId,
|
|
1519
|
-
// The HUMAN go-live gate + the reviewed-digest guards (b09 ship input).
|
|
1520
|
-
confirmGoLive: true,
|
|
1521
|
-
expectedAggregateSha: plan.pinnedSha,
|
|
1522
|
-
expectedArtifactDigest: plan.artifactDigest,
|
|
1523
|
-
}),
|
|
1524
|
-
});
|
|
1525
|
-
const data = await res.json().catch(() => ({}));
|
|
1526
|
-
// b09 AggregateShipResult: the ONLY live claim is state === "shipped".
|
|
1527
|
-
if (data.state === "shipped") {
|
|
1528
|
-
setStatus(
|
|
1529
|
-
aggStatusEl,
|
|
1530
|
-
"Published live · build " + shortSha(data.pinnedSha) + " (receipt " + (data.receiptId || "—") + ").",
|
|
1531
|
-
"ok",
|
|
1532
|
-
);
|
|
1533
|
-
outcome(
|
|
1534
|
-
"ok",
|
|
1535
|
-
"Your store is live",
|
|
1536
|
-
"Build " + shortSha(data.pinnedSha) + " — the exact digest you reviewed — is now serving" +
|
|
1537
|
-
(data.receiptId ? " (receipt " + data.receiptId + ")." : "."),
|
|
1538
|
-
true,
|
|
1539
|
-
);
|
|
1540
|
-
} else {
|
|
1541
|
-
let message;
|
|
1542
|
-
if (data.state === "promote_failed" || data.state === "record_failed") {
|
|
1543
|
-
// Honest partial-failure truth — recoverable, never a false "shipped".
|
|
1544
|
-
message = data.message || "Publish did not complete (" + data.state + "). Retry.";
|
|
1545
|
-
} else if (data.reason === "paywall") {
|
|
1546
|
-
message = data.message || "Publishing to your live store is part of the paid plan.";
|
|
1547
|
-
} else {
|
|
1548
|
-
message = data.message || data.error || "Publish refused (" + (data.reason || res.status) + ")";
|
|
1549
|
-
}
|
|
1550
|
-
setStatus(aggStatusEl, message, "err");
|
|
1551
|
-
outcome("err", "Publish did not complete", message + " Your live store is unchanged.", false);
|
|
1552
|
-
}
|
|
1553
|
-
} catch {
|
|
1554
|
-
setStatus(aggStatusEl, "Publish failed — network error", "err");
|
|
1555
|
-
outcome(
|
|
1556
|
-
"err",
|
|
1557
|
-
"Publish did not complete",
|
|
1558
|
-
"Network error mid-publish — refresh to see the honest state; your live store serves the last published version.",
|
|
1559
|
-
false,
|
|
1560
|
-
);
|
|
1561
|
-
} finally {
|
|
1562
|
-
aggregatePublishing = false;
|
|
1563
|
-
if (button) button.disabled = false;
|
|
1564
|
-
loadQueue();
|
|
1565
|
-
loadVersions();
|
|
1566
|
-
}
|
|
1567
|
-
};
|
|
1568
|
-
|
|
1569
|
-
const aggStatusEl = aggPanel && aggPanel.querySelector("[data-aggregate-status]");
|
|
1570
|
-
aggPanel
|
|
1571
|
-
?.querySelector("[data-aggregate-publish]")
|
|
1572
|
-
?.addEventListener("click", (event) => publishAggregate(event.currentTarget));
|
|
1573
|
-
|
|
1574
|
-
// ── Accept-to-preview LIFTOFF ─────────────────────────────────────────────
|
|
1575
|
-
// The plan confirm + live integration flight + outcome ceremony (replaces
|
|
1576
|
-
// the old window.confirm plan popup). Same b11 contract as before: accept =
|
|
1577
|
-
// `POST /api/changes/integrate` (b08) → integrate into the protected
|
|
1578
|
-
// `preview` aggregate, rebuild + combined-evidence it (b05). Flips NO live
|
|
1579
|
-
// channel — going live is the aggregate card's Publish.
|
|
1580
|
-
//
|
|
1581
|
-
// HONESTY RULE for the stage rail: a stage only advances on a REAL signal —
|
|
1582
|
-
// the outcome POST resolving, or GET /api/changes polling showing the
|
|
1583
|
-
// candidate inside `aggregate.includedPrs` (merge landed) / an `activeRun`
|
|
1584
|
-
// (build+evidence running, statusMessage relayed verbatim). No timers ever
|
|
1585
|
-
// fake progress.
|
|
1586
|
-
const liftoff = root.querySelector("[data-liftoff]");
|
|
1587
|
-
const liftoffCard = liftoff && liftoff.querySelector(".liftoff-card");
|
|
1588
|
-
const liftoffEls = liftoff
|
|
1589
|
-
? {
|
|
1590
|
-
plan: liftoff.querySelector("[data-liftoff-plan]"),
|
|
1591
|
-
flight: liftoff.querySelector("[data-liftoff-flight]"),
|
|
1592
|
-
outcome: liftoff.querySelector("[data-liftoff-outcome]"),
|
|
1593
|
-
title: liftoff.querySelector("[data-liftoff-title]"),
|
|
1594
|
-
grid: liftoff.querySelector("[data-liftoff-grid]"),
|
|
1595
|
-
proceed: liftoff.querySelector("[data-liftoff-proceed]"),
|
|
1596
|
-
cancel: liftoff.querySelector("[data-liftoff-cancel]"),
|
|
1597
|
-
flightTitle: liftoff.querySelector("[data-liftoff-flight-title]"),
|
|
1598
|
-
live: liftoff.querySelector("[data-liftoff-live]"),
|
|
1599
|
-
elapsed: liftoff.querySelector("[data-liftoff-elapsed]"),
|
|
1600
|
-
hide: liftoff.querySelector("[data-liftoff-hide]"),
|
|
1601
|
-
seal: liftoff.querySelector("[data-liftoff-seal]"),
|
|
1602
|
-
outcomeTitle: liftoff.querySelector("[data-liftoff-outcome-title]"),
|
|
1603
|
-
outcomeDetail: liftoff.querySelector("[data-liftoff-outcome-detail]"),
|
|
1604
|
-
review: liftoff.querySelector("[data-liftoff-review]"),
|
|
1605
|
-
done: liftoff.querySelector("[data-liftoff-done]"),
|
|
1606
|
-
confetti: liftoff.querySelector("[data-liftoff-confetti]"),
|
|
1607
|
-
}
|
|
1608
|
-
: null;
|
|
1609
|
-
|
|
1610
|
-
const liftoffStage = (name) =>
|
|
1611
|
-
liftoff && liftoff.querySelector('[data-liftoff-stage="' + name + '"]');
|
|
1612
|
-
const LIFTOFF_STAGES = ["merge", "build", "evidence", "green"];
|
|
1613
|
-
const setStage = (name, state) => {
|
|
1614
|
-
const el = liftoffStage(name);
|
|
1615
|
-
if (el) el.dataset.state = state; // pending | active | done | fail
|
|
1616
|
-
};
|
|
1617
|
-
// Advance the rail so everything before `name` is done and `name` is active.
|
|
1618
|
-
const railTo = (name) => {
|
|
1619
|
-
const idx = LIFTOFF_STAGES.indexOf(name);
|
|
1620
|
-
LIFTOFF_STAGES.forEach((s, i) => {
|
|
1621
|
-
const el = liftoffStage(s);
|
|
1622
|
-
const cur = el && el.dataset.state;
|
|
1623
|
-
if (cur === "fail") return; // never un-fail
|
|
1624
|
-
setStage(s, i < idx ? "done" : i === idx ? "active" : "pending");
|
|
1625
|
-
});
|
|
1626
|
-
};
|
|
1627
|
-
|
|
1628
|
-
let liftoffTimer = null;
|
|
1629
|
-
let liftoffPoll = null;
|
|
1630
|
-
let liftoffOpenedFrom = null;
|
|
1631
|
-
const stopLiftoffLoops = () => {
|
|
1632
|
-
if (liftoffTimer) { clearInterval(liftoffTimer); liftoffTimer = null; }
|
|
1633
|
-
if (liftoffPoll) { clearInterval(liftoffPoll); liftoffPoll = null; }
|
|
1634
|
-
};
|
|
1635
|
-
const closeLiftoff = () => {
|
|
1636
|
-
stopLiftoffLoops();
|
|
1637
|
-
if (liftoff) liftoff.hidden = true;
|
|
1638
|
-
if (liftoffEls) liftoffEls.confetti.textContent = "";
|
|
1639
|
-
if (liftoffOpenedFrom && document.contains(liftoffOpenedFrom)) liftoffOpenedFrom.focus();
|
|
1640
|
-
liftoffOpenedFrom = null;
|
|
1641
|
-
};
|
|
1642
|
-
const showPhase = (phase) => {
|
|
1643
|
-
if (!liftoffEls) return;
|
|
1644
|
-
liftoffEls.plan.hidden = phase !== "plan";
|
|
1645
|
-
liftoffEls.flight.hidden = phase !== "flight";
|
|
1646
|
-
liftoffEls.outcome.hidden = phase !== "outcome";
|
|
1647
|
-
if (liftoffCard) liftoffCard.dataset.mode = phase;
|
|
1648
|
-
};
|
|
1649
|
-
|
|
1650
|
-
const planRow = (dt, dd, mono) => {
|
|
1651
|
-
const t = document.createElement("dt");
|
|
1652
|
-
t.textContent = dt;
|
|
1653
|
-
const d = document.createElement("dd");
|
|
1654
|
-
d.textContent = dd;
|
|
1655
|
-
if (mono) d.className = "publish-mono";
|
|
1656
|
-
return [t, d];
|
|
1657
|
-
};
|
|
1658
|
-
|
|
1659
|
-
const celebrate = (container) => {
|
|
1660
|
-
if (!container) return;
|
|
1661
|
-
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
1662
|
-
const colors = ["#0f9d8f", "#25b8a8", "#f4b942", "#5b8def", "#e2634f"];
|
|
1663
|
-
for (let i = 0; i < 42; i++) {
|
|
1664
|
-
const p = document.createElement("i");
|
|
1665
|
-
p.style.left = Math.random() * 100 + "%";
|
|
1666
|
-
p.style.background = colors[i % colors.length];
|
|
1667
|
-
p.style.animationDelay = Math.random() * 0.35 + "s";
|
|
1668
|
-
p.style.animationDuration = 0.9 + Math.random() * 0.9 + "s";
|
|
1669
|
-
p.style.setProperty("--liftoff-drift", (Math.random() * 160 - 80).toFixed(0) + "px");
|
|
1670
|
-
p.style.setProperty("--liftoff-spin", (Math.random() * 640 - 320).toFixed(0) + "deg");
|
|
1671
|
-
container.append(p);
|
|
1672
|
-
}
|
|
1673
|
-
setTimeout(() => { container.textContent = ""; }, 2400);
|
|
1674
|
-
};
|
|
1675
|
-
|
|
1676
|
-
// The live poll during flight: reflect the server's REAL progress.
|
|
1677
|
-
const startFlightPoll = (change, preFlightSha) => {
|
|
1678
|
-
let inAggregate = false;
|
|
1679
|
-
liftoffPoll = setInterval(async () => {
|
|
1680
|
-
try {
|
|
1681
|
-
const res = await fetch(ds.changesEndpoint, { headers: { accept: "application/json" } });
|
|
1682
|
-
if (!res.ok) return;
|
|
1683
|
-
const data = await res.json();
|
|
1684
|
-
const agg = data.aggregate || {};
|
|
1685
|
-
const included = (agg.includedPrs || []).some((p) => p.changeId === change.changeId);
|
|
1686
|
-
const shaMoved = agg.aggregateSha && agg.aggregateSha !== preFlightSha;
|
|
1687
|
-
const run = agg.activeRun || null;
|
|
1688
|
-
if (!inAggregate && (included || shaMoved)) {
|
|
1689
|
-
inAggregate = true;
|
|
1690
|
-
railTo("build");
|
|
1691
|
-
if (liftoffEls) liftoffEls.live.textContent =
|
|
1692
|
-
"Merged into preview — rebuilding the combined aggregate…";
|
|
1693
|
-
}
|
|
1694
|
-
if (run) {
|
|
1695
|
-
railTo("evidence");
|
|
1696
|
-
if (liftoffEls) liftoffEls.live.textContent =
|
|
1697
|
-
run.statusMessage || "Combined evidence is running…";
|
|
1698
|
-
}
|
|
1699
|
-
} catch { /* polling is best-effort; the POST outcome is authoritative */ }
|
|
1700
|
-
}, 2500);
|
|
1701
|
-
};
|
|
1702
|
-
|
|
1703
|
-
const openLiftoff = (change, button) => {
|
|
1704
|
-
if (!liftoff || !liftoffEls) return false;
|
|
1705
|
-
liftoffOpenedFrom = button || null;
|
|
1706
|
-
const target = change.prNumber != null ? "PR #" + change.prNumber : change.changeId;
|
|
1707
|
-
liftoffEls.title.textContent = "Integrate " + target + " into preview";
|
|
1708
|
-
liftoffEls.grid.textContent = "";
|
|
1709
|
-
liftoffEls.grid.append(
|
|
1710
|
-
...planRow("Store", appDomain || repo),
|
|
1711
|
-
...(change.prNumber != null ? planRow("Pull request", "#" + change.prNumber) : []),
|
|
1712
|
-
...planRow("Change", change.changeId, true),
|
|
1713
|
-
...(change.headSha ? planRow("Head", shortSha(change.headSha), true) : []),
|
|
1714
|
-
);
|
|
1715
|
-
LIFTOFF_STAGES.forEach((s) => setStage(s, "pending"));
|
|
1716
|
-
liftoffEls.live.textContent = "Working…";
|
|
1717
|
-
liftoffEls.elapsed.textContent = "0:00";
|
|
1718
|
-
showPhase("plan");
|
|
1719
|
-
liftoff.hidden = false;
|
|
1720
|
-
liftoffEls.proceed.focus();
|
|
1721
|
-
|
|
1722
|
-
liftoffEls.cancel.onclick = () => closeLiftoff();
|
|
1723
|
-
liftoffEls.done.onclick = () => closeLiftoff();
|
|
1724
|
-
liftoffEls.hide.onclick = () => { liftoff.hidden = true; }; // flight keeps running
|
|
1725
|
-
liftoffEls.proceed.onclick = () => runIntegration(change, button);
|
|
1726
|
-
return true;
|
|
1727
|
-
};
|
|
1728
|
-
|
|
1729
|
-
// Esc: close in plan/outcome; during flight just hide (the POST keeps going).
|
|
1730
|
-
// Handles BOTH ceremonies (accept liftoff + publish shipoff).
|
|
1731
|
-
document.addEventListener("keydown", (ev) => {
|
|
1732
|
-
if (ev.key !== "Escape") return;
|
|
1733
|
-
if (liftoff && !liftoff.hidden) {
|
|
1734
|
-
const mode = liftoffCard && liftoffCard.dataset.mode;
|
|
1735
|
-
if (mode === "flight") liftoff.hidden = true;
|
|
1736
|
-
else closeLiftoff();
|
|
1737
|
-
}
|
|
1738
|
-
if (shipoff && !shipoff.hidden) {
|
|
1739
|
-
const mode = shipoffCard && shipoffCard.dataset.mode;
|
|
1740
|
-
if (mode === "flight") shipoff.hidden = true;
|
|
1741
|
-
else closeShipoff();
|
|
1742
|
-
}
|
|
1743
|
-
});
|
|
1744
|
-
|
|
1745
|
-
// b08 integrate returns an IntegrateOutcome — { ok, queueState, runState,
|
|
1746
|
-
// aggregateSha, pointerMoved, statusMessage, reason? } — NEVER a bare
|
|
1747
|
-
// merged:true. Render it honestly: green advances the aggregate; red left it
|
|
1748
|
-
// exactly where it was and names the failing stage.
|
|
1749
|
-
const runIntegration = async (change, button) => {
|
|
1750
|
-
const target = change.prNumber != null ? "PR #" + change.prNumber : change.changeId;
|
|
1751
|
-
if (button) button.disabled = true;
|
|
1752
|
-
busyChanges.add(change.changeId); // rux7: survive a poll re-render as busy
|
|
1753
|
-
setStatus(statusEl, "Integrating " + change.changeId + " into preview…");
|
|
1754
|
-
const startedAt = Date.now();
|
|
1755
|
-
let preFlightSha = null;
|
|
1756
|
-
if (liftoffEls) {
|
|
1757
|
-
showPhase("flight");
|
|
1758
|
-
liftoffEls.flightTitle.textContent = "Integrating " + target + " into preview";
|
|
1759
|
-
railTo("merge");
|
|
1760
|
-
liftoffEls.live.textContent = "Squash-merging the candidate into preview…";
|
|
1761
|
-
liftoffTimer = setInterval(() => {
|
|
1762
|
-
const s = Math.floor((Date.now() - startedAt) / 1000);
|
|
1763
|
-
liftoffEls.elapsed.textContent = Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0");
|
|
1764
|
-
}, 1000);
|
|
1765
|
-
}
|
|
1766
|
-
try {
|
|
1767
|
-
// Snapshot the pre-flight aggregate sha so the poll can detect the merge
|
|
1768
|
-
// landing (sha moved) even before includedPrs reflects it.
|
|
1769
|
-
try {
|
|
1770
|
-
const pre = await fetch(ds.changesEndpoint, { headers: { accept: "application/json" } });
|
|
1771
|
-
if (pre.ok) preFlightSha = ((await pre.json()).aggregate || {}).aggregateSha || null;
|
|
1772
|
-
} catch { /* best-effort */ }
|
|
1773
|
-
startFlightPoll(change, preFlightSha);
|
|
1774
|
-
|
|
1775
|
-
const res = await fetch(ds.integrateEndpoint, {
|
|
1776
|
-
method: "POST",
|
|
1777
|
-
headers: {
|
|
1778
|
-
"content-type": "application/json",
|
|
1779
|
-
// Mirror of the server-side capability model (UX): the server
|
|
1780
|
-
// re-derives capability and stays authoritative.
|
|
1781
|
-
"x-tot-capability": "ship-on-behalf",
|
|
1782
|
-
},
|
|
1783
|
-
body: JSON.stringify({
|
|
1784
|
-
repo,
|
|
1785
|
-
changeId: change.changeId,
|
|
1786
|
-
prNumber: change.prNumber ?? undefined,
|
|
1787
|
-
expectedHeadSha: change.headSha || undefined,
|
|
1788
|
-
}),
|
|
1789
|
-
});
|
|
1790
|
-
const data = await res.json().catch(() => ({}));
|
|
1791
|
-
stopLiftoffLoops();
|
|
1792
|
-
if (res.ok && data.ok) {
|
|
1793
|
-
setStatus(
|
|
1794
|
-
statusEl,
|
|
1795
|
-
"Integrated " + change.changeId + " — preview aggregate is green (" +
|
|
1796
|
-
shortSha(data.aggregateSha) + "). Review the aggregate, then Publish to go live.",
|
|
1797
|
-
"ok",
|
|
1798
|
-
);
|
|
1799
|
-
if (liftoffEls) {
|
|
1800
|
-
LIFTOFF_STAGES.forEach((s) => setStage(s, "done"));
|
|
1801
|
-
liftoffEls.seal.dataset.state = "ok";
|
|
1802
|
-
liftoffEls.outcomeTitle.textContent = "The preview aggregate is green";
|
|
1803
|
-
liftoffEls.outcomeDetail.textContent =
|
|
1804
|
-
target + " is in. Aggregate build " + shortSha(data.aggregateSha) +
|
|
1805
|
-
" passed its combined evidence — this exact build is what you review, and what " +
|
|
1806
|
-
"Publish would take live. Nothing is live yet.";
|
|
1807
|
-
if (data.aggregateSha) {
|
|
1808
|
-
liftoffEls.review.href =
|
|
1809
|
-
"/preview/" + encodeURIComponent(appDomain) + "/rev/" + data.aggregateSha + "/";
|
|
1810
|
-
liftoffEls.review.hidden = false;
|
|
1811
|
-
} else {
|
|
1812
|
-
liftoffEls.review.hidden = true;
|
|
1813
|
-
}
|
|
1814
|
-
if (liftoff.hidden) liftoff.hidden = false; // surface the payoff even if backgrounded
|
|
1815
|
-
showPhase("outcome");
|
|
1816
|
-
liftoffEls.done.focus();
|
|
1817
|
-
celebrate(liftoffEls.confetti);
|
|
1818
|
-
}
|
|
1819
|
-
} else {
|
|
1820
|
-
let message;
|
|
1821
|
-
let failStage = "merge";
|
|
1822
|
-
if (res.ok && data.queueState === "red") {
|
|
1823
|
-
// The queue ran but the aggregate did not go green — the candidate did
|
|
1824
|
-
// NOT advance the shippable set. Name the honest failing stage.
|
|
1825
|
-
failStage = "evidence";
|
|
1826
|
-
message =
|
|
1827
|
-
"Integration of " + change.changeId + " failed (" + (data.reason || "red") + "): " +
|
|
1828
|
-
(data.statusMessage || "the aggregate did not pass its combined evidence.") +
|
|
1829
|
-
" The aggregate is unchanged.";
|
|
1830
|
-
} else if (data.reason === "not_preview_base") {
|
|
1831
|
-
message =
|
|
1832
|
-
"Not integrated: " + change.changeId + " does not target the preview branch " +
|
|
1833
|
-
"(base is not \"preview\"). " + (data.statusMessage || "");
|
|
1834
|
-
} else {
|
|
1835
|
-
message = data.statusMessage || data.error || "Integrate failed (" + res.status + ")";
|
|
1836
|
-
}
|
|
1837
|
-
setStatus(statusEl, message, "err");
|
|
1838
|
-
if (liftoffEls) {
|
|
1839
|
-
setStage(failStage, "fail");
|
|
1840
|
-
liftoffEls.seal.dataset.state = "err";
|
|
1841
|
-
liftoffEls.outcomeTitle.textContent = "Integration did not land";
|
|
1842
|
-
liftoffEls.outcomeDetail.textContent = message;
|
|
1843
|
-
liftoffEls.review.hidden = true;
|
|
1844
|
-
if (liftoff.hidden) liftoff.hidden = false;
|
|
1845
|
-
showPhase("outcome");
|
|
1846
|
-
liftoffEls.done.focus();
|
|
1847
|
-
}
|
|
1848
|
-
}
|
|
1849
|
-
} catch {
|
|
1850
|
-
stopLiftoffLoops();
|
|
1851
|
-
setStatus(statusEl, "Integrate failed — network error", "err");
|
|
1852
|
-
if (liftoffEls) {
|
|
1853
|
-
liftoffEls.seal.dataset.state = "err";
|
|
1854
|
-
liftoffEls.outcomeTitle.textContent = "Integration did not land";
|
|
1855
|
-
liftoffEls.outcomeDetail.textContent =
|
|
1856
|
-
"Network error mid-flight — refresh the queue to see the aggregate's honest state.";
|
|
1857
|
-
liftoffEls.review.hidden = true;
|
|
1858
|
-
if (liftoff.hidden) liftoff.hidden = false;
|
|
1859
|
-
showPhase("outcome");
|
|
1860
|
-
}
|
|
1861
|
-
} finally {
|
|
1862
|
-
busyChanges.delete(change.changeId);
|
|
1863
|
-
if (button) button.disabled = false;
|
|
1864
|
-
loadQueue();
|
|
1865
|
-
}
|
|
1866
|
-
};
|
|
1867
|
-
|
|
1868
|
-
const integrateCandidate = (change, button) => {
|
|
1869
|
-
// The liftoff dialog IS the plan confirm now; window.confirm stays only as
|
|
1870
|
-
// the no-dialog fallback (markup missing = fail open to the old popup).
|
|
1871
|
-
if (openLiftoff(change, button)) return;
|
|
1872
|
-
const target = change.prNumber != null ? "PR #" + change.prNumber : change.changeId;
|
|
1873
|
-
const plan =
|
|
1874
|
-
"Accept-to-preview plan:\n tenant: " + (appDomain || repo) +
|
|
1875
|
-
(change.prNumber != null ? "\n PR: #" + change.prNumber : "") +
|
|
1876
|
-
"\n change id: " + change.changeId +
|
|
1877
|
-
(change.headSha ? "\n head sha: " + shortSha(change.headSha) : "") +
|
|
1878
|
-
"\n effect: integrate " + target + " into the preview aggregate, then rebuild + " +
|
|
1879
|
-
"re-evidence it. Does NOT go live — publish the reviewed build separately.";
|
|
1880
|
-
if (!window.confirm(plan + "\n\nProceed?")) return;
|
|
1881
|
-
runIntegration(change, button);
|
|
1882
|
-
};
|
|
1883
|
-
|
|
1884
|
-
const rejectChange = async (change, button) => {
|
|
1885
|
-
const reason = window.prompt(
|
|
1886
|
-
"Reject " + change.changeId + " — add a short note for the submitter:",
|
|
1887
|
-
);
|
|
1888
|
-
if (reason == null) return;
|
|
1889
|
-
if (!reason.trim()) {
|
|
1890
|
-
setStatus(statusEl, "A reject note is required.", "err");
|
|
1891
|
-
return;
|
|
1892
|
-
}
|
|
1893
|
-
button.disabled = true;
|
|
1894
|
-
busyChanges.add(change.changeId); // rux7: survive a poll re-render as busy
|
|
1895
|
-
setStatus(statusEl, "Rejecting " + change.changeId + "…");
|
|
1896
|
-
try {
|
|
1897
|
-
const res = await fetch(
|
|
1898
|
-
ds.changesEndpoint + "/" + encodeURIComponent(change.changeId) + "/reject",
|
|
1899
|
-
{
|
|
1900
|
-
method: "POST",
|
|
1901
|
-
headers: {
|
|
1902
|
-
"content-type": "application/json",
|
|
1903
|
-
"x-tot-capability": "ship-on-behalf",
|
|
1904
|
-
},
|
|
1905
|
-
body: JSON.stringify({ repo, reason: reason.trim() }),
|
|
1906
|
-
},
|
|
1907
|
-
);
|
|
1908
|
-
const data = await res.json().catch(() => ({}));
|
|
1909
|
-
if (res.ok) {
|
|
1910
|
-
setStatus(statusEl, "Change " + change.changeId + " rejected.", "ok");
|
|
1911
|
-
} else {
|
|
1912
|
-
setStatus(statusEl, data.error || "Reject failed (" + res.status + ")", "err");
|
|
1913
|
-
}
|
|
1914
|
-
} catch {
|
|
1915
|
-
setStatus(statusEl, "Reject failed — network error", "err");
|
|
1916
|
-
} finally {
|
|
1917
|
-
busyChanges.delete(change.changeId);
|
|
1918
|
-
button.disabled = false;
|
|
1919
|
-
loadQueue();
|
|
1920
|
-
}
|
|
1921
|
-
};
|
|
1922
|
-
|
|
1923
|
-
// U7 — retire/GC. Mirror of the accept plan affordance (decision
|
|
1924
|
-
// operator-verb-and-hosting-model): state EXACTLY what retire does — evict the
|
|
1925
|
-
// candidate's preview environment + immutable version — and that it is
|
|
1926
|
-
// REBUILDABLE (degrades to "not built yet", not a 404), so an operator can't
|
|
1927
|
-
// confuse it with Reject. Same shape as `tot retire`'s CLI plan for parity.
|
|
1928
|
-
const buildRetirePlan = (change) => {
|
|
1929
|
-
const target = change.prNumber != null ? "PR #" + change.prNumber : change.changeId;
|
|
1930
|
-
const lines = ["Retire plan:", " tenant: " + (appDomain || repo)];
|
|
1931
|
-
if (change.prNumber != null) lines.push(" PR: #" + change.prNumber);
|
|
1932
|
-
lines.push(" change id: " + change.changeId);
|
|
1933
|
-
if (change.headSha) lines.push(" head sha: " + shortSha(change.headSha));
|
|
1934
|
-
lines.push(
|
|
1935
|
-
" effect: evict " + target + "'s preview environment + version to reclaim space.",
|
|
1936
|
-
);
|
|
1937
|
-
lines.push(
|
|
1938
|
-
" rebuild: REVERSIBLE — rebuild it any time (this is NOT Reject; the change stays open).",
|
|
1939
|
-
);
|
|
1940
|
-
return lines.join("\n");
|
|
1941
|
-
};
|
|
1942
|
-
|
|
1943
|
-
const retireChange = async (change, button) => {
|
|
1944
|
-
if (!window.confirm(buildRetirePlan(change) + "\n\nProceed?")) return;
|
|
1945
|
-
button.disabled = true;
|
|
1946
|
-
busyChanges.add(change.changeId); // rux7: survive a poll re-render as busy
|
|
1947
|
-
setStatus(statusEl, "Retiring " + change.changeId + "…");
|
|
1948
|
-
try {
|
|
1949
|
-
const res = await fetch(ds.retireEndpoint, {
|
|
1950
|
-
method: "POST",
|
|
1951
|
-
headers: {
|
|
1952
|
-
"content-type": "application/json",
|
|
1953
|
-
// Mirror of the server-side capability model (UX); the server
|
|
1954
|
-
// re-derives capability and stays authoritative.
|
|
1955
|
-
"x-tot-capability": "ship-on-behalf",
|
|
1956
|
-
},
|
|
1957
|
-
body: JSON.stringify({
|
|
1958
|
-
changeId: change.changeId,
|
|
1959
|
-
pr: change.prNumber ?? undefined,
|
|
1960
|
-
}),
|
|
1961
|
-
});
|
|
1962
|
-
const data = await res.json().catch(() => ({}));
|
|
1963
|
-
if (res.ok && data.ok) {
|
|
1964
|
-
setStatus(
|
|
1965
|
-
statusEl,
|
|
1966
|
-
data.evicted
|
|
1967
|
-
? "Retired " + change.changeId + " — the preview was evicted (rebuildable on demand)."
|
|
1968
|
-
: "Retire " + change.changeId + ": nothing to evict (already retired).",
|
|
1969
|
-
"ok",
|
|
1970
|
-
);
|
|
1971
|
-
} else {
|
|
1972
|
-
setStatus(statusEl, data.error || "Retire failed (" + res.status + ")", "err");
|
|
1973
|
-
}
|
|
1974
|
-
} catch {
|
|
1975
|
-
setStatus(statusEl, "Retire failed — network error", "err");
|
|
1976
|
-
} finally {
|
|
1977
|
-
busyChanges.delete(change.changeId);
|
|
1978
|
-
button.disabled = false;
|
|
1979
|
-
loadQueue();
|
|
1980
|
-
}
|
|
1981
|
-
};
|
|
1982
|
-
|
|
1983
|
-
// ---- rux5: build-on-demand for a synthetic not-built PR ------------------
|
|
1984
|
-
// Mirror of the integrate/retire plan affordances: state EXACTLY what build
|
|
1985
|
-
// does — materialize this PR's hosted preview at its forge head — and that it
|
|
1986
|
-
// is INERT (flips no live channel). POSTs { pr, headSha?, baseSha? }; the row
|
|
1987
|
-
// already carries the head sha GET /api/changes resolved via forge PR-read, and
|
|
1988
|
-
// the server re-resolves it authoritatively when the body omits it. Shows an
|
|
1989
|
-
// in-progress state (disabled + "Building…") while the build runs, then reloads
|
|
1990
|
-
// the queue so the freshly-built PR reappears as a built candidate.
|
|
1991
|
-
|
|
1992
|
-
const buildPreviewPlan = (change) => {
|
|
1993
|
-
const lines = ["Build preview plan:", " tenant: " + (appDomain || repo)];
|
|
1994
|
-
lines.push(" PR: #" + change.prNumber);
|
|
1995
|
-
if (change.headSha) lines.push(" head sha: " + shortSha(change.headSha));
|
|
1996
|
-
lines.push(
|
|
1997
|
-
" effect: materialize this PR's hosted preview at its head — INERT " +
|
|
1998
|
-
"(does NOT go live).",
|
|
1999
|
-
);
|
|
2000
|
-
return lines.join("\n");
|
|
2001
|
-
};
|
|
2002
|
-
|
|
2003
|
-
const buildPreview = async (change, button) => {
|
|
2004
|
-
const label = "PR #" + change.prNumber;
|
|
2005
|
-
if (!window.confirm(buildPreviewPlan(change) + "\n\nBuild the preview now?")) return;
|
|
2006
|
-
button.disabled = true;
|
|
2007
|
-
const originalText = button.textContent;
|
|
2008
|
-
button.textContent = "Building…";
|
|
2009
|
-
button.dataset.building = "true";
|
|
2010
|
-
// rux7: record the in-flight build so a poll re-render preserves the
|
|
2011
|
-
// spinner/disabled state on the freshly-rendered button (cleared in finally).
|
|
2012
|
-
if (change.prNumber != null) inflightBuilds.add(change.prNumber);
|
|
2013
|
-
setStatus(statusEl, "Building preview for " + label + "…");
|
|
2014
|
-
try {
|
|
2015
|
-
const res = await fetch(ds.buildEndpoint, {
|
|
2016
|
-
method: "POST",
|
|
2017
|
-
headers: {
|
|
2018
|
-
"content-type": "application/json",
|
|
2019
|
-
// Mirror of the server-side capability model (UX); the server
|
|
2020
|
-
// re-derives capability (buildGate) and stays authoritative.
|
|
2021
|
-
"x-tot-capability": "ship-on-behalf",
|
|
2022
|
-
},
|
|
2023
|
-
body: JSON.stringify({
|
|
2024
|
-
pr: change.prNumber,
|
|
2025
|
-
headSha: change.headSha || undefined,
|
|
2026
|
-
baseSha: change.baseSha || undefined,
|
|
2027
|
-
}),
|
|
2028
|
-
});
|
|
2029
|
-
const data = await res.json().catch(() => ({}));
|
|
2030
|
-
if (res.ok && data.ok) {
|
|
2031
|
-
setStatus(
|
|
2032
|
-
statusEl,
|
|
2033
|
-
"Built preview for " + label +
|
|
2034
|
-
" — it now appears as a built change in the queue.",
|
|
2035
|
-
"ok",
|
|
2036
|
-
);
|
|
2037
|
-
} else if (data && data.ok === false) {
|
|
2038
|
-
// reconcileCandidate returned a blocked/failed candidate (e.g. a safety
|
|
2039
|
-
// finding with no override) — the honest reason, never a false success.
|
|
2040
|
-
setStatus(
|
|
2041
|
-
statusEl,
|
|
2042
|
-
"Build did not complete for " + label + ": " +
|
|
2043
|
-
((data.errors && data.errors.join("; ")) || data.error || "blocked"),
|
|
2044
|
-
"err",
|
|
2045
|
-
);
|
|
2046
|
-
} else {
|
|
2047
|
-
setStatus(statusEl, data.error || "Build failed (" + res.status + ")", "err");
|
|
2048
|
-
}
|
|
2049
|
-
} catch {
|
|
2050
|
-
setStatus(statusEl, "Build failed — network error", "err");
|
|
2051
|
-
} finally {
|
|
2052
|
-
if (change.prNumber != null) inflightBuilds.delete(change.prNumber);
|
|
2053
|
-
button.disabled = false;
|
|
2054
|
-
button.textContent = originalText;
|
|
2055
|
-
delete button.dataset.building;
|
|
2056
|
-
loadQueue();
|
|
2057
|
-
}
|
|
2058
|
-
};
|
|
2059
|
-
|
|
2060
|
-
// ---- rux6: unified review pane -----------------------------------------
|
|
2061
|
-
// "One review moment": expanding a candidate row reveals a single pane with
|
|
2062
|
-
// the live preview (iframed SAME-ORIGIN — the /preview/<tenant>/pr/<N> route
|
|
2063
|
-
// serves `frame-ancestors 'self'` + `X-Frame-Options: SAMEORIGIN`, so a
|
|
2064
|
-
// same-origin admin embed is permitted; a cross-origin embed is refused,
|
|
2065
|
-
// which is why the pane also always offers an "Open in new tab" link), the
|
|
2066
|
-
// evidence chips the row already carries (change.evidence), the dg7 diff
|
|
2067
|
-
// (reusing renderDiff below — GET /api/changes/<id>/diff, which never invents
|
|
2068
|
-
// a diff for a non-renderable candidate), and the SAME Accept-to-preview /
|
|
2069
|
-
// Reject actions. The reviewer never leaves the queue to review and act; the
|
|
2070
|
-
// server stays authoritative for every action (this is UI consolidation only).
|
|
2071
|
-
|
|
2072
|
-
const diffBase = ds.diffEndpointBase;
|
|
2073
|
-
|
|
2074
|
-
const reviewRowFor = (tr) => {
|
|
2075
|
-
const next = tr.nextElementSibling;
|
|
2076
|
-
return next && next.dataset && next.dataset.publishReviewRow === tr.dataset.changeRowId
|
|
2077
|
-
? next
|
|
2078
|
-
: null;
|
|
2079
|
-
};
|
|
2080
|
-
|
|
2081
|
-
const closeReview = (tr, button) => {
|
|
2082
|
-
const existing = reviewRowFor(tr);
|
|
2083
|
-
if (existing) existing.remove();
|
|
2084
|
-
if (button) {
|
|
2085
|
-
button.textContent = "Review";
|
|
2086
|
-
button.setAttribute("aria-expanded", "false");
|
|
2087
|
-
}
|
|
2088
|
-
};
|
|
2089
|
-
|
|
2090
|
-
// Evidence chips from the data the row already carries (change.evidence):
|
|
2091
|
-
// an overall promotable verdict + one chip per reported check.
|
|
2092
|
-
const renderEvidenceChips = (container, change) => {
|
|
2093
|
-
const checks = (change.evidence && change.evidence.checks) || [];
|
|
2094
|
-
const promotable = Boolean(change.evidence && change.evidence.promotable);
|
|
2095
|
-
container.append(
|
|
2096
|
-
promotable ? chip("Checks passed", "green") : chip("Checks blocked", "amber"),
|
|
2097
|
-
);
|
|
2098
|
-
for (const c of checks) {
|
|
2099
|
-
const tone = c.status === "pass" ? "green" : c.status === "fail" ? "red" : "amber";
|
|
2100
|
-
const label = (c.label || c.id || "check") + (c.status && c.status !== "pass" ? " · " + c.status : "");
|
|
2101
|
-
const el = chip(label, tone);
|
|
2102
|
-
if (c.detail) el.title = c.detail;
|
|
2103
|
-
container.append(el);
|
|
2104
|
-
}
|
|
2105
|
-
if (!checks.length) container.append(subtle("No checks reported yet"));
|
|
2106
|
-
};
|
|
2107
|
-
|
|
2108
|
-
// Build the split pane. Returns the pane node + the diff container so the
|
|
2109
|
-
// caller can populate the diff asynchronously (reusing renderDiff).
|
|
2110
|
-
const buildReviewPane = (change) => {
|
|
2111
|
-
const pane = document.createElement("div");
|
|
2112
|
-
pane.className = "review-pane";
|
|
2113
|
-
|
|
2114
|
-
// Preview column — same-origin iframe of the friendly per-PR route (it
|
|
2115
|
-
// handles every state: ready render, or the not-built page). Falls back to
|
|
2116
|
-
// any server-provided previewUrl for a change-first row with no PR number.
|
|
2117
|
-
const previewUrl = candidatePrUrl(change.prNumber) || change.previewUrl;
|
|
2118
|
-
const previewCol = document.createElement("div");
|
|
2119
|
-
previewCol.className = "review-preview";
|
|
2120
|
-
const pHead = document.createElement("div");
|
|
2121
|
-
pHead.className = "review-pane-head";
|
|
2122
|
-
const pTitle = document.createElement("b");
|
|
2123
|
-
pTitle.textContent = "Preview";
|
|
2124
|
-
pHead.append(pTitle);
|
|
2125
|
-
if (previewUrl) {
|
|
2126
|
-
const openLink = document.createElement("a");
|
|
2127
|
-
openLink.href = previewUrl;
|
|
2128
|
-
openLink.target = "_blank";
|
|
2129
|
-
openLink.rel = "noopener";
|
|
2130
|
-
openLink.textContent = "Open in new tab ↗";
|
|
2131
|
-
pHead.append(openLink);
|
|
2132
|
-
}
|
|
2133
|
-
previewCol.append(pHead);
|
|
2134
|
-
if (previewUrl) {
|
|
2135
|
-
const frame = document.createElement("iframe");
|
|
2136
|
-
frame.className = "review-frame";
|
|
2137
|
-
frame.src = previewUrl;
|
|
2138
|
-
frame.loading = "lazy";
|
|
2139
|
-
frame.title =
|
|
2140
|
-
change.prNumber != null
|
|
2141
|
-
? "Preview of PR #" + change.prNumber
|
|
2142
|
-
: "Preview of " + change.changeId;
|
|
2143
|
-
previewCol.append(frame);
|
|
2144
|
-
} else {
|
|
2145
|
-
previewCol.append(subtle("No preview is available for this change yet."));
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
// Detail column — evidence chips + diff + the same Accept/Reject actions.
|
|
2149
|
-
const detailCol = document.createElement("div");
|
|
2150
|
-
detailCol.className = "review-detail";
|
|
2151
|
-
|
|
2152
|
-
const evSection = document.createElement("div");
|
|
2153
|
-
evSection.className = "review-section";
|
|
2154
|
-
const evTitle = document.createElement("b");
|
|
2155
|
-
evTitle.textContent = "Checks";
|
|
2156
|
-
const evChips = document.createElement("div");
|
|
2157
|
-
evChips.className = "review-chips";
|
|
2158
|
-
renderEvidenceChips(evChips, change);
|
|
2159
|
-
evSection.append(evTitle, evChips);
|
|
2160
|
-
detailCol.append(evSection);
|
|
2161
|
-
|
|
2162
|
-
const diffSection = document.createElement("div");
|
|
2163
|
-
diffSection.className = "review-section";
|
|
2164
|
-
const diffTitle = document.createElement("b");
|
|
2165
|
-
diffTitle.textContent = "Diff";
|
|
2166
|
-
const diffTarget = document.createElement("div");
|
|
2167
|
-
diffTarget.className = "review-diff-target";
|
|
2168
|
-
diffTarget.append(subtle("Loading the diff…"));
|
|
2169
|
-
diffSection.append(diffTitle, diffTarget);
|
|
2170
|
-
detailCol.append(diffSection);
|
|
2171
|
-
|
|
2172
|
-
// The SAME actions as the row — same handlers, same gates. Not a new authz
|
|
2173
|
-
// path: integrateCandidate/rejectChange POST to the same endpoints, and the
|
|
2174
|
-
// server re-authorizes.
|
|
2175
|
-
const actionRow = document.createElement("div");
|
|
2176
|
-
actionRow.className = "review-actions";
|
|
2177
|
-
const accept = document.createElement("button");
|
|
2178
|
-
accept.type = "button";
|
|
2179
|
-
accept.className = "btn primary";
|
|
2180
|
-
accept.textContent = "Accept to preview";
|
|
2181
|
-
accept.disabled = !canShip || !change.mergeable;
|
|
2182
|
-
accept.title = !canShip
|
|
2183
|
-
? "Requires an owner or admin sign-in"
|
|
2184
|
-
: !change.mergeable
|
|
2185
|
-
? "This change is not ready to integrate"
|
|
2186
|
-
: "Integrate this change into the preview aggregate (does not go live)";
|
|
2187
|
-
accept.addEventListener("click", () => integrateCandidate(change, accept));
|
|
2188
|
-
const reject = document.createElement("button");
|
|
2189
|
-
reject.type = "button";
|
|
2190
|
-
reject.className = "btn";
|
|
2191
|
-
reject.textContent = "Reject";
|
|
2192
|
-
reject.disabled = !canShip;
|
|
2193
|
-
reject.title = canShip
|
|
2194
|
-
? "Close this change without publishing"
|
|
2195
|
-
: "Requires an owner or admin sign-in";
|
|
2196
|
-
reject.addEventListener("click", () => rejectChange(change, reject));
|
|
2197
|
-
actionRow.append(accept, reject);
|
|
2198
|
-
detailCol.append(actionRow);
|
|
2199
|
-
|
|
2200
|
-
pane.append(previewCol, detailCol);
|
|
2201
|
-
return { pane, diffTarget };
|
|
2202
|
-
};
|
|
2203
|
-
|
|
2204
|
-
const loadReviewDiff = async (change, diffTarget) => {
|
|
2205
|
-
try {
|
|
2206
|
-
const res = await fetch(diffBase + "/" + encodeURIComponent(change.changeId) + "/diff", {
|
|
2207
|
-
headers: { accept: "application/json" },
|
|
2208
|
-
});
|
|
2209
|
-
const data = await res.json().catch(() => ({}));
|
|
2210
|
-
diffTarget.textContent = "";
|
|
2211
|
-
if (!res.ok) {
|
|
2212
|
-
diffTarget.append(subtle(data.error || "Could not load the diff (" + res.status + ")"));
|
|
2213
|
-
} else {
|
|
2214
|
-
renderDiff(diffTarget, data.diff || {});
|
|
2215
|
-
}
|
|
2216
|
-
} catch {
|
|
2217
|
-
diffTarget.textContent = "";
|
|
2218
|
-
diffTarget.append(subtle("Could not load the diff — network error"));
|
|
2219
|
-
}
|
|
2220
|
-
};
|
|
2221
|
-
|
|
2222
|
-
const toggleReview = (change, button, tr) => {
|
|
2223
|
-
if (!tr.dataset.changeRowId) tr.dataset.changeRowId = change.changeId;
|
|
2224
|
-
if (reviewRowFor(tr)) {
|
|
2225
|
-
closeReview(tr, button);
|
|
2226
|
-
return;
|
|
2227
|
-
}
|
|
2228
|
-
const detailTr = document.createElement("tr");
|
|
2229
|
-
detailTr.dataset.publishReviewRow = tr.dataset.changeRowId;
|
|
2230
|
-
const td = document.createElement("td");
|
|
2231
|
-
td.colSpan = 6;
|
|
2232
|
-
td.className = "publish-review-cell";
|
|
2233
|
-
const { pane, diffTarget } = buildReviewPane(change);
|
|
2234
|
-
td.append(pane);
|
|
2235
|
-
detailTr.append(td);
|
|
2236
|
-
tr.after(detailTr);
|
|
2237
|
-
button.textContent = "Hide review";
|
|
2238
|
-
button.setAttribute("aria-expanded", "true");
|
|
2239
|
-
loadReviewDiff(change, diffTarget);
|
|
2240
|
-
};
|
|
2241
|
-
|
|
2242
|
-
const CHANGE_MARK = { added: "+", changed: "~", removed: "−" };
|
|
2243
|
-
|
|
2244
|
-
const renderDiff = (td, diff) => {
|
|
2245
|
-
td.textContent = "";
|
|
2246
|
-
const head = document.createElement("div");
|
|
2247
|
-
head.className = "publish-diff-head";
|
|
2248
|
-
|
|
2249
|
-
// Authority: honest label of whether the diff describes the exact head.
|
|
2250
|
-
const authority = diff.authority || {};
|
|
2251
|
-
const authChip = chip(
|
|
2252
|
-
authority.authoritative
|
|
2253
|
-
? "Reviewed at head"
|
|
2254
|
-
: authority.stale
|
|
2255
|
-
? "Last-good preview (stale vs head)"
|
|
2256
|
-
: authority.missing
|
|
2257
|
-
? "No evidence yet"
|
|
2258
|
-
: "Head",
|
|
2259
|
-
authority.authoritative ? "green" : authority.stale ? "amber" : "",
|
|
2260
|
-
);
|
|
2261
|
-
head.append(authChip);
|
|
2262
|
-
|
|
2263
|
-
if (diff.headSha) {
|
|
2264
|
-
const sha = document.createElement("span");
|
|
2265
|
-
sha.className = "publish-mono subtle";
|
|
2266
|
-
sha.textContent = "head " + shortSha(diff.headSha);
|
|
2267
|
-
head.append(sha);
|
|
2268
|
-
}
|
|
2269
|
-
if (diff.revHref) {
|
|
2270
|
-
const rev = document.createElement("a");
|
|
2271
|
-
rev.href = diff.revHref;
|
|
2272
|
-
rev.target = "_blank";
|
|
2273
|
-
rev.rel = "noopener";
|
|
2274
|
-
rev.textContent = "Open reviewed revision";
|
|
2275
|
-
head.append(rev);
|
|
2276
|
-
}
|
|
2277
|
-
td.append(head);
|
|
2278
|
-
|
|
2279
|
-
if (!diff.renderable) {
|
|
2280
|
-
// Honest: a candidate that can't render has NO diff — say why (dg3), no fake.
|
|
2281
|
-
const reason = document.createElement("p");
|
|
2282
|
-
reason.className = "note publish-diff-blocked";
|
|
2283
|
-
reason.textContent =
|
|
2284
|
-
diff.blockedReason || "This candidate has no renderable version to diff yet.";
|
|
2285
|
-
td.append(reason);
|
|
2286
|
-
return;
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
const files = Array.isArray(diff.files) ? diff.files : [];
|
|
2290
|
-
if (!files.length) {
|
|
2291
|
-
td.append(subtle("No file changes vs the current live version."));
|
|
2292
|
-
return;
|
|
2293
|
-
}
|
|
2294
|
-
const list = document.createElement("ul");
|
|
2295
|
-
list.className = "publish-diff-list";
|
|
2296
|
-
for (const f of files) {
|
|
2297
|
-
const li = document.createElement("li");
|
|
2298
|
-
li.className = "publish-diff-" + f.change;
|
|
2299
|
-
const mark = document.createElement("span");
|
|
2300
|
-
mark.className = "publish-diff-mark";
|
|
2301
|
-
mark.textContent = CHANGE_MARK[f.change] || "•";
|
|
2302
|
-
const path = document.createElement("code");
|
|
2303
|
-
path.textContent = f.path;
|
|
2304
|
-
li.append(mark, path);
|
|
2305
|
-
list.append(li);
|
|
2306
|
-
}
|
|
2307
|
-
td.append(list);
|
|
2308
|
-
};
|
|
2309
|
-
|
|
2310
|
-
// ---- dg7: versions / promotion history + rollback ----------------------
|
|
2311
|
-
// The promotion history (u2/u3 promotion_status) with the tenant Gitea SHA,
|
|
2312
|
-
// its /rev link (u5), and the current-live marker; a prior version rolls back
|
|
2313
|
-
// THROUGH the one orchestrator (/api/changes/rollback → u8), rendering only the
|
|
2314
|
-
// HONEST terminal state (rolled_back only when verified).
|
|
2315
|
-
|
|
2316
|
-
const versionsPanel = root.querySelector("[data-publish-versions]");
|
|
2317
|
-
const versionsBody = versionsPanel?.querySelector("[data-versions-body]");
|
|
2318
|
-
const versionsChip = versionsPanel?.querySelector("[data-versions-chip]");
|
|
2319
|
-
const versionsStatus = versionsPanel?.querySelector("[data-versions-status]");
|
|
2320
|
-
const versionsRollbackNote = versionsPanel?.querySelector("[data-versions-rollback-note]");
|
|
2321
|
-
let rollbackMode = "control-plane";
|
|
2322
|
-
|
|
2323
|
-
const renderVersions = (view) => {
|
|
2324
|
-
if (!versionsBody) return;
|
|
2325
|
-
const versions = Array.isArray(view.versions) ? view.versions : [];
|
|
2326
|
-
rollbackMode = view.rollbackMode || "control-plane";
|
|
2327
|
-
versionsBody.textContent = "";
|
|
2328
|
-
|
|
2329
|
-
if (versionsChip) {
|
|
2330
|
-
versionsChip.textContent = versions.length === 1 ? "1 version" : versions.length + " versions";
|
|
2331
|
-
versionsChip.className = "chip" + (versions.length ? " blue" : "");
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
// Static tenants (rux13): per-version rollback is an HONEST ONE-DISPATCH.
|
|
2335
|
-
// The console can't enumerate the S3 versions archive digest, so it dispatches
|
|
2336
|
-
// by the tenant version id and CI resolves the digest + re-publishes — it
|
|
2337
|
-
// completes via CI, not instantly (never implied one-click).
|
|
2338
|
-
if (versionsRollbackNote) {
|
|
2339
|
-
if (rollbackMode === "static-archive") {
|
|
2340
|
-
versionsRollbackNote.hidden = false;
|
|
2341
|
-
versionsRollbackNote.textContent =
|
|
2342
|
-
"This store publishes to a static public site. Rolling back DISPATCHES a CI job that " +
|
|
2343
|
-
"re-publishes the prior version from the S3 versions archive — it completes via CI, not " +
|
|
2344
|
-
"instantly. The store keeps serving the current version until the re-publish is verified live.";
|
|
2345
|
-
} else {
|
|
2346
|
-
versionsRollbackNote.hidden = true;
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
if (!versions.length) {
|
|
2351
|
-
const tr = document.createElement("tr");
|
|
2352
|
-
const td = cell(subtle("No versions have been promoted to live yet."));
|
|
2353
|
-
td.colSpan = 6;
|
|
2354
|
-
tr.append(td);
|
|
2355
|
-
versionsBody.append(tr);
|
|
2356
|
-
return;
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
for (const v of versions) {
|
|
2360
|
-
const tr = document.createElement("tr");
|
|
2361
|
-
|
|
2362
|
-
// Version identity: the tenant Gitea SHA (content authority) + source.
|
|
2363
|
-
const idCell = document.createElement("td");
|
|
2364
|
-
const strong = document.createElement("strong");
|
|
2365
|
-
strong.className = "publish-mono";
|
|
2366
|
-
strong.textContent = shortSha(v.versionId);
|
|
2367
|
-
idCell.append(strong);
|
|
2368
|
-
if (v.source) idCell.append(subtle(v.source === "commit" ? "From a commit" : "Content hash"));
|
|
2369
|
-
tr.append(idCell);
|
|
2370
|
-
|
|
2371
|
-
// Digest (static current only — never fabricated for priors).
|
|
2372
|
-
const digestCell = cell(v.digest ? shortSha(v.digest) : "—");
|
|
2373
|
-
digestCell.classList.add("publish-mono");
|
|
2374
|
-
if (!v.digest) digestCell.classList.add("subtle");
|
|
2375
|
-
tr.append(digestCell);
|
|
2376
|
-
|
|
2377
|
-
tr.append(cell(age(v.promotedAt)));
|
|
2378
|
-
|
|
2379
|
-
// Preview: the u5 immutable /rev/<sha> link.
|
|
2380
|
-
const revCell = document.createElement("td");
|
|
2381
|
-
if (v.revHref) {
|
|
2382
|
-
const a = document.createElement("a");
|
|
2383
|
-
a.href = v.revHref;
|
|
2384
|
-
a.target = "_blank";
|
|
2385
|
-
a.rel = "noopener";
|
|
2386
|
-
a.textContent = "Revision";
|
|
2387
|
-
revCell.append(a);
|
|
2388
|
-
} else {
|
|
2389
|
-
revCell.append(subtle("—"));
|
|
2390
|
-
}
|
|
2391
|
-
tr.append(revCell);
|
|
2392
|
-
|
|
2393
|
-
// State: current live vs prior.
|
|
2394
|
-
tr.append(cell(v.isCurrent ? chip("Current live", "green") : chip("Prior", "")));
|
|
2395
|
-
|
|
2396
|
-
// Action: roll back to a PRIOR version (never the current one).
|
|
2397
|
-
const act = document.createElement("td");
|
|
2398
|
-
act.className = "action publish-actions";
|
|
2399
|
-
if (!v.isCurrent) {
|
|
2400
|
-
const rollbackBtn = document.createElement("button");
|
|
2401
|
-
rollbackBtn.type = "button";
|
|
2402
|
-
rollbackBtn.className = "btn";
|
|
2403
|
-
const isStatic = rollbackMode === "static-archive";
|
|
2404
|
-
// Honest label: a static rollback completes via CI, not instantly (rux13).
|
|
2405
|
-
rollbackBtn.textContent = isStatic ? "Roll back via CI" : "Roll back";
|
|
2406
|
-
rollbackBtn.disabled = !canShip;
|
|
2407
|
-
rollbackBtn.title = !canShip
|
|
2408
|
-
? "Requires an owner or ship-on-behalf sign-in"
|
|
2409
|
-
: isStatic
|
|
2410
|
-
? "Dispatch a rollback to this version — completes via CI (not instant)"
|
|
2411
|
-
: "Roll the live site back to this version";
|
|
2412
|
-
rollbackBtn.addEventListener("click", () => rollbackVersion(v, rollbackBtn));
|
|
2413
|
-
act.append(rollbackBtn);
|
|
2414
|
-
}
|
|
2415
|
-
tr.append(act);
|
|
2416
|
-
|
|
2417
|
-
versionsBody.append(tr);
|
|
2418
|
-
}
|
|
2419
|
-
};
|
|
2420
|
-
|
|
2421
|
-
const loadVersions = async () => {
|
|
2422
|
-
if (!versionsBody) return;
|
|
2423
|
-
try {
|
|
2424
|
-
const res = await fetch(ds.versionsEndpoint, { headers: { accept: "application/json" } });
|
|
2425
|
-
if (res.status === 401 || res.status === 403) {
|
|
2426
|
-
if (versionsChip) versionsChip.textContent = "Sign-in required";
|
|
2427
|
-
versionsBody.textContent = "";
|
|
2428
|
-
const tr = document.createElement("tr");
|
|
2429
|
-
const td = cell(subtle("Sign in as a store owner or admin to view published versions."));
|
|
2430
|
-
td.colSpan = 6;
|
|
2431
|
-
tr.append(td);
|
|
2432
|
-
versionsBody.append(tr);
|
|
2433
|
-
return;
|
|
2434
|
-
}
|
|
2435
|
-
const data = await res.json().catch(() => ({}));
|
|
2436
|
-
if (!res.ok) {
|
|
2437
|
-
setStatus(versionsStatus, data.error || "Could not load versions (" + res.status + ")", "err");
|
|
2438
|
-
return;
|
|
2439
|
-
}
|
|
2440
|
-
renderVersions(data.view || {});
|
|
2441
|
-
} catch {
|
|
2442
|
-
setStatus(versionsStatus, "Could not load versions — network error", "err");
|
|
2443
|
-
}
|
|
2444
|
-
};
|
|
2445
|
-
|
|
2446
|
-
// Interpret the rollback response into ONE honest terminal state (mirrors the
|
|
2447
|
-
// pure lib/publish/shipWorkspace `interpretRollbackResponse`; duplicated in
|
|
2448
|
-
// plain JS because is:inline scripts can't import modules). `rolledBack` is
|
|
2449
|
-
// true ONLY for a verified `rolled_back` — a `publish_pending` is NEVER a
|
|
2450
|
-
// rolled-back claim.
|
|
2451
|
-
const interpretRollback = (httpOk, body) => {
|
|
2452
|
-
const toVersionId = typeof body.toVersionId === "string" ? body.toVersionId : null;
|
|
2453
|
-
if (typeof body.state === "string") {
|
|
2454
|
-
const state = ["rolled_back", "publish_pending", "publish_failed", "blocked"].includes(body.state)
|
|
2455
|
-
? body.state
|
|
2456
|
-
: "error";
|
|
2457
|
-
return {
|
|
2458
|
-
state,
|
|
2459
|
-
rolledBack: state === "rolled_back",
|
|
2460
|
-
message: body.message || body.error || "Rollback returned state " + state + ".",
|
|
2461
|
-
};
|
|
2462
|
-
}
|
|
2463
|
-
if (httpOk && body.rolledBack === true) {
|
|
2464
|
-
return {
|
|
2465
|
-
state: "rolled_back",
|
|
2466
|
-
rolledBack: true,
|
|
2467
|
-
message: toVersionId ? "Rolled back live to " + shortSha(toVersionId) + "." : "Rolled back live.",
|
|
2468
|
-
};
|
|
2469
|
-
}
|
|
2470
|
-
return {
|
|
2471
|
-
state: "blocked",
|
|
2472
|
-
rolledBack: false,
|
|
2473
|
-
message: (body.error || "Rollback failed") + (body.reason ? " (" + body.reason + ")" : ""),
|
|
2474
|
-
};
|
|
2475
|
-
};
|
|
2476
|
-
|
|
2477
|
-
const rollbackVersion = async (version, button) => {
|
|
2478
|
-
// Static rollback (rux13) is an honest one-DISPATCH — completes via CI, not
|
|
2479
|
-
// instantly; the confirm/status copy must never imply an instant flip.
|
|
2480
|
-
const isStatic = rollbackMode === "static-archive";
|
|
2481
|
-
const confirmMsg = isStatic
|
|
2482
|
-
? "Roll the static public site back to version " + shortSha(version.versionId) +
|
|
2483
|
-
"? This DISPATCHES a rollback that completes via CI (not instantly) — the store keeps " +
|
|
2484
|
-
"serving the current version until CI re-publishes the prior one."
|
|
2485
|
-
: "Roll the live store back to version " + shortSha(version.versionId) +
|
|
2486
|
-
"? This publishes that prior version to the live site.";
|
|
2487
|
-
if (!window.confirm(confirmMsg)) return;
|
|
2488
|
-
button.disabled = true;
|
|
2489
|
-
setStatus(
|
|
2490
|
-
versionsStatus,
|
|
2491
|
-
isStatic
|
|
2492
|
-
? "Dispatching rollback to " + shortSha(version.versionId) + " (completes via CI)…"
|
|
2493
|
-
: "Rolling back to " + shortSha(version.versionId) + "…",
|
|
2494
|
-
);
|
|
2495
|
-
try {
|
|
2496
|
-
// The console dispatches by version id only; a static prior version's
|
|
2497
|
-
// archive digest lives only in S3 (CI resolves it). Pass a digest ONLY when
|
|
2498
|
-
// we honestly have one (never for a static prior — its digest is always null).
|
|
2499
|
-
const payload = { targetVersionId: version.versionId };
|
|
2500
|
-
if (version.digest) payload.targetDigest = version.digest;
|
|
2501
|
-
const res = await fetch(ds.rollbackEndpoint, {
|
|
2502
|
-
method: "POST",
|
|
2503
|
-
headers: { "content-type": "application/json", "x-tot-capability": "ship-on-behalf" },
|
|
2504
|
-
body: JSON.stringify(payload),
|
|
2505
|
-
});
|
|
2506
|
-
const data = await res.json().catch(() => ({}));
|
|
2507
|
-
const outcome = interpretRollback(res.ok, data);
|
|
2508
|
-
setStatus(versionsStatus, outcome.message, outcome.rolledBack ? "ok" : outcome.state === "publish_pending" ? "ok" : "err");
|
|
2509
|
-
} catch {
|
|
2510
|
-
setStatus(versionsStatus, "Rollback failed — network error", "err");
|
|
2511
|
-
} finally {
|
|
2512
|
-
button.disabled = false;
|
|
2513
|
-
loadVersions();
|
|
2514
|
-
loadQueue();
|
|
2515
|
-
}
|
|
2516
|
-
};
|
|
2517
|
-
|
|
2518
|
-
// ---- Domain section ----------------------------------------------------
|
|
2519
|
-
|
|
2520
|
-
const domainPanel = root.querySelector("[data-publish-domain]");
|
|
2521
|
-
let domainPollTimer = null;
|
|
2522
|
-
|
|
2523
|
-
const renderDomain = (payload) => {
|
|
2524
|
-
if (!domainPanel) return;
|
|
2525
|
-
const domain = payload.domain || {};
|
|
2526
|
-
const stateEl = domainPanel.querySelector("[data-domain-state]");
|
|
2527
|
-
const stateChip = domainPanel.querySelector("[data-domain-state-chip]");
|
|
2528
|
-
const sinceEl = domainPanel.querySelector("[data-domain-since]");
|
|
2529
|
-
const captureEl = domainPanel.querySelector("[data-domain-capture]");
|
|
2530
|
-
const runEl = domainPanel.querySelector("[data-domain-run]");
|
|
2531
|
-
const runDetailEl = domainPanel.querySelector("[data-domain-run-detail]");
|
|
2532
|
-
const rollbackBtn = domainPanel.querySelector("[data-domain-rollback]");
|
|
2533
|
-
const rollbackDetail = domainPanel.querySelector("[data-domain-rollback-detail]");
|
|
2534
|
-
const connected = domain.state === "cloudfront";
|
|
2535
|
-
|
|
2536
|
-
if (stateEl) {
|
|
2537
|
-
stateEl.textContent = connected
|
|
2538
|
-
? "Storefront CDN (cloudfront)"
|
|
2539
|
-
: "Previous host (pantheon)";
|
|
2540
|
-
}
|
|
2541
|
-
if (stateChip) {
|
|
2542
|
-
stateChip.textContent = connected ? "Connected" : "Not connected";
|
|
2543
|
-
stateChip.className = "chip " + (connected ? "green" : "amber");
|
|
2544
|
-
}
|
|
2545
|
-
if (sinceEl) {
|
|
2546
|
-
sinceEl.textContent = domain.since
|
|
2547
|
-
? "Since " + new Date(domain.since).toLocaleString()
|
|
2548
|
-
: "Current host for " + (payload.appDomain || ds.appDomain);
|
|
2549
|
-
}
|
|
2550
|
-
if (captureEl) {
|
|
2551
|
-
captureEl.textContent = domain.priorCapturedAt
|
|
2552
|
-
? "Captured " + new Date(domain.priorCapturedAt).toLocaleString()
|
|
2553
|
-
: "None captured";
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
|
-
const run = domain.lastRun;
|
|
2557
|
-
// "dispatched" = awaiting the CI completion callback (terminal-only wire).
|
|
2558
|
-
const inFlight = run && run.status === "dispatched";
|
|
2559
|
-
if (runEl) {
|
|
2560
|
-
runEl.textContent = run
|
|
2561
|
-
? run.action + (run.rehearsal ? " (rehearsal)" : "") + " · " + run.status
|
|
2562
|
-
: "None";
|
|
2563
|
-
}
|
|
2564
|
-
if (runDetailEl) {
|
|
2565
|
-
runDetailEl.textContent = "";
|
|
2566
|
-
if (run) {
|
|
2567
|
-
const parts = [];
|
|
2568
|
-
if (run.actor) parts.push("by " + run.actor);
|
|
2569
|
-
if (run.startedAt) parts.push(age(run.startedAt));
|
|
2570
|
-
runDetailEl.append(document.createTextNode(parts.join(" · ")));
|
|
2571
|
-
if (run.runUrl) {
|
|
2572
|
-
const link = document.createElement("a");
|
|
2573
|
-
link.href = run.runUrl;
|
|
2574
|
-
link.target = "_blank";
|
|
2575
|
-
link.rel = "noopener";
|
|
2576
|
-
link.textContent = "View run";
|
|
2577
|
-
runDetailEl.append(document.createTextNode(" · "));
|
|
2578
|
-
runDetailEl.append(link);
|
|
2579
|
-
}
|
|
2580
|
-
if (run.status === "failed" && run.error) {
|
|
2581
|
-
const err = document.createElement("span");
|
|
2582
|
-
err.className = "publish-error-detail";
|
|
2583
|
-
err.textContent = run.error;
|
|
2584
|
-
runDetailEl.append(document.createElement("br"));
|
|
2585
|
-
runDetailEl.append(err);
|
|
2586
|
-
}
|
|
2587
|
-
} else {
|
|
2588
|
-
runDetailEl.textContent = "Connect and rollback runs appear here.";
|
|
2589
|
-
}
|
|
2590
|
-
}
|
|
2591
|
-
if (rollbackBtn) {
|
|
2592
|
-
rollbackBtn.disabled =
|
|
2593
|
-
!canShip || inFlight || !domain.priorRecordsKey;
|
|
2594
|
-
}
|
|
2595
|
-
if (rollbackDetail && domain.priorCapturedAt) {
|
|
2596
|
-
rollbackDetail.textContent =
|
|
2597
|
-
"Restores the DNS records captured " +
|
|
2598
|
-
new Date(domain.priorCapturedAt).toLocaleString() + ".";
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
// Poll while a run is in flight so completion lands without a manual refresh.
|
|
2602
|
-
if (domainPollTimer) {
|
|
2603
|
-
clearTimeout(domainPollTimer);
|
|
2604
|
-
domainPollTimer = null;
|
|
2605
|
-
}
|
|
2606
|
-
if (inFlight) domainPollTimer = setTimeout(loadDomain, 15000);
|
|
2607
|
-
};
|
|
2608
|
-
|
|
2609
|
-
const loadDomain = async () => {
|
|
2610
|
-
if (!domainPanel) return;
|
|
2611
|
-
try {
|
|
2612
|
-
const res = await fetch(ds.domainStatusEndpoint, {
|
|
2613
|
-
headers: { accept: "application/json" },
|
|
2614
|
-
});
|
|
2615
|
-
if (!res.ok) return;
|
|
2616
|
-
renderDomain(await res.json());
|
|
2617
|
-
} catch {
|
|
2618
|
-
/* leave the last rendered state */
|
|
2619
|
-
}
|
|
2620
|
-
// u9: refresh the readiness itemization alongside the status.
|
|
2621
|
-
loadReadiness();
|
|
2622
|
-
};
|
|
2623
|
-
|
|
2624
|
-
// ---- Cutover readiness (u9) --------------------------------------------
|
|
2625
|
-
// Reads the SAME server gate `tot go-live` reads (/api/domain/readiness) and
|
|
2626
|
-
// itemizes the preconditions. The Connect button additionally requires the
|
|
2627
|
-
// server to report ready — the display and the enforcement can't diverge.
|
|
2628
|
-
const readinessBox = domainPanel?.querySelector("[data-domain-readiness]");
|
|
2629
|
-
const connectBtn = domainPanel?.querySelector("[data-domain-connect]");
|
|
2630
|
-
let domainReady = false;
|
|
2631
|
-
|
|
2632
|
-
const renderReadiness = (payload) => {
|
|
2633
|
-
if (!readinessBox) return;
|
|
2634
|
-
const readiness = (payload && payload.readiness) || {};
|
|
2635
|
-
const checks = Array.isArray(readiness.checks) ? readiness.checks : [];
|
|
2636
|
-
domainReady = readiness.ready === true;
|
|
2637
|
-
readinessBox.hidden = false;
|
|
2638
|
-
|
|
2639
|
-
const chip = readinessBox.querySelector("[data-domain-readiness-chip]");
|
|
2640
|
-
if (chip) {
|
|
2641
|
-
chip.textContent = domainReady ? "Ready" : "Not ready";
|
|
2642
|
-
chip.className = "chip " + (domainReady ? "green" : "amber");
|
|
2643
|
-
}
|
|
2644
|
-
const list = readinessBox.querySelector("[data-domain-readiness-list]");
|
|
2645
|
-
if (list) {
|
|
2646
|
-
list.textContent = "";
|
|
2647
|
-
for (const c of checks) {
|
|
2648
|
-
const li = document.createElement("li");
|
|
2649
|
-
li.className = c.ok ? "ok" : "blocked";
|
|
2650
|
-
const mark = document.createElement("span");
|
|
2651
|
-
mark.className = "domain-readiness-mark";
|
|
2652
|
-
mark.textContent = c.ok ? "✓" : "✗";
|
|
2653
|
-
const label = document.createElement("b");
|
|
2654
|
-
label.textContent = c.label || c.id || "";
|
|
2655
|
-
li.append(mark, label);
|
|
2656
|
-
if (c.detail) {
|
|
2657
|
-
const detail = document.createElement("span");
|
|
2658
|
-
detail.className = "domain-readiness-detail";
|
|
2659
|
-
detail.textContent = c.detail;
|
|
2660
|
-
li.append(detail);
|
|
2661
|
-
}
|
|
2662
|
-
list.append(li);
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
const note = readinessBox.querySelector("[data-domain-readiness-note]");
|
|
2666
|
-
if (note) {
|
|
2667
|
-
note.textContent = payload && payload.ownerCapable === false
|
|
2668
|
-
? "Apex cutover is owner-only — sign in as the store owner to connect the domain."
|
|
2669
|
-
: domainReady
|
|
2670
|
-
? "All preconditions are green — the cutover is permitted."
|
|
2671
|
-
: "Clear the failing preconditions above before connecting the apex.";
|
|
2672
|
-
}
|
|
2673
|
-
// Gate the Connect button on server readiness in ADDITION to owner (the
|
|
2674
|
-
// markup already disables it for non-owners); never enable it when blocked.
|
|
2675
|
-
if (connectBtn && !connectBtn.dataset.ownerLocked) {
|
|
2676
|
-
connectBtn.disabled = !domainReady;
|
|
2677
|
-
}
|
|
2678
|
-
};
|
|
2679
|
-
|
|
2680
|
-
const loadReadiness = async () => {
|
|
2681
|
-
if (!readinessBox || !ds.domainReadinessEndpoint) return;
|
|
2682
|
-
try {
|
|
2683
|
-
const res = await fetch(ds.domainReadinessEndpoint, {
|
|
2684
|
-
headers: { accept: "application/json", "x-tot-capability": "ship-on-behalf" },
|
|
2685
|
-
});
|
|
2686
|
-
if (!res.ok) return;
|
|
2687
|
-
renderReadiness(await res.json());
|
|
2688
|
-
} catch {
|
|
2689
|
-
/* leave the last rendered readiness */
|
|
2690
|
-
}
|
|
2691
|
-
};
|
|
2692
|
-
|
|
2693
|
-
const domainStatusEl = domainPanel?.querySelector("[data-domain-status]");
|
|
2694
|
-
|
|
2695
|
-
const domainAction = async (body, button) => {
|
|
2696
|
-
button.disabled = true;
|
|
2697
|
-
setStatus(domainStatusEl, (body.action === "connect" ? "Connecting" : "Rolling back") + "…");
|
|
2698
|
-
try {
|
|
2699
|
-
const res = await fetch(ds.domainDispatchEndpoint, {
|
|
2700
|
-
method: "POST",
|
|
2701
|
-
headers: {
|
|
2702
|
-
"content-type": "application/json",
|
|
2703
|
-
"x-tot-capability": "ship-on-behalf",
|
|
2704
|
-
},
|
|
2705
|
-
body: JSON.stringify(body),
|
|
2706
|
-
});
|
|
2707
|
-
const data = await res.json().catch(() => ({}));
|
|
2708
|
-
if (res.ok && data.dispatched) {
|
|
2709
|
-
setStatus(
|
|
2710
|
-
domainStatusEl,
|
|
2711
|
-
(body.action === "connect" ? "Connect" : "Rollback") +
|
|
2712
|
-
(body.rehearsal ? " rehearsal" : "") +
|
|
2713
|
-
" dispatched — the run status updates below as it progresses.",
|
|
2714
|
-
"ok",
|
|
2715
|
-
);
|
|
2716
|
-
} else {
|
|
2717
|
-
setStatus(domainStatusEl, data.error || "Request failed (" + res.status + ")", "err");
|
|
2718
|
-
}
|
|
2719
|
-
} catch {
|
|
2720
|
-
setStatus(domainStatusEl, "Request failed — network error", "err");
|
|
2721
|
-
} finally {
|
|
2722
|
-
button.disabled = false;
|
|
2723
|
-
loadDomain();
|
|
2724
|
-
}
|
|
2725
|
-
};
|
|
2726
|
-
|
|
2727
|
-
domainPanel?.querySelector("[data-domain-connect]")?.addEventListener("click", (event) => {
|
|
2728
|
-
const confirmInput = domainPanel.querySelector("[data-domain-confirm]");
|
|
2729
|
-
const rehearsal = Boolean(domainPanel.querySelector("[data-domain-rehearsal]")?.checked);
|
|
2730
|
-
domainAction(
|
|
2731
|
-
{
|
|
2732
|
-
action: "connect",
|
|
2733
|
-
confirmDomain: (confirmInput?.value || "").trim(),
|
|
2734
|
-
rehearsal,
|
|
2735
|
-
},
|
|
2736
|
-
event.currentTarget,
|
|
2737
|
-
);
|
|
2738
|
-
});
|
|
2739
|
-
|
|
2740
|
-
domainPanel?.querySelector("[data-domain-rollback]")?.addEventListener("click", (event) => {
|
|
2741
|
-
if (
|
|
2742
|
-
!window.confirm(
|
|
2743
|
-
"Roll back " + ds.appDomain + " to the previous host? DNS returns to the captured records in about a minute.",
|
|
2744
|
-
)
|
|
2745
|
-
) {
|
|
2746
|
-
return;
|
|
2747
|
-
}
|
|
2748
|
-
domainAction({ action: "rollback" }, event.currentTarget);
|
|
2749
|
-
});
|
|
2750
|
-
|
|
2751
|
-
// ---- b17: branch retention ----------------------------------------------
|
|
2752
|
-
// Renders the GET /api/changes/branches dry-run classification (already
|
|
2753
|
-
// projected server-side by branchRetentionView.ts — this script only
|
|
2754
|
-
// paints it, same division of labor as renderQueue/renderVersions above)
|
|
2755
|
-
// and drives the guarded delete. The server (candidate_delete, b13)
|
|
2756
|
-
// re-classifies fresh on every delete, so the confirm below is the HUMAN
|
|
2757
|
-
// gate, not the safety guarantee — the safety guarantee is server-side.
|
|
2758
|
-
|
|
2759
|
-
const branchesPanel = root.querySelector("[data-publish-branches]");
|
|
2760
|
-
const branchesBody = branchesPanel?.querySelector("[data-branches-body]");
|
|
2761
|
-
const branchesChip = branchesPanel?.querySelector("[data-branches-chip]");
|
|
2762
|
-
const branchesStatus = branchesPanel?.querySelector("[data-branches-status]");
|
|
2763
|
-
|
|
2764
|
-
const BRANCH_TONE = {
|
|
2765
|
-
protected: "",
|
|
2766
|
-
"active-open": "green",
|
|
2767
|
-
"stale-open": "amber",
|
|
2768
|
-
integrated: "blue",
|
|
2769
|
-
"closed-or-rejected": "blue",
|
|
2770
|
-
orphan: "red",
|
|
2771
|
-
};
|
|
2772
|
-
|
|
2773
|
-
const buildCleanupPlan = (row) => {
|
|
2774
|
-
return [
|
|
2775
|
-
"Delete branch plan:",
|
|
2776
|
-
" repo: " + (appDomain || repo),
|
|
2777
|
-
" branch: " + row.ref,
|
|
2778
|
-
" tip sha: " + row.shortSha,
|
|
2779
|
-
" classification: " + row.classificationLabel,
|
|
2780
|
-
" reason: " + row.reason,
|
|
2781
|
-
"",
|
|
2782
|
-
"This deletes the Git BRANCH only — it never evicts a hosted preview build",
|
|
2783
|
-
"(that is Retire, a separate action above). The server re-classifies this",
|
|
2784
|
-
"branch fresh before deleting; a moved tip or a branch that is no longer",
|
|
2785
|
-
"integrated/closed-or-rejected is refused, not silently skipped.",
|
|
2786
|
-
].join("\n");
|
|
2787
|
-
};
|
|
2788
|
-
|
|
2789
|
-
const cleanupBranch = async (row, button) => {
|
|
2790
|
-
if (!window.confirm(buildCleanupPlan(row) + "\n\nDelete this branch?")) return;
|
|
2791
|
-
button.disabled = true;
|
|
2792
|
-
setStatus(branchesStatus, "Deleting " + row.shortRef + "…");
|
|
2793
|
-
try {
|
|
2794
|
-
const res = await fetch(ds.branchesEndpoint, {
|
|
2795
|
-
method: "POST",
|
|
2796
|
-
headers: { "content-type": "application/json", "x-tot-capability": "ship-on-behalf" },
|
|
2797
|
-
body: JSON.stringify({ ref: row.ref, expectedSha: row.sha }),
|
|
2798
|
-
});
|
|
2799
|
-
const data = await res.json().catch(() => ({}));
|
|
2800
|
-
if (res.ok && data.ok) {
|
|
2801
|
-
setStatus(
|
|
2802
|
-
branchesStatus,
|
|
2803
|
-
data.alreadyAbsent
|
|
2804
|
-
? row.shortRef + " was already gone (nothing to delete)."
|
|
2805
|
-
: "Deleted " + row.shortRef + ".",
|
|
2806
|
-
"ok",
|
|
2807
|
-
);
|
|
2808
|
-
} else {
|
|
2809
|
-
setStatus(branchesStatus, data.error || "Delete refused (" + res.status + ")", "err");
|
|
2810
|
-
}
|
|
2811
|
-
} catch {
|
|
2812
|
-
setStatus(branchesStatus, "Delete failed — network error", "err");
|
|
2813
|
-
} finally {
|
|
2814
|
-
button.disabled = false;
|
|
2815
|
-
loadBranches();
|
|
2816
|
-
}
|
|
2817
|
-
};
|
|
2818
|
-
|
|
2819
|
-
const renderBranches = (view) => {
|
|
2820
|
-
if (!branchesBody) return;
|
|
2821
|
-
const rows = Array.isArray(view.rows) ? view.rows : [];
|
|
2822
|
-
if (branchesChip) {
|
|
2823
|
-
const needsReview = rows.filter((r) => r.quarantine || r.eligibleForDelete).length;
|
|
2824
|
-
branchesChip.textContent = needsReview
|
|
2825
|
-
? needsReview + " to review"
|
|
2826
|
-
: rows.length
|
|
2827
|
-
? rows.length + " branches"
|
|
2828
|
-
: "0 branches";
|
|
2829
|
-
branchesChip.className = "chip" + (needsReview ? " amber" : "");
|
|
2830
|
-
}
|
|
2831
|
-
branchesBody.textContent = "";
|
|
2832
|
-
if (!rows.length) {
|
|
2833
|
-
const tr = document.createElement("tr");
|
|
2834
|
-
const td = cell(subtle("No candidate branches found for this repo."));
|
|
2835
|
-
td.colSpan = 7;
|
|
2836
|
-
tr.append(td);
|
|
2837
|
-
branchesBody.append(tr);
|
|
2838
|
-
return;
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
|
-
for (const row of rows) {
|
|
2842
|
-
const tr = document.createElement("tr");
|
|
2843
|
-
|
|
2844
|
-
const branchCell = document.createElement("td");
|
|
2845
|
-
const branchStrong = document.createElement("strong");
|
|
2846
|
-
branchStrong.textContent = row.shortRef;
|
|
2847
|
-
branchCell.append(branchStrong, subtle(row.ref + " @ " + row.shortSha));
|
|
2848
|
-
tr.append(branchCell);
|
|
2849
|
-
|
|
2850
|
-
tr.append(cell(chip(row.classificationLabel, BRANCH_TONE[row.classification] || "")));
|
|
2851
|
-
|
|
2852
|
-
const prCell = document.createElement("td");
|
|
2853
|
-
if (row.prNumber != null) {
|
|
2854
|
-
// Link to the INTERNAL friendly PR route (/preview/<tenant>/pr/<N>), never
|
|
2855
|
-
// the server-provided `row.prUrl` — that is the raw Gitea forge html_url the
|
|
2856
|
-
// MCP candidate_list projection emits (<forge-host>/<org>/<repo>/pulls/N),
|
|
2857
|
-
// and surfacing a raw forge link to an owner is exactly the boundary
|
|
2858
|
-
// noGiteaLinks.test.ts guards (the source scan can't catch a host that
|
|
2859
|
-
// arrives in data, so we refuse it here at render). Mirrors the candidate
|
|
2860
|
-
// queue, which already builds its preview link this way.
|
|
2861
|
-
const prHref = candidatePrUrl(row.prNumber);
|
|
2862
|
-
if (prHref) {
|
|
2863
|
-
const a = document.createElement("a");
|
|
2864
|
-
a.href = prHref;
|
|
2865
|
-
a.target = "_blank";
|
|
2866
|
-
a.rel = "noopener";
|
|
2867
|
-
a.textContent = "PR #" + row.prNumber;
|
|
2868
|
-
prCell.append(a);
|
|
2869
|
-
} else {
|
|
2870
|
-
prCell.append(document.createTextNode("PR #" + row.prNumber));
|
|
2871
|
-
}
|
|
2872
|
-
prCell.append(subtle((row.prState || "") + (row.integratedIntoAggregate ? " · in aggregate" : "")));
|
|
2873
|
-
} else {
|
|
2874
|
-
prCell.append(subtle("No PR record"));
|
|
2875
|
-
}
|
|
2876
|
-
tr.append(prCell);
|
|
2877
|
-
|
|
2878
|
-
tr.append(cell(row.commitTimestampDisplay));
|
|
2879
|
-
tr.append(cell(row.ageLabel));
|
|
2880
|
-
tr.append(cell(row.evidenceSummary));
|
|
2881
|
-
|
|
2882
|
-
const actionsCell = document.createElement("td");
|
|
2883
|
-
actionsCell.className = "action";
|
|
2884
|
-
if (row.quarantine) {
|
|
2885
|
-
actionsCell.append(chip("Needs owner review", "red"));
|
|
2886
|
-
} else if (row.eligibleForDelete) {
|
|
2887
|
-
const btn = document.createElement("button");
|
|
2888
|
-
btn.type = "button";
|
|
2889
|
-
btn.className = "btn";
|
|
2890
|
-
btn.disabled = !canShip;
|
|
2891
|
-
btn.title = canShip ? "" : "Requires an owner/admin sign-in.";
|
|
2892
|
-
btn.textContent = "Clean up";
|
|
2893
|
-
btn.addEventListener("click", () => cleanupBranch(row, btn));
|
|
2894
|
-
actionsCell.append(btn);
|
|
2895
|
-
} else {
|
|
2896
|
-
actionsCell.append(subtle("Keep"));
|
|
2897
|
-
}
|
|
2898
|
-
tr.append(actionsCell);
|
|
2899
|
-
|
|
2900
|
-
branchesBody.append(tr);
|
|
2901
|
-
}
|
|
2902
|
-
};
|
|
2903
|
-
|
|
2904
|
-
const loadBranches = async () => {
|
|
2905
|
-
if (!branchesBody) return;
|
|
2906
|
-
try {
|
|
2907
|
-
const res = await fetch(ds.branchesEndpoint, { headers: { accept: "application/json" } });
|
|
2908
|
-
if (res.status === 401 || res.status === 403) {
|
|
2909
|
-
if (branchesChip) branchesChip.textContent = "Sign-in required";
|
|
2910
|
-
branchesBody.textContent = "";
|
|
2911
|
-
const tr = document.createElement("tr");
|
|
2912
|
-
const td = cell(subtle("Sign in as a store owner or admin to view branch retention."));
|
|
2913
|
-
td.colSpan = 7;
|
|
2914
|
-
tr.append(td);
|
|
2915
|
-
branchesBody.append(tr);
|
|
2916
|
-
return;
|
|
2917
|
-
}
|
|
2918
|
-
const data = await res.json().catch(() => ({}));
|
|
2919
|
-
if (!res.ok) {
|
|
2920
|
-
setStatus(branchesStatus, data.error || "Could not load branch classification (" + res.status + ")", "err");
|
|
2921
|
-
return;
|
|
2922
|
-
}
|
|
2923
|
-
renderBranches(data.report || {});
|
|
2924
|
-
} catch {
|
|
2925
|
-
setStatus(branchesStatus, "Could not load branch classification — network error", "err");
|
|
2926
|
-
}
|
|
2927
|
-
};
|
|
2928
|
-
|
|
2929
|
-
// ---- Wiring ------------------------------------------------------------
|
|
2930
|
-
|
|
2931
|
-
root.querySelector("[data-publish-refresh]")?.addEventListener("click", () => {
|
|
2932
|
-
setStatus(statusEl, "");
|
|
2933
|
-
loadQueue();
|
|
2934
|
-
loadVersions();
|
|
2935
|
-
loadDomain();
|
|
2936
|
-
loadBranches();
|
|
2937
|
-
});
|
|
2938
|
-
|
|
2939
|
-
// rux7: pause polling while hidden; on return, refresh immediately (loadQueue
|
|
2940
|
-
// re-renders + resyncs the poll clock) rather than waiting out the interval.
|
|
2941
|
-
document.addEventListener("visibilitychange", () => {
|
|
2942
|
-
if (document.hidden) {
|
|
2943
|
-
clearPoll();
|
|
2944
|
-
} else {
|
|
2945
|
-
loadQueue();
|
|
2946
|
-
}
|
|
2947
|
-
});
|
|
2948
|
-
|
|
2949
|
-
// loadQueue starts the poll cadence via its tail (schedulePoll).
|
|
2950
|
-
loadQueue();
|
|
2951
|
-
loadVersions();
|
|
2952
|
-
loadDomain();
|
|
2953
|
-
loadBranches();
|
|
2954
|
-
})();
|
|
1198
|
+
<script>
|
|
1199
|
+
import { initAdminPublishTab } from "@/lib/publish/adminPublishTab";
|
|
1200
|
+
initAdminPublishTab();
|
|
2955
1201
|
</script>
|
|
2956
1202
|
|
|
2957
1203
|
<style>
|
|
@@ -2959,11 +1205,241 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
2959
1205
|
min-width: 900px;
|
|
2960
1206
|
}
|
|
2961
1207
|
|
|
1208
|
+
/* r06 — responsive change queue. On a narrow screen the min-width:900px queue
|
|
1209
|
+
table forces horizontal scroll, unusable on a phone. Below ~700px, restack the
|
|
1210
|
+
existing rows into labeled cards — CSS-only, no DOM/JS change: the table, rows
|
|
1211
|
+
and cells become blocks, the visual header is hidden (kept for assistive tech),
|
|
1212
|
+
and each cell gains its column label via a ::before drawn from the fixed thead
|
|
1213
|
+
order. Scoped to #publish-queue so the aggregate/versions tables are untouched.
|
|
1214
|
+
Full-width colspan rows (empty state, sign-in notice, the inline review pane)
|
|
1215
|
+
stay unlabeled and full width. */
|
|
1216
|
+
@media (max-width: 700px) {
|
|
1217
|
+
#publish-queue .publish-table,
|
|
1218
|
+
#publish-queue .publish-table tbody,
|
|
1219
|
+
#publish-queue .publish-table tr,
|
|
1220
|
+
#publish-queue .publish-table td {
|
|
1221
|
+
display: block;
|
|
1222
|
+
min-width: 0;
|
|
1223
|
+
width: auto;
|
|
1224
|
+
}
|
|
1225
|
+
#publish-queue .publish-table thead {
|
|
1226
|
+
position: absolute;
|
|
1227
|
+
width: 1px;
|
|
1228
|
+
height: 1px;
|
|
1229
|
+
overflow: hidden;
|
|
1230
|
+
clip: rect(0 0 0 0);
|
|
1231
|
+
clip-path: inset(50%);
|
|
1232
|
+
white-space: nowrap;
|
|
1233
|
+
}
|
|
1234
|
+
#publish-queue .publish-table tbody tr {
|
|
1235
|
+
margin: 0 0 12px;
|
|
1236
|
+
padding: 10px 12px;
|
|
1237
|
+
border: 1px solid var(--admin-line);
|
|
1238
|
+
border-radius: var(--admin-radius-sm);
|
|
1239
|
+
background: var(--admin-surface, #fff);
|
|
1240
|
+
}
|
|
1241
|
+
#publish-queue .publish-table tbody td {
|
|
1242
|
+
padding: 4px 0;
|
|
1243
|
+
text-align: left;
|
|
1244
|
+
border: 0;
|
|
1245
|
+
}
|
|
1246
|
+
#publish-queue .publish-table tbody td:not([colspan])::before {
|
|
1247
|
+
display: block;
|
|
1248
|
+
margin-bottom: 2px;
|
|
1249
|
+
font-size: 11px;
|
|
1250
|
+
font-weight: 600;
|
|
1251
|
+
text-transform: uppercase;
|
|
1252
|
+
letter-spacing: 0.04em;
|
|
1253
|
+
opacity: 0.6;
|
|
1254
|
+
}
|
|
1255
|
+
#publish-queue .publish-table tbody td:not([colspan]):nth-of-type(1)::before {
|
|
1256
|
+
content: "Change";
|
|
1257
|
+
}
|
|
1258
|
+
#publish-queue .publish-table tbody td:not([colspan]):nth-of-type(2)::before {
|
|
1259
|
+
content: "Status";
|
|
1260
|
+
}
|
|
1261
|
+
#publish-queue .publish-table tbody td:not([colspan]):nth-of-type(3)::before {
|
|
1262
|
+
content: "Checks";
|
|
1263
|
+
}
|
|
1264
|
+
#publish-queue .publish-table tbody td:not([colspan]):nth-of-type(4)::before {
|
|
1265
|
+
content: "Head";
|
|
1266
|
+
}
|
|
1267
|
+
#publish-queue .publish-table tbody td:not([colspan]):nth-of-type(5)::before {
|
|
1268
|
+
content: "Updated";
|
|
1269
|
+
}
|
|
1270
|
+
#publish-queue .publish-table tbody td:not([colspan]):nth-of-type(6)::before {
|
|
1271
|
+
content: "Actions";
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/* Collapsible panels (What needs your approval, Next release, Live Site): the
|
|
1276
|
+
panel-head IS the summary. Kill the native marker, add a rotating chevron, and
|
|
1277
|
+
make the whole head a click target. Mirrors the Rollback options (.versions-details). */
|
|
1278
|
+
.publish-queue-details > summary.panel-head,
|
|
1279
|
+
.closed-changes-details > summary.panel-head,
|
|
1280
|
+
.agg-console-details > summary.panel-head,
|
|
1281
|
+
.live-site-details > summary.panel-head,
|
|
1282
|
+
.versions-details > summary.live-site-subhead {
|
|
1283
|
+
cursor: pointer;
|
|
1284
|
+
list-style: none;
|
|
1285
|
+
user-select: none;
|
|
1286
|
+
}
|
|
1287
|
+
.publish-queue-details > summary.panel-head::-webkit-details-marker,
|
|
1288
|
+
.closed-changes-details > summary.panel-head::-webkit-details-marker,
|
|
1289
|
+
.agg-console-details > summary.panel-head::-webkit-details-marker,
|
|
1290
|
+
.live-site-details > summary.panel-head::-webkit-details-marker,
|
|
1291
|
+
.versions-details > summary.live-site-subhead::-webkit-details-marker {
|
|
1292
|
+
display: none;
|
|
1293
|
+
}
|
|
1294
|
+
.publish-queue-details > summary.panel-head h3::before,
|
|
1295
|
+
.closed-changes-details > summary.panel-head h3::before,
|
|
1296
|
+
.agg-console-details > summary.panel-head h3::before,
|
|
1297
|
+
.live-site-details > summary.panel-head h3::before,
|
|
1298
|
+
.versions-details > summary.live-site-subhead h4::before {
|
|
1299
|
+
content: "";
|
|
1300
|
+
width: 7px;
|
|
1301
|
+
height: 7px;
|
|
1302
|
+
margin-right: 2px;
|
|
1303
|
+
border-right: 2px solid currentColor;
|
|
1304
|
+
border-bottom: 2px solid currentColor;
|
|
1305
|
+
transform: rotate(-45deg);
|
|
1306
|
+
transition: transform 0.15s ease;
|
|
1307
|
+
opacity: 0.6;
|
|
1308
|
+
}
|
|
1309
|
+
.publish-queue-details[open] > summary.panel-head h3::before,
|
|
1310
|
+
.closed-changes-details[open] > summary.panel-head h3::before,
|
|
1311
|
+
.agg-console-details[open] > summary.panel-head h3::before,
|
|
1312
|
+
.live-site-details[open] > summary.panel-head h3::before,
|
|
1313
|
+
.versions-details[open] > summary.live-site-subhead h4::before {
|
|
1314
|
+
transform: rotate(45deg);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/* The collapsed release summary owns one mutually exclusive state affordance:
|
|
1318
|
+
a green current-state chip or the action that advances live. Both classes set
|
|
1319
|
+
display, so explicitly preserve the semantic hidden state. */
|
|
1320
|
+
[data-live-release-state][hidden],
|
|
1321
|
+
.live-release-publish[hidden] {
|
|
1322
|
+
display: none;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/* Live Site — the two clickable public entry points, previously undiscoverable. */
|
|
1326
|
+
.live-site-links {
|
|
1327
|
+
list-style: none;
|
|
1328
|
+
margin: 0;
|
|
1329
|
+
padding: 0;
|
|
1330
|
+
display: grid;
|
|
1331
|
+
gap: 12px;
|
|
1332
|
+
}
|
|
1333
|
+
.live-site-links > li {
|
|
1334
|
+
display: grid;
|
|
1335
|
+
grid-template-columns: minmax(96px, auto) 1fr;
|
|
1336
|
+
align-items: baseline;
|
|
1337
|
+
gap: 4px 12px;
|
|
1338
|
+
}
|
|
1339
|
+
.live-site-link-label {
|
|
1340
|
+
font-weight: 600;
|
|
1341
|
+
font-size: 13px;
|
|
1342
|
+
color: var(--admin-fg, inherit);
|
|
1343
|
+
}
|
|
1344
|
+
.live-site-link-url {
|
|
1345
|
+
word-break: break-all;
|
|
1346
|
+
font-size: 13px;
|
|
1347
|
+
}
|
|
1348
|
+
.live-site-link-note {
|
|
1349
|
+
grid-column: 2;
|
|
1350
|
+
font-size: 12px;
|
|
1351
|
+
opacity: 0.7;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/* r13 — Rollback + Public domain nested INSIDE Live Site read as a subsection,
|
|
1355
|
+
not a sibling panel: a single left rule + generous spacing rather than a boxed
|
|
1356
|
+
card, so the Live Site panel doesn't stack boxes-in-boxes. */
|
|
1357
|
+
.live-site-subsection {
|
|
1358
|
+
border-left: 2px solid var(--admin-line);
|
|
1359
|
+
padding-left: 16px;
|
|
1360
|
+
margin-top: 18px;
|
|
1361
|
+
display: grid;
|
|
1362
|
+
gap: 12px;
|
|
1363
|
+
}
|
|
1364
|
+
/* r13 — the Public-domain status trio (Serving from / Rollback records / Last run)
|
|
1365
|
+
were full `.field` cards (border + fill) nested inside the Live Site panel — the
|
|
1366
|
+
genuine boxes-in-boxes. Flatten them to plain label/value blocks (hooks kept);
|
|
1367
|
+
the subsection's left rule + grid gap carry the separation instead. */
|
|
1368
|
+
.live-site-subsection .field {
|
|
1369
|
+
border: 0;
|
|
1370
|
+
background: transparent;
|
|
1371
|
+
padding: 0;
|
|
1372
|
+
}
|
|
1373
|
+
.live-site-subhead {
|
|
1374
|
+
display: flex;
|
|
1375
|
+
align-items: center;
|
|
1376
|
+
justify-content: space-between;
|
|
1377
|
+
gap: 12px;
|
|
1378
|
+
}
|
|
1379
|
+
.live-site-subhead h4 {
|
|
1380
|
+
margin: 0;
|
|
1381
|
+
display: flex;
|
|
1382
|
+
align-items: center;
|
|
1383
|
+
gap: 8px;
|
|
1384
|
+
font-size: 14px;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
2962
1387
|
.publish-mono {
|
|
2963
1388
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
2964
1389
|
font-size: 12px;
|
|
2965
1390
|
}
|
|
2966
1391
|
|
|
1392
|
+
/* r11 — the human release name beside the "Next release" heading. */
|
|
1393
|
+
.agg-release-name {
|
|
1394
|
+
font-size: 13px;
|
|
1395
|
+
font-weight: 600;
|
|
1396
|
+
color: var(--admin-muted);
|
|
1397
|
+
}
|
|
1398
|
+
.agg-release-name[hidden] {
|
|
1399
|
+
display: none;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/* r02 — the humanized "Live version" metric: "v42" as a link to the live store,
|
|
1403
|
+
with the raw SHA tucked into a "Technical details" disclosure. */
|
|
1404
|
+
.live-version-link {
|
|
1405
|
+
color: inherit;
|
|
1406
|
+
text-decoration: none;
|
|
1407
|
+
border-bottom: 1px solid color-mix(in srgb, currentColor 35%, transparent);
|
|
1408
|
+
}
|
|
1409
|
+
.live-version-link:hover {
|
|
1410
|
+
border-bottom-color: currentColor;
|
|
1411
|
+
}
|
|
1412
|
+
.metric-tech,
|
|
1413
|
+
.ver-tech {
|
|
1414
|
+
margin-top: 4px;
|
|
1415
|
+
font-size: 12px;
|
|
1416
|
+
}
|
|
1417
|
+
.metric-tech > summary,
|
|
1418
|
+
.ver-tech > summary {
|
|
1419
|
+
cursor: pointer;
|
|
1420
|
+
color: var(--muted, #64716d);
|
|
1421
|
+
font-size: 11px;
|
|
1422
|
+
list-style: none;
|
|
1423
|
+
}
|
|
1424
|
+
.metric-tech > summary::-webkit-details-marker,
|
|
1425
|
+
.ver-tech > summary::-webkit-details-marker {
|
|
1426
|
+
display: none;
|
|
1427
|
+
}
|
|
1428
|
+
.ver-tech {
|
|
1429
|
+
margin-top: 6px;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/* r08 — the "Copy preview link" affordance (icon + text, never color-only). */
|
|
1433
|
+
.publish-copy-preview {
|
|
1434
|
+
display: inline-flex;
|
|
1435
|
+
align-items: center;
|
|
1436
|
+
gap: 5px;
|
|
1437
|
+
}
|
|
1438
|
+
.publish-copy-preview svg {
|
|
1439
|
+
width: 14px;
|
|
1440
|
+
height: 14px;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
2967
1443
|
.publish-actions {
|
|
2968
1444
|
white-space: nowrap;
|
|
2969
1445
|
}
|
|
@@ -2972,6 +1448,19 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
2972
1448
|
margin-left: 6px;
|
|
2973
1449
|
}
|
|
2974
1450
|
|
|
1451
|
+
/* The per-row orphan "Delete this branch" action — a deliberate, destructive,
|
|
1452
|
+
one-at-a-time HUMAN delete, styled distinct from the neutral "Clean up" and the
|
|
1453
|
+
bulk "Sweep dead branches" so it never reads as a routine cleanup. */
|
|
1454
|
+
.btn.branch-delete-orphan {
|
|
1455
|
+
border-color: var(--admin-danger, #b3261e);
|
|
1456
|
+
color: var(--admin-danger, #b3261e);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
.btn.branch-delete-orphan:not(:disabled):hover {
|
|
1460
|
+
background: var(--admin-danger, #b3261e);
|
|
1461
|
+
color: #fff;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
2975
1464
|
.publish-status {
|
|
2976
1465
|
min-height: 0;
|
|
2977
1466
|
}
|
|
@@ -2988,13 +1477,166 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
2988
1477
|
color: var(--admin-red);
|
|
2989
1478
|
}
|
|
2990
1479
|
|
|
2991
|
-
/*
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
border-
|
|
2995
|
-
background: var(--admin-
|
|
2996
|
-
|
|
2997
|
-
|
|
1480
|
+
/* Public-site status card, verified-current tone — .risk-alert defaults to an
|
|
1481
|
+
amber/warning look (admin.astro), wrong for "this is fine, confirmed fresh". */
|
|
1482
|
+
.risk-alert.public-site-ok {
|
|
1483
|
+
border-color: var(--admin-green);
|
|
1484
|
+
background: var(--admin-green-soft);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
.risk-alert.public-site-ok svg {
|
|
1488
|
+
color: var(--admin-green);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
/* ── State-driven hero (r01) ─────────────────────────────────────────────
|
|
1492
|
+
The single top-of-tab state element. One row: an icon whose tone conveys
|
|
1493
|
+
urgency (never color alone — a "Action needed" kicker rides the two blocker
|
|
1494
|
+
tones), the one-sentence state + supporting detail, and one primary CTA laid
|
|
1495
|
+
out to the trailing edge (like the paywall gate). Tone is set by
|
|
1496
|
+
renderHero() via [data-tone]. */
|
|
1497
|
+
.publish-hero-region {
|
|
1498
|
+
display: grid;
|
|
1499
|
+
gap: 12px;
|
|
1500
|
+
}
|
|
1501
|
+
.publish-hero {
|
|
1502
|
+
display: flex;
|
|
1503
|
+
align-items: center;
|
|
1504
|
+
justify-content: space-between;
|
|
1505
|
+
gap: 16px;
|
|
1506
|
+
flex-wrap: wrap;
|
|
1507
|
+
padding: 14px 16px;
|
|
1508
|
+
border: 1px solid var(--admin-line-strong);
|
|
1509
|
+
border-left-width: 4px;
|
|
1510
|
+
border-radius: var(--admin-radius, 12px);
|
|
1511
|
+
background: var(--admin-surface);
|
|
1512
|
+
}
|
|
1513
|
+
.publish-hero[hidden] {
|
|
1514
|
+
display: none;
|
|
1515
|
+
}
|
|
1516
|
+
.publish-hero-main {
|
|
1517
|
+
display: flex;
|
|
1518
|
+
align-items: center;
|
|
1519
|
+
gap: 13px;
|
|
1520
|
+
min-width: 0;
|
|
1521
|
+
}
|
|
1522
|
+
.publish-hero-icon {
|
|
1523
|
+
flex: none;
|
|
1524
|
+
width: 34px;
|
|
1525
|
+
height: 34px;
|
|
1526
|
+
display: grid;
|
|
1527
|
+
place-items: center;
|
|
1528
|
+
border-radius: 9px;
|
|
1529
|
+
background: var(--admin-surface-soft);
|
|
1530
|
+
}
|
|
1531
|
+
.publish-hero-icon svg {
|
|
1532
|
+
width: 19px;
|
|
1533
|
+
height: 19px;
|
|
1534
|
+
}
|
|
1535
|
+
.publish-hero-copy {
|
|
1536
|
+
display: grid;
|
|
1537
|
+
gap: 2px;
|
|
1538
|
+
min-width: 0;
|
|
1539
|
+
}
|
|
1540
|
+
.publish-hero-kicker {
|
|
1541
|
+
font-size: 10.5px;
|
|
1542
|
+
font-weight: 760;
|
|
1543
|
+
letter-spacing: 0.08em;
|
|
1544
|
+
text-transform: uppercase;
|
|
1545
|
+
color: var(--admin-red);
|
|
1546
|
+
}
|
|
1547
|
+
.publish-hero-kicker[hidden] {
|
|
1548
|
+
display: none;
|
|
1549
|
+
}
|
|
1550
|
+
.publish-hero-headline {
|
|
1551
|
+
font-size: 15.5px;
|
|
1552
|
+
line-height: 1.3;
|
|
1553
|
+
color: var(--admin-ink);
|
|
1554
|
+
}
|
|
1555
|
+
.publish-hero-detail {
|
|
1556
|
+
font-size: 13px;
|
|
1557
|
+
color: var(--admin-muted);
|
|
1558
|
+
line-height: 1.45;
|
|
1559
|
+
}
|
|
1560
|
+
.publish-hero-detail:empty {
|
|
1561
|
+
display: none;
|
|
1562
|
+
}
|
|
1563
|
+
.publish-hero-actions {
|
|
1564
|
+
flex: none;
|
|
1565
|
+
display: flex;
|
|
1566
|
+
gap: 8px;
|
|
1567
|
+
}
|
|
1568
|
+
.publish-hero-cta[hidden] {
|
|
1569
|
+
display: none;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/* Tones — the accent rail + icon colour. Blocker/warn borrow the admin
|
|
1573
|
+
red/amber tokens; ready/attention the teal; calm stays neutral. */
|
|
1574
|
+
.publish-hero[data-tone="blocker"] {
|
|
1575
|
+
border-left-color: var(--admin-red);
|
|
1576
|
+
background: var(--admin-red-soft);
|
|
1577
|
+
}
|
|
1578
|
+
.publish-hero[data-tone="blocker"] .publish-hero-icon svg {
|
|
1579
|
+
color: var(--admin-red);
|
|
1580
|
+
}
|
|
1581
|
+
.publish-hero[data-tone="warn"] {
|
|
1582
|
+
border-left-color: var(--admin-amber);
|
|
1583
|
+
background: var(--admin-surface-soft);
|
|
1584
|
+
}
|
|
1585
|
+
.publish-hero[data-tone="warn"] .publish-hero-icon svg {
|
|
1586
|
+
color: var(--admin-amber);
|
|
1587
|
+
}
|
|
1588
|
+
.publish-hero[data-tone="attention"] {
|
|
1589
|
+
border-left-color: var(--admin-teal, #0f9d8f);
|
|
1590
|
+
}
|
|
1591
|
+
.publish-hero[data-tone="attention"] .publish-hero-icon svg {
|
|
1592
|
+
color: var(--admin-teal, #0f9d8f);
|
|
1593
|
+
}
|
|
1594
|
+
.publish-hero[data-tone="ready"] {
|
|
1595
|
+
border-left-color: var(--admin-green);
|
|
1596
|
+
background: var(--admin-green-soft);
|
|
1597
|
+
}
|
|
1598
|
+
.publish-hero[data-tone="ready"] .publish-hero-icon svg {
|
|
1599
|
+
color: var(--admin-green);
|
|
1600
|
+
}
|
|
1601
|
+
.publish-hero[data-tone="calm"] {
|
|
1602
|
+
border-left-color: var(--admin-line-strong);
|
|
1603
|
+
}
|
|
1604
|
+
.publish-hero[data-tone="calm"] .publish-hero-icon svg {
|
|
1605
|
+
color: var(--admin-green);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/* r03 stale-public-site alarm — lays its "Open Live Site" deep-link to the
|
|
1609
|
+
trailing edge, like the paywall gate. Inherits the amber .risk-alert look. */
|
|
1610
|
+
.risk-alert.publish-stale-site {
|
|
1611
|
+
display: flex;
|
|
1612
|
+
align-items: center;
|
|
1613
|
+
justify-content: space-between;
|
|
1614
|
+
gap: 12px;
|
|
1615
|
+
flex-wrap: wrap;
|
|
1616
|
+
}
|
|
1617
|
+
.risk-alert.publish-stale-site .btn {
|
|
1618
|
+
flex: none;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/* Per-row finding: the ownership-tag chip leads the row's action cell (what's
|
|
1622
|
+
wrong, in operator words), and the mapped action button gets a subtle ring so
|
|
1623
|
+
the operator sees which button resolves it. Tone (red/amber/neutral) rides the
|
|
1624
|
+
shared .chip convention; primed emphasis is added to the EXISTING button. */
|
|
1625
|
+
.publish-actions .publish-finding-chip {
|
|
1626
|
+
vertical-align: middle;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
.publish-actions .btn.publish-cta-primed {
|
|
1630
|
+
box-shadow: 0 0 0 2px var(--admin-amber);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
/* ?pr=N deep link: "not in the queue" callout + row flash */
|
|
1634
|
+
.publish-pr-callout {
|
|
1635
|
+
border: 1px solid var(--admin-line-strong);
|
|
1636
|
+
border-left: 3px solid var(--admin-amber);
|
|
1637
|
+
background: var(--admin-surface-soft);
|
|
1638
|
+
padding: 8px 12px;
|
|
1639
|
+
border-radius: var(--admin-radius-sm);
|
|
2998
1640
|
color: var(--admin-ink);
|
|
2999
1641
|
}
|
|
3000
1642
|
|
|
@@ -3127,6 +1769,126 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
3127
1769
|
margin-top: 16px;
|
|
3128
1770
|
}
|
|
3129
1771
|
|
|
1772
|
+
/* Merge-conflict guidance (outcome phase, reason "merge_failed" only) —
|
|
1773
|
+
directions, not mood: the terminal recovery front and center, the forge's
|
|
1774
|
+
raw refusal kept small underneath for support/diagnosis. Left-aligned
|
|
1775
|
+
inside the centered outcome phase: commands and prose read as
|
|
1776
|
+
instructions, not ceremony. */
|
|
1777
|
+
.liftoff-conflict {
|
|
1778
|
+
margin-top: 10px;
|
|
1779
|
+
text-align: left;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
.liftoff-commands {
|
|
1783
|
+
margin: 10px 0 8px;
|
|
1784
|
+
padding: 10px 12px;
|
|
1785
|
+
border: 1px solid var(--admin-line-strong);
|
|
1786
|
+
border-radius: var(--admin-radius-sm);
|
|
1787
|
+
background: var(--admin-surface-soft);
|
|
1788
|
+
font-size: 12px;
|
|
1789
|
+
line-height: 1.55;
|
|
1790
|
+
overflow-x: auto;
|
|
1791
|
+
white-space: pre;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
.liftoff-conflict-actions {
|
|
1795
|
+
display: flex;
|
|
1796
|
+
justify-content: flex-end;
|
|
1797
|
+
gap: 8px;
|
|
1798
|
+
flex-wrap: wrap;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
.liftoff-conflict-raw {
|
|
1802
|
+
margin: 10px 0 0;
|
|
1803
|
+
font-size: 11px;
|
|
1804
|
+
color: var(--admin-muted);
|
|
1805
|
+
overflow-wrap: anywhere;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
/* Automate-first recovery: the primary action leads, option cards appear
|
|
1809
|
+
only on a genuine overlap, the terminal guidance is a collapsed fallback. */
|
|
1810
|
+
.liftoff-conflict-summary {
|
|
1811
|
+
margin: 0 0 10px;
|
|
1812
|
+
}
|
|
1813
|
+
.liftoff-conflict-primary {
|
|
1814
|
+
display: flex;
|
|
1815
|
+
flex-direction: column;
|
|
1816
|
+
align-items: flex-start;
|
|
1817
|
+
gap: 8px;
|
|
1818
|
+
margin-bottom: 12px;
|
|
1819
|
+
}
|
|
1820
|
+
.liftoff-conflict-primary .btn.primary {
|
|
1821
|
+
align-self: flex-start;
|
|
1822
|
+
}
|
|
1823
|
+
.liftoff-conflict-primary-status[data-state="err"],
|
|
1824
|
+
.liftoff-conflict-cards-status[data-state="err"] {
|
|
1825
|
+
color: var(--admin-danger, #b3261e);
|
|
1826
|
+
}
|
|
1827
|
+
.liftoff-conflict-cards {
|
|
1828
|
+
margin: 4px 0 12px;
|
|
1829
|
+
padding: 12px;
|
|
1830
|
+
border: 1px solid var(--admin-line-strong);
|
|
1831
|
+
border-radius: var(--admin-radius-sm);
|
|
1832
|
+
background: var(--admin-surface-soft);
|
|
1833
|
+
}
|
|
1834
|
+
.liftoff-conflict-files {
|
|
1835
|
+
list-style: none;
|
|
1836
|
+
margin: 8px 0 12px;
|
|
1837
|
+
padding: 8px 10px;
|
|
1838
|
+
border-radius: var(--admin-radius-sm);
|
|
1839
|
+
background: var(--admin-surface, #fff);
|
|
1840
|
+
font-size: 12px;
|
|
1841
|
+
line-height: 1.6;
|
|
1842
|
+
overflow-x: auto;
|
|
1843
|
+
}
|
|
1844
|
+
.liftoff-conflict-card-grid {
|
|
1845
|
+
display: grid;
|
|
1846
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
1847
|
+
gap: 8px;
|
|
1848
|
+
}
|
|
1849
|
+
.liftoff-option-card {
|
|
1850
|
+
display: flex;
|
|
1851
|
+
flex-direction: column;
|
|
1852
|
+
gap: 4px;
|
|
1853
|
+
text-align: left;
|
|
1854
|
+
padding: 12px;
|
|
1855
|
+
border: 1px solid var(--admin-line-strong);
|
|
1856
|
+
border-radius: var(--admin-radius-sm);
|
|
1857
|
+
background: var(--admin-surface, #fff);
|
|
1858
|
+
cursor: pointer;
|
|
1859
|
+
font: inherit;
|
|
1860
|
+
color: inherit;
|
|
1861
|
+
}
|
|
1862
|
+
.liftoff-option-card:hover:not(:disabled) {
|
|
1863
|
+
border-color: var(--admin-accent, #0f9d8f);
|
|
1864
|
+
}
|
|
1865
|
+
.liftoff-option-card:disabled {
|
|
1866
|
+
opacity: 0.55;
|
|
1867
|
+
cursor: default;
|
|
1868
|
+
}
|
|
1869
|
+
.liftoff-option-card[data-recommended="1"] {
|
|
1870
|
+
border-color: var(--admin-accent, #0f9d8f);
|
|
1871
|
+
box-shadow: inset 0 0 0 1px var(--admin-accent, #0f9d8f);
|
|
1872
|
+
}
|
|
1873
|
+
.liftoff-option-card b {
|
|
1874
|
+
font-size: 13px;
|
|
1875
|
+
}
|
|
1876
|
+
.liftoff-option-card span {
|
|
1877
|
+
font-size: 11px;
|
|
1878
|
+
color: var(--admin-muted);
|
|
1879
|
+
}
|
|
1880
|
+
.liftoff-conflict-expert {
|
|
1881
|
+
margin-top: 10px;
|
|
1882
|
+
border-top: 1px solid var(--admin-line, #e5e5e5);
|
|
1883
|
+
padding-top: 8px;
|
|
1884
|
+
}
|
|
1885
|
+
.liftoff-conflict-expert > summary {
|
|
1886
|
+
cursor: pointer;
|
|
1887
|
+
font-size: 12px;
|
|
1888
|
+
color: var(--admin-muted);
|
|
1889
|
+
user-select: none;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
3130
1892
|
.liftoff-stages {
|
|
3131
1893
|
list-style: none;
|
|
3132
1894
|
margin: 6px 0 4px;
|
|
@@ -3224,6 +1986,31 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
3224
1986
|
color: var(--admin-ink);
|
|
3225
1987
|
}
|
|
3226
1988
|
|
|
1989
|
+
/* The shared scrolling live-log console — a "CI build log" window reused across
|
|
1990
|
+
the build / integrate / rebuild / ship flight dialogs. Fixed height, scrolls,
|
|
1991
|
+
newest line auto-scrolled into view by the client (see makeLogConsole). */
|
|
1992
|
+
.stream-log {
|
|
1993
|
+
margin: 10px 0 0;
|
|
1994
|
+
max-height: 150px;
|
|
1995
|
+
overflow-y: auto;
|
|
1996
|
+
padding: 8px 10px;
|
|
1997
|
+
border: 1px solid var(--admin-border, rgba(0, 0, 0, 0.12));
|
|
1998
|
+
border-radius: 8px;
|
|
1999
|
+
background: var(--admin-code-bg, rgba(0, 0, 0, 0.04));
|
|
2000
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
2001
|
+
font-size: 12px;
|
|
2002
|
+
line-height: 1.5;
|
|
2003
|
+
color: var(--admin-ink);
|
|
2004
|
+
-webkit-overflow-scrolling: touch;
|
|
2005
|
+
}
|
|
2006
|
+
.stream-log:empty {
|
|
2007
|
+
display: none;
|
|
2008
|
+
}
|
|
2009
|
+
.stream-log-line {
|
|
2010
|
+
white-space: pre-wrap;
|
|
2011
|
+
word-break: break-word;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
3227
2014
|
.liftoff-meta {
|
|
3228
2015
|
display: flex;
|
|
3229
2016
|
justify-content: space-between;
|
|
@@ -3348,7 +2135,7 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
3348
2135
|
}
|
|
3349
2136
|
}
|
|
3350
2137
|
|
|
3351
|
-
/*
|
|
2138
|
+
/* Apex-cutover readiness itemization */
|
|
3352
2139
|
.domain-readiness {
|
|
3353
2140
|
margin: 0.75rem 0;
|
|
3354
2141
|
padding: 0.75rem 1rem;
|
|
@@ -3369,14 +2156,19 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
3369
2156
|
gap: 0.25rem;
|
|
3370
2157
|
}
|
|
3371
2158
|
.domain-readiness-list li {
|
|
2159
|
+
display: grid;
|
|
2160
|
+
gap: 0.1rem;
|
|
2161
|
+
font-size: 0.9em;
|
|
2162
|
+
}
|
|
2163
|
+
.domain-readiness-item-head {
|
|
3372
2164
|
display: flex;
|
|
3373
2165
|
align-items: baseline;
|
|
3374
2166
|
gap: 0.4rem;
|
|
3375
|
-
font-size: 0.9em;
|
|
3376
2167
|
}
|
|
3377
2168
|
.domain-readiness-mark {
|
|
3378
2169
|
font-weight: 700;
|
|
3379
2170
|
width: 1em;
|
|
2171
|
+
flex: 0 0 auto;
|
|
3380
2172
|
}
|
|
3381
2173
|
.domain-readiness-list li.ok .domain-readiness-mark {
|
|
3382
2174
|
color: var(--good, #1a7f37);
|
|
@@ -3384,11 +2176,22 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
3384
2176
|
.domain-readiness-list li.blocked .domain-readiness-mark {
|
|
3385
2177
|
color: var(--bad, #b42318);
|
|
3386
2178
|
}
|
|
2179
|
+
/* detail + remedy sit on their OWN lines, indented under the label (past the
|
|
2180
|
+
mark) so neither runs together with the bold label above it. */
|
|
2181
|
+
.domain-readiness-detail,
|
|
2182
|
+
.domain-readiness-remedy {
|
|
2183
|
+
margin: 0;
|
|
2184
|
+
padding-left: 1.4em;
|
|
2185
|
+
}
|
|
3387
2186
|
.domain-readiness-detail {
|
|
3388
2187
|
opacity: 0.75;
|
|
3389
2188
|
}
|
|
2189
|
+
.domain-readiness-remedy {
|
|
2190
|
+
color: var(--accent, #0b6bcb);
|
|
2191
|
+
opacity: 0.95;
|
|
2192
|
+
}
|
|
3390
2193
|
|
|
3391
|
-
/*
|
|
2194
|
+
/* Aggregate card + ship plan */
|
|
3392
2195
|
.aggregate-prs {
|
|
3393
2196
|
display: grid;
|
|
3394
2197
|
gap: 6px;
|
|
@@ -3404,11 +2207,24 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
3404
2207
|
|
|
3405
2208
|
.aggregate-prs-list li {
|
|
3406
2209
|
display: flex;
|
|
3407
|
-
align-items:
|
|
3408
|
-
gap:
|
|
2210
|
+
align-items: center;
|
|
2211
|
+
gap: 8px;
|
|
3409
2212
|
font-size: 13px;
|
|
3410
2213
|
}
|
|
3411
2214
|
|
|
2215
|
+
/* Provenance ("integrated <age> by <email>") sits after the sha, subtle. */
|
|
2216
|
+
.aggregate-pr-meta {
|
|
2217
|
+
font-size: 12px;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
/* The per-change Undo (revert) button is pushed to the row's trailing edge
|
|
2221
|
+
and kept compact so the batch reads as a list, not a stack of buttons. */
|
|
2222
|
+
.aggregate-prs-list .btn-compact {
|
|
2223
|
+
margin-left: auto;
|
|
2224
|
+
padding: 2px 10px;
|
|
2225
|
+
font-size: 12px;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
3412
2228
|
.aggregate-plan {
|
|
3413
2229
|
padding: 0.75rem 1rem;
|
|
3414
2230
|
border: 1px solid var(--hairline, rgba(0, 0, 0, 0.12));
|
|
@@ -3441,12 +2257,612 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
3441
2257
|
overflow-wrap: anywhere;
|
|
3442
2258
|
}
|
|
3443
2259
|
|
|
3444
|
-
/*
|
|
2260
|
+
/* ── Preview-aggregate console — the go-live surface ─────────────────────
|
|
2261
|
+
Everything here is scoped under [data-publish-aggregate] / .agg-* so it
|
|
2262
|
+
never leaks into the other panels (candidate queue, versions, branches)
|
|
2263
|
+
that share .panel / .field / .check-row / .chip. */
|
|
2264
|
+
.agg-console .panel-body {
|
|
2265
|
+
gap: 16px;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
[data-publish-aggregate] .sha-token {
|
|
2269
|
+
display: inline-flex;
|
|
2270
|
+
align-items: center;
|
|
2271
|
+
padding: 2px 8px;
|
|
2272
|
+
border: 1px solid var(--admin-line-strong);
|
|
2273
|
+
border-radius: 5px;
|
|
2274
|
+
background: var(--admin-surface-soft);
|
|
2275
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
2276
|
+
font-size: 12.5px;
|
|
2277
|
+
color: var(--admin-ink);
|
|
2278
|
+
}
|
|
2279
|
+
[data-publish-aggregate] .sha-token.ghost {
|
|
2280
|
+
background: transparent;
|
|
2281
|
+
color: var(--admin-muted);
|
|
2282
|
+
border-style: dashed;
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
/* AI-assembled release summary */
|
|
2286
|
+
.agg-summary {
|
|
2287
|
+
display: grid;
|
|
2288
|
+
gap: 11px;
|
|
2289
|
+
padding: 15px 16px;
|
|
2290
|
+
border: 1px solid var(--admin-line);
|
|
2291
|
+
border-radius: var(--admin-radius);
|
|
2292
|
+
background:
|
|
2293
|
+
linear-gradient(180deg, var(--admin-teal-soft), transparent 62%),
|
|
2294
|
+
var(--admin-surface);
|
|
2295
|
+
}
|
|
2296
|
+
.agg-summary[hidden] {
|
|
2297
|
+
display: none;
|
|
2298
|
+
}
|
|
2299
|
+
.agg-summary-head {
|
|
2300
|
+
display: flex;
|
|
2301
|
+
align-items: center;
|
|
2302
|
+
gap: 10px;
|
|
2303
|
+
}
|
|
2304
|
+
.agg-summary-glyph {
|
|
2305
|
+
flex: none;
|
|
2306
|
+
width: 26px;
|
|
2307
|
+
height: 26px;
|
|
2308
|
+
display: grid;
|
|
2309
|
+
place-items: center;
|
|
2310
|
+
border-radius: 7px;
|
|
2311
|
+
background: var(--admin-surface);
|
|
2312
|
+
color: var(--admin-teal-dark);
|
|
2313
|
+
border: 1px solid color-mix(in srgb, var(--admin-teal) 30%, transparent);
|
|
2314
|
+
}
|
|
2315
|
+
/* The sparkle glyphs are filled shapes; opt out of the global stroked-svg rule. */
|
|
2316
|
+
.agg-summary-glyph svg,
|
|
2317
|
+
.agg-ai-chip svg {
|
|
2318
|
+
fill: currentColor;
|
|
2319
|
+
stroke: none;
|
|
2320
|
+
}
|
|
2321
|
+
.agg-summary-glyph svg {
|
|
2322
|
+
width: 15px;
|
|
2323
|
+
height: 15px;
|
|
2324
|
+
}
|
|
2325
|
+
.agg-summary-label {
|
|
2326
|
+
flex: 1;
|
|
2327
|
+
font-size: 11px;
|
|
2328
|
+
font-weight: 760;
|
|
2329
|
+
letter-spacing: 0.08em;
|
|
2330
|
+
text-transform: uppercase;
|
|
2331
|
+
color: var(--admin-muted);
|
|
2332
|
+
}
|
|
2333
|
+
.agg-ai-chip {
|
|
2334
|
+
display: inline-flex;
|
|
2335
|
+
align-items: center;
|
|
2336
|
+
gap: 5px;
|
|
2337
|
+
padding: 3px 9px;
|
|
2338
|
+
border-radius: 999px;
|
|
2339
|
+
background: var(--admin-teal-soft);
|
|
2340
|
+
color: #007973;
|
|
2341
|
+
font-size: 10px;
|
|
2342
|
+
font-weight: 760;
|
|
2343
|
+
letter-spacing: 0.06em;
|
|
2344
|
+
text-transform: uppercase;
|
|
2345
|
+
}
|
|
2346
|
+
.agg-ai-chip svg {
|
|
2347
|
+
width: 10px;
|
|
2348
|
+
height: 10px;
|
|
2349
|
+
}
|
|
2350
|
+
.agg-summary-lead {
|
|
2351
|
+
margin: 0;
|
|
2352
|
+
font-size: 15px;
|
|
2353
|
+
line-height: 1.55;
|
|
2354
|
+
color: var(--admin-ink);
|
|
2355
|
+
max-width: 68ch;
|
|
2356
|
+
}
|
|
2357
|
+
.agg-summary-points {
|
|
2358
|
+
list-style: none;
|
|
2359
|
+
margin: 0;
|
|
2360
|
+
padding: 0;
|
|
2361
|
+
display: grid;
|
|
2362
|
+
gap: 7px;
|
|
2363
|
+
}
|
|
2364
|
+
.agg-summary-points[hidden] {
|
|
2365
|
+
display: none;
|
|
2366
|
+
}
|
|
2367
|
+
.agg-summary-points li {
|
|
2368
|
+
display: flex;
|
|
2369
|
+
align-items: flex-start;
|
|
2370
|
+
gap: 9px;
|
|
2371
|
+
font-size: 13px;
|
|
2372
|
+
color: var(--admin-muted);
|
|
2373
|
+
line-height: 1.45;
|
|
2374
|
+
}
|
|
2375
|
+
.agg-summary-points .agg-tick {
|
|
2376
|
+
flex: none;
|
|
2377
|
+
margin-top: 1px;
|
|
2378
|
+
width: 16px;
|
|
2379
|
+
height: 16px;
|
|
2380
|
+
border-radius: 50%;
|
|
2381
|
+
display: grid;
|
|
2382
|
+
place-items: center;
|
|
2383
|
+
background: var(--admin-green-soft);
|
|
2384
|
+
color: var(--admin-green);
|
|
2385
|
+
}
|
|
2386
|
+
.agg-summary-points .agg-tick svg {
|
|
2387
|
+
width: 10px;
|
|
2388
|
+
height: 10px;
|
|
2389
|
+
}
|
|
2390
|
+
.agg-summary-points b {
|
|
2391
|
+
color: var(--admin-ink);
|
|
2392
|
+
font-weight: 640;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
/* Chain of custody: build → evidence → digest */
|
|
2396
|
+
.agg-chain {
|
|
2397
|
+
display: grid;
|
|
2398
|
+
grid-template-columns: 1fr auto 1fr auto 1fr;
|
|
2399
|
+
align-items: stretch;
|
|
2400
|
+
padding: 16px;
|
|
2401
|
+
border: 1px solid var(--admin-line);
|
|
2402
|
+
border-radius: var(--admin-radius);
|
|
2403
|
+
background: var(--admin-surface-soft);
|
|
2404
|
+
}
|
|
2405
|
+
.agg-stop {
|
|
2406
|
+
display: flex;
|
|
2407
|
+
flex-direction: column;
|
|
2408
|
+
gap: 7px;
|
|
2409
|
+
padding: 2px 12px;
|
|
2410
|
+
min-width: 0;
|
|
2411
|
+
}
|
|
2412
|
+
.agg-stop:first-child {
|
|
2413
|
+
padding-left: 2px;
|
|
2414
|
+
}
|
|
2415
|
+
.agg-stop:last-child {
|
|
2416
|
+
padding-right: 2px;
|
|
2417
|
+
}
|
|
2418
|
+
.agg-stop-label {
|
|
2419
|
+
font-size: 10.5px;
|
|
2420
|
+
font-weight: 760;
|
|
2421
|
+
letter-spacing: 0.08em;
|
|
2422
|
+
text-transform: uppercase;
|
|
2423
|
+
color: var(--admin-muted);
|
|
2424
|
+
}
|
|
2425
|
+
.agg-stop-val {
|
|
2426
|
+
display: flex;
|
|
2427
|
+
align-items: center;
|
|
2428
|
+
gap: 8px;
|
|
2429
|
+
font-size: 15px;
|
|
2430
|
+
font-weight: 680;
|
|
2431
|
+
}
|
|
2432
|
+
.agg-stop-val strong {
|
|
2433
|
+
font-size: 15px;
|
|
2434
|
+
}
|
|
2435
|
+
.agg-stop-cap {
|
|
2436
|
+
margin: 0;
|
|
2437
|
+
font-size: 12px;
|
|
2438
|
+
color: var(--admin-muted);
|
|
2439
|
+
line-height: 1.45;
|
|
2440
|
+
}
|
|
2441
|
+
.agg-stop-cap a {
|
|
2442
|
+
white-space: nowrap;
|
|
2443
|
+
}
|
|
2444
|
+
.agg-digest {
|
|
2445
|
+
padding: 2px 8px;
|
|
2446
|
+
border: 1px solid color-mix(in srgb, var(--admin-teal) 55%, var(--admin-line-strong));
|
|
2447
|
+
border-radius: 5px;
|
|
2448
|
+
background: var(--admin-teal-soft);
|
|
2449
|
+
color: #007973;
|
|
2450
|
+
}
|
|
2451
|
+
[data-publish-aggregate] [data-aggregate-evidence][data-state="passed"] {
|
|
2452
|
+
color: var(--admin-green);
|
|
2453
|
+
}
|
|
2454
|
+
[data-publish-aggregate] [data-aggregate-evidence][data-state="failed"] {
|
|
2455
|
+
color: var(--admin-red);
|
|
2456
|
+
}
|
|
2457
|
+
.agg-link {
|
|
2458
|
+
display: flex;
|
|
2459
|
+
flex-direction: column;
|
|
2460
|
+
align-items: center;
|
|
2461
|
+
justify-content: center;
|
|
2462
|
+
gap: 6px;
|
|
2463
|
+
padding: 0 2px;
|
|
2464
|
+
align-self: center;
|
|
2465
|
+
}
|
|
2466
|
+
.agg-flowlabel {
|
|
2467
|
+
font-size: 9px;
|
|
2468
|
+
font-weight: 720;
|
|
2469
|
+
letter-spacing: 0.06em;
|
|
2470
|
+
text-transform: uppercase;
|
|
2471
|
+
color: var(--admin-muted-2);
|
|
2472
|
+
}
|
|
2473
|
+
.agg-arrow {
|
|
2474
|
+
display: grid;
|
|
2475
|
+
place-items: center;
|
|
2476
|
+
color: var(--admin-teal);
|
|
2477
|
+
}
|
|
2478
|
+
.agg-arrow svg {
|
|
2479
|
+
width: 32px;
|
|
2480
|
+
height: 10px;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
/* Staged → live delta strip */
|
|
2484
|
+
.agg-delta-strip {
|
|
2485
|
+
display: flex;
|
|
2486
|
+
align-items: center;
|
|
2487
|
+
gap: 12px;
|
|
2488
|
+
flex-wrap: wrap;
|
|
2489
|
+
padding: 10px 13px;
|
|
2490
|
+
border: 1px solid var(--admin-line);
|
|
2491
|
+
border-radius: var(--admin-radius-sm);
|
|
2492
|
+
background: var(--admin-surface-soft);
|
|
2493
|
+
}
|
|
2494
|
+
.agg-delta-strip[hidden] {
|
|
2495
|
+
display: none;
|
|
2496
|
+
}
|
|
2497
|
+
.agg-delta-node {
|
|
2498
|
+
display: flex;
|
|
2499
|
+
flex-direction: column;
|
|
2500
|
+
gap: 3px;
|
|
2501
|
+
}
|
|
2502
|
+
.agg-delta-mid {
|
|
2503
|
+
display: flex;
|
|
2504
|
+
align-items: center;
|
|
2505
|
+
gap: 8px;
|
|
2506
|
+
color: var(--admin-teal);
|
|
2507
|
+
}
|
|
2508
|
+
.agg-delta-mid svg {
|
|
2509
|
+
width: 24px;
|
|
2510
|
+
height: 10px;
|
|
2511
|
+
}
|
|
2512
|
+
.agg-delta-count {
|
|
2513
|
+
padding: 2px 9px;
|
|
2514
|
+
border-radius: 999px;
|
|
2515
|
+
background: var(--admin-teal-soft);
|
|
2516
|
+
color: #007973;
|
|
2517
|
+
font-size: 11.5px;
|
|
2518
|
+
font-weight: 720;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
/* Publish plan → launch manifest */
|
|
2522
|
+
[data-publish-aggregate] .agg-manifest {
|
|
2523
|
+
display: block;
|
|
2524
|
+
padding: 0;
|
|
2525
|
+
overflow: hidden;
|
|
2526
|
+
background:
|
|
2527
|
+
linear-gradient(180deg, var(--admin-surface-tint), transparent 46px),
|
|
2528
|
+
var(--admin-surface);
|
|
2529
|
+
}
|
|
2530
|
+
.agg-manifest-head {
|
|
2531
|
+
padding: 12px 15px;
|
|
2532
|
+
border-bottom: 1px dashed var(--admin-line-strong);
|
|
2533
|
+
}
|
|
2534
|
+
.agg-manifest-head b {
|
|
2535
|
+
display: flex;
|
|
2536
|
+
align-items: center;
|
|
2537
|
+
gap: 8px;
|
|
2538
|
+
font-size: 13px;
|
|
2539
|
+
}
|
|
2540
|
+
.agg-manifest-head b svg {
|
|
2541
|
+
width: 15px;
|
|
2542
|
+
height: 15px;
|
|
2543
|
+
color: var(--admin-teal);
|
|
2544
|
+
}
|
|
2545
|
+
[data-publish-aggregate] .agg-manifest .aggregate-plan-grid {
|
|
2546
|
+
padding: 8px 15px 12px;
|
|
2547
|
+
}
|
|
2548
|
+
[data-publish-aggregate] .agg-manifest > .note {
|
|
2549
|
+
margin: 0;
|
|
2550
|
+
padding: 11px 15px;
|
|
2551
|
+
border-top: 1px dashed var(--admin-line-strong);
|
|
2552
|
+
background: var(--admin-surface-soft);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
/* Openable "Included changes" — reused by the Published Versions "What
|
|
2556
|
+
shipped" per-version disclosure (.ver-changes below); the staged-delta
|
|
2557
|
+
table above no longer uses the <details> wrap itself, only the row/diff
|
|
2558
|
+
classes it shares with changeSummaryItem. */
|
|
2559
|
+
.agg-included-details > summary {
|
|
2560
|
+
list-style: none;
|
|
2561
|
+
cursor: pointer;
|
|
2562
|
+
display: flex;
|
|
2563
|
+
align-items: center;
|
|
2564
|
+
gap: 10px;
|
|
2565
|
+
padding: 11px 15px;
|
|
2566
|
+
font-size: 12px;
|
|
2567
|
+
color: var(--admin-muted);
|
|
2568
|
+
}
|
|
2569
|
+
.agg-included-details > summary::-webkit-details-marker {
|
|
2570
|
+
display: none;
|
|
2571
|
+
}
|
|
2572
|
+
.agg-included-details > summary::marker {
|
|
2573
|
+
content: "";
|
|
2574
|
+
}
|
|
2575
|
+
.agg-included-details > summary:focus-visible {
|
|
2576
|
+
outline: 2px solid var(--admin-teal);
|
|
2577
|
+
outline-offset: -2px;
|
|
2578
|
+
}
|
|
2579
|
+
.agg-included-lead {
|
|
2580
|
+
font-size: 11px;
|
|
2581
|
+
font-weight: 740;
|
|
2582
|
+
letter-spacing: 0.03em;
|
|
2583
|
+
text-transform: uppercase;
|
|
2584
|
+
color: var(--admin-ink);
|
|
2585
|
+
}
|
|
2586
|
+
.agg-included-badge {
|
|
2587
|
+
padding: 1px 8px;
|
|
2588
|
+
border-radius: 999px;
|
|
2589
|
+
background: var(--admin-surface-tint);
|
|
2590
|
+
color: #3f4d49;
|
|
2591
|
+
font-size: 11.5px;
|
|
2592
|
+
font-weight: 720;
|
|
2593
|
+
}
|
|
2594
|
+
.agg-chev {
|
|
2595
|
+
margin-left: auto;
|
|
2596
|
+
display: inline-flex;
|
|
2597
|
+
color: var(--admin-muted-2);
|
|
2598
|
+
transition: transform 0.18s ease;
|
|
2599
|
+
}
|
|
2600
|
+
.agg-chev svg {
|
|
2601
|
+
width: 14px;
|
|
2602
|
+
height: 14px;
|
|
2603
|
+
}
|
|
2604
|
+
.agg-included-details[open] > summary .agg-chev {
|
|
2605
|
+
transform: rotate(90deg);
|
|
2606
|
+
}
|
|
2607
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2608
|
+
.agg-chev {
|
|
2609
|
+
transition: none;
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
.agg-included-list {
|
|
2613
|
+
list-style: none;
|
|
2614
|
+
margin: 0;
|
|
2615
|
+
padding: 0 15px 14px;
|
|
2616
|
+
display: grid;
|
|
2617
|
+
gap: 8px;
|
|
2618
|
+
}
|
|
2619
|
+
.agg-included-item {
|
|
2620
|
+
display: grid;
|
|
2621
|
+
gap: 5px;
|
|
2622
|
+
padding: 11px 12px;
|
|
2623
|
+
border: 1px solid var(--admin-line);
|
|
2624
|
+
border-radius: var(--admin-radius-sm);
|
|
2625
|
+
background: var(--admin-surface-soft);
|
|
2626
|
+
}
|
|
2627
|
+
.agg-included-top {
|
|
2628
|
+
display: flex;
|
|
2629
|
+
align-items: center;
|
|
2630
|
+
gap: 10px;
|
|
2631
|
+
flex-wrap: wrap;
|
|
2632
|
+
}
|
|
2633
|
+
.agg-included-pr {
|
|
2634
|
+
font-weight: 720;
|
|
2635
|
+
font-size: 13px;
|
|
2636
|
+
}
|
|
2637
|
+
.agg-included-title {
|
|
2638
|
+
font-weight: 600;
|
|
2639
|
+
font-size: 13.5px;
|
|
2640
|
+
}
|
|
2641
|
+
.agg-included-desc {
|
|
2642
|
+
margin: 0;
|
|
2643
|
+
color: var(--admin-muted);
|
|
2644
|
+
font-size: 12.5px;
|
|
2645
|
+
line-height: 1.5;
|
|
2646
|
+
}
|
|
2647
|
+
.agg-included-meta {
|
|
2648
|
+
display: flex;
|
|
2649
|
+
align-items: center;
|
|
2650
|
+
gap: 6px;
|
|
2651
|
+
flex-wrap: wrap;
|
|
2652
|
+
color: var(--admin-muted-2);
|
|
2653
|
+
font-size: 11.5px;
|
|
2654
|
+
}
|
|
2655
|
+
.agg-included-meta > span + span::before {
|
|
2656
|
+
content: "·";
|
|
2657
|
+
padding-right: 6px;
|
|
2658
|
+
color: var(--admin-line-strong);
|
|
2659
|
+
}
|
|
2660
|
+
.agg-included-meta .sha-token {
|
|
2661
|
+
font-size: 11px;
|
|
2662
|
+
padding: 1px 6px;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
/* Published-versions "what shipped": the release summary lead + openable
|
|
2666
|
+
per-change list, inline in the Version cell. Reuses the .agg-included-*
|
|
2667
|
+
item markup; only the container chrome is retuned for a table cell. */
|
|
2668
|
+
.ver-summary {
|
|
2669
|
+
margin: 6px 0 0;
|
|
2670
|
+
color: var(--admin-muted);
|
|
2671
|
+
font-size: 12.5px;
|
|
2672
|
+
line-height: 1.5;
|
|
2673
|
+
max-width: 48ch;
|
|
2674
|
+
}
|
|
2675
|
+
.ver-changes {
|
|
2676
|
+
margin-top: 8px;
|
|
2677
|
+
border: 1px solid var(--admin-line);
|
|
2678
|
+
border-radius: var(--admin-radius-sm);
|
|
2679
|
+
background: var(--admin-surface-soft);
|
|
2680
|
+
}
|
|
2681
|
+
.ver-changes > summary {
|
|
2682
|
+
padding: 8px 10px;
|
|
2683
|
+
}
|
|
2684
|
+
.ver-changes .agg-included-list {
|
|
2685
|
+
padding: 0 10px 10px;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
/* Branch-retention "what this branch changes": the PR title + one-line
|
|
2689
|
+
description + scope meta, inline under the branch ref. */
|
|
2690
|
+
.branch-pr-title {
|
|
2691
|
+
margin: 6px 0 0;
|
|
2692
|
+
font-weight: 600;
|
|
2693
|
+
font-size: 13px;
|
|
2694
|
+
color: var(--admin-ink);
|
|
2695
|
+
max-width: 46ch;
|
|
2696
|
+
}
|
|
2697
|
+
.branch-pr-desc {
|
|
2698
|
+
margin: 3px 0 0;
|
|
2699
|
+
color: var(--admin-muted);
|
|
2700
|
+
font-size: 12.5px;
|
|
2701
|
+
line-height: 1.5;
|
|
2702
|
+
max-width: 46ch;
|
|
2703
|
+
}
|
|
2704
|
+
.branch-pr-meta {
|
|
2705
|
+
margin-top: 5px;
|
|
2706
|
+
}
|
|
2707
|
+
.branch-pr-ai {
|
|
2708
|
+
color: var(--admin-teal-dark);
|
|
2709
|
+
font-weight: 640;
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
/* Scope-of-change: lines added/removed (green/red), + the top-level readout */
|
|
2713
|
+
.agg-lines {
|
|
2714
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
2715
|
+
font-variant-numeric: tabular-nums;
|
|
2716
|
+
white-space: nowrap;
|
|
2717
|
+
}
|
|
2718
|
+
.agg-add {
|
|
2719
|
+
color: var(--admin-green);
|
|
2720
|
+
}
|
|
2721
|
+
.agg-del {
|
|
2722
|
+
color: var(--admin-red);
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
/* "View diff" disclosure for a trivial change (the diff IS its summary). */
|
|
2726
|
+
.agg-diff {
|
|
2727
|
+
margin-top: 2px;
|
|
2728
|
+
}
|
|
2729
|
+
.agg-diff > summary {
|
|
2730
|
+
cursor: pointer;
|
|
2731
|
+
font-size: 11.5px;
|
|
2732
|
+
font-weight: 640;
|
|
2733
|
+
color: var(--admin-teal-dark);
|
|
2734
|
+
width: max-content;
|
|
2735
|
+
}
|
|
2736
|
+
.agg-diff[open] > summary {
|
|
2737
|
+
margin-bottom: 6px;
|
|
2738
|
+
}
|
|
2739
|
+
.agg-diff-file {
|
|
2740
|
+
margin-top: 6px;
|
|
2741
|
+
}
|
|
2742
|
+
.agg-diff-path {
|
|
2743
|
+
font-size: 11px;
|
|
2744
|
+
color: var(--admin-muted);
|
|
2745
|
+
margin-bottom: 3px;
|
|
2746
|
+
}
|
|
2747
|
+
.agg-diff-patch {
|
|
2748
|
+
margin: 0;
|
|
2749
|
+
padding: 8px 10px;
|
|
2750
|
+
border: 1px solid var(--admin-line);
|
|
2751
|
+
border-radius: var(--admin-radius-sm);
|
|
2752
|
+
background: var(--admin-surface);
|
|
2753
|
+
font-size: 11.5px;
|
|
2754
|
+
line-height: 1.5;
|
|
2755
|
+
overflow-x: auto;
|
|
2756
|
+
white-space: pre;
|
|
2757
|
+
}
|
|
2758
|
+
.agg-diff-add {
|
|
2759
|
+
color: var(--admin-green);
|
|
2760
|
+
background: var(--admin-green-soft);
|
|
2761
|
+
}
|
|
2762
|
+
.agg-diff-del {
|
|
2763
|
+
color: var(--admin-red);
|
|
2764
|
+
background: var(--admin-red-soft);
|
|
2765
|
+
}
|
|
2766
|
+
.agg-diff-hunk {
|
|
2767
|
+
color: var(--admin-muted-2);
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
.agg-staged-head {
|
|
2771
|
+
display: flex;
|
|
2772
|
+
align-items: baseline;
|
|
2773
|
+
justify-content: space-between;
|
|
2774
|
+
gap: 12px;
|
|
2775
|
+
flex-wrap: wrap;
|
|
2776
|
+
}
|
|
2777
|
+
.agg-scope {
|
|
2778
|
+
display: inline-flex;
|
|
2779
|
+
align-items: center;
|
|
2780
|
+
gap: 8px;
|
|
2781
|
+
padding: 3px 10px;
|
|
2782
|
+
border: 1px solid var(--admin-line);
|
|
2783
|
+
border-radius: 999px;
|
|
2784
|
+
background: var(--admin-surface-soft);
|
|
2785
|
+
font-size: 11.5px;
|
|
2786
|
+
color: var(--admin-ink);
|
|
2787
|
+
}
|
|
2788
|
+
.agg-scope[hidden] {
|
|
2789
|
+
display: none;
|
|
2790
|
+
}
|
|
2791
|
+
.agg-scope > span + span::before {
|
|
2792
|
+
content: "·";
|
|
2793
|
+
padding-right: 8px;
|
|
2794
|
+
color: var(--admin-line-strong);
|
|
2795
|
+
}
|
|
2796
|
+
.agg-scope-label {
|
|
2797
|
+
font-size: 10px;
|
|
2798
|
+
font-weight: 760;
|
|
2799
|
+
letter-spacing: 0.08em;
|
|
2800
|
+
text-transform: uppercase;
|
|
2801
|
+
color: var(--admin-muted);
|
|
2802
|
+
}
|
|
2803
|
+
.agg-scope-partial {
|
|
2804
|
+
color: var(--admin-muted-2);
|
|
2805
|
+
font-style: italic;
|
|
2806
|
+
cursor: help;
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
/* Publish action — stack the copy above a full-width, weighted CTA */
|
|
2810
|
+
[data-publish-aggregate] .agg-launch {
|
|
2811
|
+
display: grid;
|
|
2812
|
+
grid-template-columns: 1fr;
|
|
2813
|
+
gap: 12px;
|
|
2814
|
+
padding: 14px 0 2px;
|
|
2815
|
+
}
|
|
2816
|
+
/* Extra selector weight so the gradient/shadow beat the .btn.primary base. */
|
|
2817
|
+
[data-publish-aggregate] .btn.agg-publish-btn {
|
|
2818
|
+
width: 100%;
|
|
2819
|
+
justify-content: center;
|
|
2820
|
+
min-height: 50px;
|
|
2821
|
+
gap: 9px;
|
|
2822
|
+
font-size: 15px;
|
|
2823
|
+
border-radius: var(--admin-radius-sm);
|
|
2824
|
+
background: linear-gradient(178deg, var(--admin-teal), var(--admin-teal-dark));
|
|
2825
|
+
box-shadow: 0 8px 22px color-mix(in srgb, var(--admin-teal) 26%, transparent);
|
|
2826
|
+
}
|
|
2827
|
+
.agg-publish-btn svg {
|
|
2828
|
+
width: 18px;
|
|
2829
|
+
height: 18px;
|
|
2830
|
+
}
|
|
2831
|
+
[data-publish-aggregate] .btn.agg-publish-btn:not(:disabled):hover {
|
|
2832
|
+
filter: brightness(1.05);
|
|
2833
|
+
}
|
|
2834
|
+
[data-publish-aggregate] .btn.agg-publish-btn:disabled {
|
|
2835
|
+
background: var(--admin-surface-tint);
|
|
2836
|
+
border-color: var(--admin-line-strong);
|
|
2837
|
+
color: var(--admin-muted);
|
|
2838
|
+
box-shadow: none;
|
|
2839
|
+
cursor: not-allowed;
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
@media (max-width: 760px) {
|
|
2843
|
+
.agg-chain {
|
|
2844
|
+
grid-template-columns: 1fr;
|
|
2845
|
+
gap: 4px;
|
|
2846
|
+
}
|
|
2847
|
+
.agg-link {
|
|
2848
|
+
flex-direction: row;
|
|
2849
|
+
gap: 10px;
|
|
2850
|
+
padding: 4px 12px;
|
|
2851
|
+
}
|
|
2852
|
+
.agg-link .agg-arrow svg {
|
|
2853
|
+
transform: rotate(90deg);
|
|
2854
|
+
}
|
|
2855
|
+
.agg-stop {
|
|
2856
|
+
padding: 8px 2px;
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
/* Versions + diff */
|
|
3445
2861
|
.versions-table {
|
|
3446
2862
|
min-width: 780px;
|
|
3447
2863
|
}
|
|
3448
2864
|
|
|
3449
|
-
/*
|
|
2865
|
+
/* Unified review pane */
|
|
3450
2866
|
.publish-review-cell {
|
|
3451
2867
|
padding: 12px 14px;
|
|
3452
2868
|
background: var(--admin-surface-soft);
|