@tokenoftrust/storefront-runner 1.4.0 → 1.4.2-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/storefront/astro.config.mjs +17 -1
- package/apps/storefront/integrations/materialize-guard.mjs +56 -0
- package/apps/storefront/migrations-apps/0001_woozy_lyja.sql +22 -0
- package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
- package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
- package/apps/storefront/migrations-apps/meta/0001_snapshot.json +990 -0
- package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
- package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +21 -0
- package/apps/storefront/package.json +0 -1
- package/apps/storefront/public/js/dashboard-team.js +38 -2
- package/apps/storefront/src/components/Seo.astro +65 -1
- package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +1432 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1756 -1382
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
- package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
- package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
- package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
- package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
- package/apps/storefront/src/layouts/Layout.astro +48 -3
- package/apps/storefront/src/lib/activity/alerts.ts +426 -0
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
- package/apps/storefront/src/lib/activity/deployVersion.ts +120 -0
- package/apps/storefront/src/lib/activity/ingest.ts +188 -0
- package/apps/storefront/src/lib/activity/ingestAuth.ts +105 -0
- package/apps/storefront/src/lib/activity/killSwitch.ts +80 -0
- package/apps/storefront/src/lib/activity/query.ts +403 -0
- package/apps/storefront/src/lib/activity/recordActivity.ts +101 -0
- package/apps/storefront/src/lib/activity/store.ts +120 -0
- package/apps/storefront/src/lib/activity/uiActor.ts +150 -0
- package/apps/storefront/src/lib/activity/workerCommit.ts +70 -0
- package/apps/storefront/src/lib/admin/adminShell.ts +290 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
- package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
- package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
- package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
- package/apps/storefront/src/lib/apps/adminService.ts +4 -4
- package/apps/storefront/src/lib/apps/adminSession.ts +53 -44
- package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
- package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
- package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
- package/apps/storefront/src/lib/apps/credentials.ts +2 -2
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
- package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +6 -6
- package/apps/storefront/src/lib/apps/registryService.ts +6 -6
- package/apps/storefront/src/lib/apps/scopes.ts +2 -2
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
- package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
- package/apps/storefront/src/lib/auth/adminEntry.ts +9 -9
- package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
- package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +6 -4
- package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
- package/apps/storefront/src/lib/auth/route.ts +44 -4
- package/apps/storefront/src/lib/auth/session.ts +7 -8
- package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
- package/apps/storefront/src/lib/basePath.ts +9 -2
- package/apps/storefront/src/lib/blog/access.ts +29 -0
- package/apps/storefront/src/lib/blog/collection.ts +313 -0
- package/apps/storefront/src/lib/blog/markdown.ts +174 -0
- package/apps/storefront/src/lib/blog/provider.ts +5 -0
- package/apps/storefront/src/lib/blog/reactions.ts +87 -0
- package/apps/storefront/src/lib/blog/rss.ts +92 -0
- package/apps/storefront/src/lib/blog/teaser.ts +83 -0
- package/apps/storefront/src/lib/blog/types.ts +34 -0
- package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
- package/apps/storefront/src/lib/d1/catalog.ts +2 -2
- package/apps/storefront/src/lib/d1/schema-apps.ts +147 -14
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
- package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
- package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
- package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
- package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
- package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
- package/apps/storefront/src/lib/edgeCache.ts +17 -0
- package/apps/storefront/src/lib/env.ts +51 -0
- package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
- package/apps/storefront/src/lib/i18n.ts +25 -0
- package/apps/storefront/src/lib/jsonld.ts +118 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
- package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
- package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
- package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
- package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
- package/apps/storefront/src/lib/newsletter/send.ts +265 -0
- package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +3519 -0
- package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
- package/apps/storefront/src/lib/publish/domainState.ts +22 -13
- package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +100 -42
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
- package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
- package/apps/storefront/src/lib/search/index.ts +2 -2
- package/apps/storefront/src/lib/seo/alternates.ts +42 -0
- package/apps/storefront/src/lib/seo/meta.ts +65 -0
- package/apps/storefront/src/lib/social/golive.ts +212 -0
- package/apps/storefront/src/lib/social/notify.ts +32 -0
- package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
- package/apps/storefront/src/lib/the-build/provider.ts +27 -0
- package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
- package/apps/storefront/src/lib/the-build/toc.ts +95 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
- package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
- package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
- package/apps/storefront/src/middleware/index.ts +130 -2
- package/apps/storefront/src/pages/admin/ops-timeline.astro +420 -0
- package/apps/storefront/src/pages/admin.astro +52 -1206
- package/apps/storefront/src/pages/api/activity.ts +137 -0
- package/apps/storefront/src/pages/api/admin/activity-alerts.ts +73 -0
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +27 -1
- package/apps/storefront/src/pages/api/apps/admin/install.ts +26 -1
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +25 -1
- package/apps/storefront/src/pages/api/apps/admin/update.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +11 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +11 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +20 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +12 -0
- package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
- package/apps/storefront/src/pages/api/cache-purge.ts +11 -0
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -0
- package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
- package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
- package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
- package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +21 -2
- package/apps/storefront/src/pages/auth/login.astro +5 -4
- package/apps/storefront/src/pages/blog/[slug].astro +96 -16
- package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
- package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
- package/apps/storefront/src/pages/blog/index.astro +2 -0
- package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
- package/apps/storefront/src/pages/cockpit.astro +74 -12
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
- package/apps/storefront/src/pages/search.astro +1 -1
- package/apps/storefront/src/pages/sitemap.xml.ts +20 -0
- package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
- package/apps/storefront/src/pages/the-build/index.astro +146 -0
- package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
- package/apps/storefront/src/styles/admin.css +1080 -0
- package/apps/storefront/tsconfig.json +5 -0
- package/package.json +1 -1
- package/packages/public-runtime/package.json +1 -0
- package/packages/public-runtime/src/activity/README.md +146 -0
- package/packages/public-runtime/src/activity/catalog.ts +553 -0
- package/packages/public-runtime/src/activity/event.ts +168 -0
- package/packages/public-runtime/src/activity/index.ts +16 -0
- package/packages/public-runtime/src/activity/redaction.ts +263 -0
- package/packages/public-runtime/src/candidate-index.ts +13 -0
- package/packages/public-runtime/src/catalog-d1.ts +1 -1
- package/packages/public-runtime/src/customization-preview.ts +25 -2
- package/packages/public-runtime/src/customization-runtime.ts +125 -3
- package/packages/public-runtime/src/customization-versioning.ts +10 -0
- package/packages/public-runtime/src/index.ts +7 -0
- package/packages/public-runtime/src/static-bundle.ts +357 -0
- package/packages/public-runtime/src/tenant.ts +36 -8
- package/scripts/build/copy-tenant-assets.mjs +14 -4
- package/scripts/dev/ai-edit.mjs +1 -1
- package/scripts/dev/checkout-watch.mjs +2 -2
- package/scripts/dev/file-browser.mjs +2 -4
- package/scripts/dev/file-writer.mjs +1 -1
- package/scripts/dev/git-status.mjs +2 -2
- package/scripts/dev/locate-handler.mjs +3 -3
- package/scripts/dev/port-check.mjs +3 -3
- package/scripts/dev/publish.mjs +5 -9
- package/scripts/dev/shot.mjs +1 -1
- package/scripts/dev/unified-diff.mjs +8 -3
- package/scripts/tot-dev.mjs +6 -9
- package/apps/storefront/src/lib/webhooks/signing.ts +0 -29
- package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +0 -146
|
@@ -6,9 +6,10 @@
|
|
|
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
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
* REJECT (with a note) per change.
|
|
9
|
+
* - ACCEPT TO PREVIEW (POST /api/changes/integrate — b11's accept-to-preview
|
|
10
|
+
* split; advances the shared `preview` aggregate, does NOT go live) and
|
|
11
|
+
* REJECT (with a note) per change. Going live is the aggregate card's
|
|
12
|
+
* exact-digest PUBLISH (POST /api/changes/ship), never a per-change accept.
|
|
12
13
|
* - LIVE STATE: the current live version (live-channel pointer) + the
|
|
13
14
|
* static-republish dispatch status for static-publish tenants (fired /
|
|
14
15
|
* skipped / sink-unbound notice).
|
|
@@ -31,10 +32,12 @@ import { readViewerSession } from "@/lib/auth/route";
|
|
|
31
32
|
import { withBase } from "@/lib/basePath";
|
|
32
33
|
import { readEnv } from "@/lib/env";
|
|
33
34
|
import { resolveViewerShipCapability } from "@/lib/change";
|
|
35
|
+
import { resolveLiveVersion } from "@/lib/change/liveVersion";
|
|
36
|
+
import { isPublicSiteCurrent } from "@/lib/change/liveDigest";
|
|
34
37
|
import { isHostedStoreDomain, isInternalTestTenant } from "@tot/public-runtime";
|
|
35
38
|
|
|
36
39
|
const { envelope } = Astro.props;
|
|
37
|
-
const { tenant, basePath
|
|
40
|
+
const { tenant, basePath } = Astro.locals;
|
|
38
41
|
|
|
39
42
|
// Viewer capability, mirrored from the same session model the API routes gate
|
|
40
43
|
// on (resolveOwnerSession admits capability owner|admin). UX-only: the server
|
|
@@ -77,8 +80,36 @@ const staticPublishEnabled = Boolean(
|
|
|
77
80
|
// Presence-only signal for the T5 dispatch sink (never the value).
|
|
78
81
|
const dispatchConfigured = Boolean((await readEnv("WWW_PUBLISH_DISPATCH_TOKEN"))?.trim());
|
|
79
82
|
|
|
83
|
+
// "What does the public site actually serve right now?" — the read-back half of
|
|
84
|
+
// the static-publish seam (resolveLiveVersion's staticLiveDigest), computed here
|
|
85
|
+
// so it's ALWAYS visible rather than only living in GET /api/changes JSON. This is
|
|
86
|
+
// the exact gap that let tokenoftrust.com's public site (preview.tokenoftrust.com)
|
|
87
|
+
// sit two days stale behind several accepted-but-unpublished changes with nothing
|
|
88
|
+
// in the console calling it out — going live and staying live for the PUBLIC
|
|
89
|
+
// static target are two different facts, and this card makes the second one
|
|
90
|
+
// honest instead of assumed. Best-effort: resolveLiveVersion never throws.
|
|
91
|
+
// BUDGETED: resolveLiveVersion chains several TENANT_CACHE reads (resolvePreviewTenant,
|
|
92
|
+
// resolveChannelVersion, readSiteVersion) that are pathologically slow + variable on the
|
|
93
|
+
// preview worker (measured 2-10s EACH, spiking together — a KV/storage-latency issue, not
|
|
94
|
+
// this code). It runs in blocking SSR frontmatter, so it gated the whole admin document
|
|
95
|
+
// stream. It's best-effort display chrome ("what does live/ serve right now"), and the
|
|
96
|
+
// client-side GET /api/changes already returns `current`, so cap it: on timeout the card
|
|
97
|
+
// renders from the client fetch instead of blocking first paint.
|
|
98
|
+
const liveVersion = staticPublishEnabled
|
|
99
|
+
? await Promise.race([
|
|
100
|
+
resolveLiveVersion(tenant.appDomain),
|
|
101
|
+
new Promise<null>((resolve) => setTimeout(() => resolve(null), 1500)),
|
|
102
|
+
])
|
|
103
|
+
: null;
|
|
104
|
+
const publicSiteDigest = liveVersion?.staticLiveDigest ?? null;
|
|
105
|
+
// The actual verdict: does the public target's manifest sha match what was last
|
|
106
|
+
// promoted to "live" here? Both are real tenant Gitea shas (dg5) — a direct,
|
|
107
|
+
// honest comparison, not a heuristic. See liveDigest.ts's isPublicSiteCurrent.
|
|
108
|
+
const publicSiteVerdict = publicSiteDigest
|
|
109
|
+
? isPublicSiteCurrent(liveVersion?.versionId ?? null, publicSiteDigest)
|
|
110
|
+
: null;
|
|
111
|
+
|
|
80
112
|
const changesEndpoint = withBase(basePath || "", "/api/changes");
|
|
81
|
-
const acceptEndpoint = withBase(basePath || "", "/api/changes/accept");
|
|
82
113
|
// b11 — the branch-integration lifecycle SPLIT of accept from publish:
|
|
83
114
|
// - integrateEndpoint = b08's "accept = integrate into the protected preview
|
|
84
115
|
// aggregate" (POST /api/changes/integrate → TenantIntegrationQueue.enqueue).
|
|
@@ -97,13 +128,25 @@ const shipEndpoint = withBase(basePath || "", "/api/changes/ship");
|
|
|
97
128
|
// U7 — retire/GC: evict a candidate's ReviewEnvironment + version (rebuildable),
|
|
98
129
|
// DISTINCT from reject (which closes the change). Gated server-side by retireGate.
|
|
99
130
|
const retireEndpoint = withBase(basePath || "", "/api/preview/retire");
|
|
131
|
+
// rux5 — build-on-demand for a synthetic not-built PR row: materialize the PR's
|
|
132
|
+
// hosted preview (INERT — flips no live channel). Gated server-side by buildGate
|
|
133
|
+
// (the ship-on-behalf floor); the endpoint resolves the head sha via forge PR-read
|
|
134
|
+
// when the row supplies only the PR number.
|
|
135
|
+
const buildEndpoint = withBase(basePath || "", "/api/preview/build");
|
|
100
136
|
// dg7 — the ship-operations workspace woven into this tab: versions/promotion
|
|
101
137
|
// history, per-candidate diff, and rollback (through the ONE rollback
|
|
102
138
|
// orchestrator). Read endpoints mirror the change-queue's auth/scope.
|
|
103
139
|
const versionsEndpoint = withBase(basePath || "", "/api/changes/versions");
|
|
104
140
|
const rollbackEndpoint = withBase(basePath || "", "/api/changes/rollback");
|
|
141
|
+
// b17 — branch retention: GET dry-run classification / POST guarded delete.
|
|
142
|
+
// DISTINCT endpoint from retireEndpoint above (branch delete vs artifact evict).
|
|
143
|
+
const branchesEndpoint = withBase(basePath || "", "/api/changes/branches");
|
|
105
144
|
// The client builds `${diffEndpointBase}/<changeId>/diff` per candidate.
|
|
106
145
|
const diffEndpointBase = changesEndpoint;
|
|
146
|
+
// The live store's root on THIS surface (basePath-aware): where the published
|
|
147
|
+
// digest actually serves. Rendered into "already live" copy + the publish
|
|
148
|
+
// ceremony's "View your live store" CTA.
|
|
149
|
+
const liveHref = withBase(basePath || "", "/");
|
|
107
150
|
const domainStatusEndpoint = withBase(basePath || "", "/api/domain/status");
|
|
108
151
|
const domainDispatchEndpoint = withBase(basePath || "", "/api/domain/dispatch");
|
|
109
152
|
// u9: the machine-verifiable apex-cutover readiness gate — the SAME server gate the
|
|
@@ -120,18 +163,20 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
120
163
|
data-agent-scope="publish"
|
|
121
164
|
role="tabpanel"
|
|
122
165
|
data-changes-endpoint={changesEndpoint}
|
|
123
|
-
data-accept-endpoint={acceptEndpoint}
|
|
124
166
|
data-integrate-endpoint={integrateEndpoint}
|
|
125
167
|
data-ship-endpoint={shipEndpoint}
|
|
126
168
|
data-retire-endpoint={retireEndpoint}
|
|
169
|
+
data-build-endpoint={buildEndpoint}
|
|
127
170
|
data-versions-endpoint={versionsEndpoint}
|
|
128
171
|
data-rollback-endpoint={rollbackEndpoint}
|
|
172
|
+
data-branches-endpoint={branchesEndpoint}
|
|
129
173
|
data-diff-endpoint-base={diffEndpointBase}
|
|
130
174
|
data-domain-status-endpoint={domainStatusEndpoint}
|
|
131
175
|
data-domain-dispatch-endpoint={domainDispatchEndpoint}
|
|
132
176
|
data-domain-readiness-endpoint={domainReadinessEndpoint}
|
|
133
177
|
data-repo={tenant.appDomain}
|
|
134
178
|
data-app-domain={tenant.appDomain}
|
|
179
|
+
data-live-href={liveHref}
|
|
135
180
|
data-can-ship={viewerCanShip ? "true" : "false"}
|
|
136
181
|
data-is-owner={viewerIsOwner ? "true" : "false"}
|
|
137
182
|
data-entitled={goLiveEntitled ? "true" : "false"}
|
|
@@ -202,6 +247,7 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
202
247
|
<span>Published</span>
|
|
203
248
|
<strong data-publish-live-at>—</strong>
|
|
204
249
|
<small>When the live version last changed</small>
|
|
250
|
+
<small data-publish-live-shipped-by></small>
|
|
205
251
|
</div>
|
|
206
252
|
<div class="metric">
|
|
207
253
|
<span>Open changes</span>
|
|
@@ -225,6 +271,59 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
225
271
|
</div>
|
|
226
272
|
)}
|
|
227
273
|
|
|
274
|
+
<!-- Public-site status — the READ-BACK half of the static-publish seam
|
|
275
|
+
(resolveLiveVersion's staticLiveDigest), made visible instead of only
|
|
276
|
+
existing in API JSON. Distinct from "Live version" above: that's the
|
|
277
|
+
CONTROL-PLANE record of what was promoted; this is what the external
|
|
278
|
+
target (CloudFront/S3) is HONESTLY observed serving right now — the two
|
|
279
|
+
can diverge (a stale/failed republish job) with nothing else in this
|
|
280
|
+
console able to tell you. Answers "did going live actually reach the
|
|
281
|
+
public site?" without a manual curl. -->
|
|
282
|
+
{staticPublishEnabled && publicSiteDigest && publicSiteVerdict && (
|
|
283
|
+
<div
|
|
284
|
+
class={`risk-alert ${publicSiteVerdict.status === "current" ? "public-site-ok" : "amber"}`}
|
|
285
|
+
data-publish-public-site
|
|
286
|
+
>
|
|
287
|
+
<div class="risk-alert-main">
|
|
288
|
+
<svg><use href={publicSiteVerdict.status === "current" ? "#i-check" : "#i-alert"}></use></svg>
|
|
289
|
+
<div>
|
|
290
|
+
{publicSiteVerdict.status === "current" ? (
|
|
291
|
+
<>
|
|
292
|
+
<b>Public site is up to date</b>
|
|
293
|
+
<span>
|
|
294
|
+
<code>{tenant.appDomain}</code>'s public target is serving the same commit
|
|
295
|
+
(<code class="publish-mono">{liveVersion?.versionId?.slice(0, 12)}</code>)
|
|
296
|
+
as the hosted store's live pointer. Publishing live above
|
|
297
|
+
republishes it automatically — usually within a few minutes.
|
|
298
|
+
</span>
|
|
299
|
+
</>
|
|
300
|
+
) : publicSiteVerdict.status === "behind" ? (
|
|
301
|
+
<>
|
|
302
|
+
<b>Public site is behind the hosted store's live pointer</b>
|
|
303
|
+
<span>
|
|
304
|
+
<code>{tenant.appDomain}</code>'s public target is still serving{" "}
|
|
305
|
+
<code class="publish-mono">{publicSiteVerdict.publishedSha.slice(0, 12)}</code>, but
|
|
306
|
+
the hosted store's live pointer has since moved to{" "}
|
|
307
|
+
<code class="publish-mono">{publicSiteVerdict.livePointerSha.slice(0, 12)}</code>.
|
|
308
|
+
The republish job hasn't caught up yet (or a go-live happened before it was enabled) —
|
|
309
|
+
check the workflow run, or trigger a fresh go-live to re-dispatch it.
|
|
310
|
+
</span>
|
|
311
|
+
</>
|
|
312
|
+
) : (
|
|
313
|
+
<>
|
|
314
|
+
<b>Couldn't verify the public site is current</b>
|
|
315
|
+
<span>
|
|
316
|
+
Checked <code class="publish-mono">{publicSiteDigest.url}</code>: {publicSiteVerdict.reason}.
|
|
317
|
+
This does NOT mean the hosted store's own "Live version" above is wrong, only that the
|
|
318
|
+
separate public static target couldn't be confirmed current.
|
|
319
|
+
</span>
|
|
320
|
+
</>
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
|
|
228
327
|
<div class="panel">
|
|
229
328
|
<div class="panel-head">
|
|
230
329
|
<h3><svg><use href="#i-export"></use></svg>Candidate queue</h3>
|
|
@@ -232,6 +331,23 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
232
331
|
</div>
|
|
233
332
|
<div class="panel-body grid">
|
|
234
333
|
<p class="note publish-status" data-publish-status role="status" aria-live="polite"></p>
|
|
334
|
+
<!-- rux1: forge-listing degraded strip. Shown ONLY when GET /api/changes
|
|
335
|
+
reports `degraded` (the open-PR listing failed) — distinct from the
|
|
336
|
+
genuine empty queue, so a forge blip never reads as "No open changes". -->
|
|
337
|
+
<div class="risk-alert" data-publish-degraded hidden role="alert">
|
|
338
|
+
<div class="risk-alert-main">
|
|
339
|
+
<svg><use href="#i-alert"></use></svg>
|
|
340
|
+
<div>
|
|
341
|
+
<b>Some open changes may not be shown</b>
|
|
342
|
+
<span>
|
|
343
|
+
We couldn't reach the pull-request list, so open changes that haven't been
|
|
344
|
+
built yet may be missing from the queue below. Built candidates are
|
|
345
|
+
unaffected. Refresh to try again.
|
|
346
|
+
</span>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
<p class="note publish-pr-callout" data-publish-pr-callout hidden role="status" aria-live="polite"></p>
|
|
235
351
|
<div class="table-wrap">
|
|
236
352
|
<table class="publish-table">
|
|
237
353
|
<thead>
|
|
@@ -265,7 +381,7 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
265
381
|
candidates integrate into, its combined evidence (the last integration
|
|
266
382
|
run's honest state), and the exact-digest Publish (b09/b10 ship the
|
|
267
383
|
reviewed pinned SHA + digest, never a rebuild). -->
|
|
268
|
-
<div class="panel" data-publish-aggregate>
|
|
384
|
+
<div class="panel agg-console" data-publish-aggregate>
|
|
269
385
|
<div class="panel-head">
|
|
270
386
|
<h3><svg><use href="#i-box"></use></svg>Preview aggregate</h3>
|
|
271
387
|
<span class="chip" data-aggregate-chip>Loading</span>
|
|
@@ -273,44 +389,137 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
273
389
|
<div class="panel-body grid">
|
|
274
390
|
<p class="note publish-status" data-aggregate-status role="status" aria-live="polite"></p>
|
|
275
391
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
392
|
+
<!-- AI-assembled release summary — a plain-language "what am I about to
|
|
393
|
+
publish?" ahead of the technical trust chain. Rendered ONLY when the
|
|
394
|
+
server supplies data.releaseSummary; otherwise it stays hidden and the
|
|
395
|
+
panel degrades to today's behavior (graceful fallback). -->
|
|
396
|
+
<div class="agg-summary" data-aggregate-summary hidden>
|
|
397
|
+
<div class="agg-summary-head">
|
|
398
|
+
<span class="agg-summary-glyph" aria-hidden="true">
|
|
399
|
+
<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>
|
|
400
|
+
</span>
|
|
401
|
+
<span class="agg-summary-label">What’s in this release</span>
|
|
402
|
+
<span class="agg-ai-chip">
|
|
403
|
+
<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>
|
|
404
|
+
AI summary
|
|
405
|
+
</span>
|
|
406
|
+
</div>
|
|
407
|
+
<p class="agg-summary-lead" data-aggregate-summary-lead></p>
|
|
408
|
+
<ul class="agg-summary-points" data-aggregate-summary-points></ul>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<!-- Chain of custody: the build that was verified → its combined evidence →
|
|
412
|
+
the digest that ships. The reviewed digest wears an accented token so
|
|
413
|
+
the eye can track that exactly what was reviewed is what goes live. The
|
|
414
|
+
three values keep their original data hooks (sha / evidence / digest). -->
|
|
415
|
+
<div class="agg-chain">
|
|
416
|
+
<div class="agg-stop">
|
|
417
|
+
<span class="agg-stop-label">Aggregate build</span>
|
|
418
|
+
<span class="agg-stop-val"><strong class="publish-mono" data-aggregate-sha>—</strong></span>
|
|
419
|
+
<p class="agg-stop-cap" data-aggregate-run-link>The current green preview build for this store.</p>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="agg-link" aria-hidden="true">
|
|
422
|
+
<span class="agg-flowlabel">verified by</span>
|
|
423
|
+
<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>
|
|
281
424
|
</div>
|
|
282
|
-
<div class="
|
|
283
|
-
<label>Combined evidence</
|
|
284
|
-
<strong data-aggregate-evidence>—</strong>
|
|
285
|
-
<p data-aggregate-evidence-detail>The last integration run's verdict.</p>
|
|
425
|
+
<div class="agg-stop">
|
|
426
|
+
<span class="agg-stop-label">Combined evidence</span>
|
|
427
|
+
<span class="agg-stop-val"><strong data-aggregate-evidence>—</strong></span>
|
|
428
|
+
<p class="agg-stop-cap" data-aggregate-evidence-detail>The last integration run's verdict.</p>
|
|
286
429
|
</div>
|
|
287
|
-
<div class="
|
|
288
|
-
<
|
|
289
|
-
<
|
|
290
|
-
|
|
430
|
+
<div class="agg-link" aria-hidden="true">
|
|
431
|
+
<span class="agg-flowlabel">produces</span>
|
|
432
|
+
<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>
|
|
433
|
+
</div>
|
|
434
|
+
<div class="agg-stop">
|
|
435
|
+
<span class="agg-stop-label">Reviewed digest</span>
|
|
436
|
+
<span class="agg-stop-val"><strong class="publish-mono agg-digest" data-aggregate-digest>—</strong></span>
|
|
437
|
+
<p class="agg-stop-cap">The content digest that will be published — exactly what you reviewed.</p>
|
|
291
438
|
</div>
|
|
292
439
|
</div>
|
|
293
440
|
|
|
294
|
-
<!-- Included PRs — the batch this aggregate composes (b01 includedPrs).
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
441
|
+
<!-- Included PRs — the batch this aggregate composes (b01 includedPrs). u14:
|
|
442
|
+
the "missing middle" — this is the STAGED delta vs live. The title + note
|
|
443
|
+
narrate how far ahead of the live site the staged preview is, and each row
|
|
444
|
+
carries an Undo (per-change revert) to roll the staged preview backward. -->
|
|
445
|
+
<div class="aggregate-prs agg-staged" data-aggregate-prs-wrap hidden>
|
|
446
|
+
<div class="agg-staged-head">
|
|
447
|
+
<b data-aggregate-prs-title>Included changes</b>
|
|
448
|
+
<!-- Scope-of-change risk measure across the whole staged batch (total
|
|
449
|
+
files + lines). Populated by renderAggregate from data.stagedScope;
|
|
450
|
+
hidden when no staged change carries size stats. -->
|
|
451
|
+
<span class="agg-scope" data-aggregate-scope hidden></span>
|
|
452
|
+
</div>
|
|
453
|
+
<!-- Staged → live delta strip: staged aggregate sha ahead of the live
|
|
454
|
+
pointer. Populated by renderAggregate; hidden when nothing is ahead. -->
|
|
455
|
+
<div class="agg-delta-strip" data-aggregate-delta-strip hidden>
|
|
456
|
+
<span class="agg-delta-node">
|
|
457
|
+
<span class="agg-stop-label">Staged preview</span>
|
|
458
|
+
<span class="sha-token" data-agg-delta-staged>—</span>
|
|
459
|
+
</span>
|
|
460
|
+
<span class="agg-delta-mid" aria-hidden="true">
|
|
461
|
+
<span class="agg-delta-count" data-agg-delta-count></span>
|
|
462
|
+
<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>
|
|
463
|
+
</span>
|
|
464
|
+
<span class="agg-delta-node">
|
|
465
|
+
<span class="agg-stop-label">Live site</span>
|
|
466
|
+
<span class="sha-token ghost" data-agg-delta-live>—</span>
|
|
467
|
+
</span>
|
|
468
|
+
</div>
|
|
469
|
+
<p class="note" data-aggregate-delta-note hidden></p>
|
|
470
|
+
<ul class="aggregate-prs-list agg-changes" data-aggregate-prs></ul>
|
|
298
471
|
</div>
|
|
299
472
|
|
|
300
473
|
<!-- The exact-digest Publish plan (b09 plan()): pinned SHA + digest +
|
|
301
474
|
included PRs + rollback target + paywall verdict + run link. Shown
|
|
302
475
|
inline (from GET /api/changes `plan`) so the confirm never POSTs to
|
|
303
476
|
preview. Publish is refused until the plan is `ok`. -->
|
|
304
|
-
<div class="aggregate-plan" data-aggregate-plan hidden>
|
|
305
|
-
<div class="aggregate-plan-head">
|
|
306
|
-
<b>Publish plan</b>
|
|
477
|
+
<div class="aggregate-plan agg-manifest" data-aggregate-plan hidden>
|
|
478
|
+
<div class="aggregate-plan-head agg-manifest-head">
|
|
479
|
+
<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>
|
|
307
480
|
<span class="chip" data-aggregate-plan-chip></span>
|
|
308
481
|
</div>
|
|
309
482
|
<dl class="aggregate-plan-grid" data-aggregate-plan-grid></dl>
|
|
483
|
+
|
|
484
|
+
<!-- Openable "Included changes": each accepted PR as a line item with its
|
|
485
|
+
human title, a one-line description of what it does, and meta (sha /
|
|
486
|
+
files / who + when). Populated by renderPlan; degrades to a bare count
|
|
487
|
+
when per-change titles/descriptions aren't available. The <details>
|
|
488
|
+
element is static so its open/closed state survives a poll re-render —
|
|
489
|
+
only the list contents are replaced. -->
|
|
490
|
+
<div class="agg-included" data-aggregate-plan-included hidden>
|
|
491
|
+
<details class="agg-included-details">
|
|
492
|
+
<summary>
|
|
493
|
+
<span class="agg-included-lead">Included changes</span>
|
|
494
|
+
<span class="agg-included-badge" data-aggregate-plan-included-count>0</span>
|
|
495
|
+
<span class="agg-included-hint" data-aggregate-plan-included-hint></span>
|
|
496
|
+
<span class="agg-chev" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"></path></svg></span>
|
|
497
|
+
</summary>
|
|
498
|
+
<ul class="agg-included-list" data-aggregate-plan-included-list></ul>
|
|
499
|
+
</details>
|
|
500
|
+
</div>
|
|
501
|
+
|
|
310
502
|
<p class="note" data-aggregate-plan-note></p>
|
|
311
503
|
</div>
|
|
312
504
|
|
|
313
|
-
|
|
505
|
+
<!-- u15 — retry a DEAD rebuild (no new merge). Shown only while the aggregate
|
|
506
|
+
is red: a candidate's PR already merged into preview, but the
|
|
507
|
+
build/evidence step that follows died (e.g. a transient MCP error) — this
|
|
508
|
+
retries ONLY that step, against the sha the forge already carries. -->
|
|
509
|
+
<div class="check-row" data-aggregate-retry-row hidden>
|
|
510
|
+
<div>
|
|
511
|
+
<b>Retry the build</b>
|
|
512
|
+
<p>
|
|
513
|
+
The last merge landed on preview, but rebuilding it failed. Retry re-runs
|
|
514
|
+
the build + evidence check alone — no new merge, no membership change.
|
|
515
|
+
</p>
|
|
516
|
+
</div>
|
|
517
|
+
<button class="btn" type="button" data-aggregate-retry-build>
|
|
518
|
+
<svg><use href="#i-refresh"></use></svg>Retry build
|
|
519
|
+
</button>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<div class="check-row agg-launch">
|
|
314
523
|
<div>
|
|
315
524
|
<b>Publish the reviewed build to your live store</b>
|
|
316
525
|
<p data-aggregate-publish-detail>
|
|
@@ -318,7 +527,7 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
318
527
|
aggregate is green and its integration run passed.
|
|
319
528
|
</p>
|
|
320
529
|
</div>
|
|
321
|
-
<button class="btn primary" type="button" data-aggregate-publish disabled>
|
|
530
|
+
<button class="btn primary agg-publish-btn" type="button" data-aggregate-publish disabled>
|
|
322
531
|
<svg><use href="#i-export"></use></svg>Publish live
|
|
323
532
|
</button>
|
|
324
533
|
</div>
|
|
@@ -335,6 +544,11 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
335
544
|
<div class="panel-body grid">
|
|
336
545
|
<p class="note publish-status" data-versions-status role="status" aria-live="polite"></p>
|
|
337
546
|
<p class="note" data-versions-rollback-note hidden></p>
|
|
547
|
+
<!-- u14: orients the owner between the staged aggregate above and live versions
|
|
548
|
+
below — "N staged change(s) not yet published; Publish live creates the next
|
|
549
|
+
version here." Owned by renderAggregate (its own element, so its timing never
|
|
550
|
+
races renderVersions). -->
|
|
551
|
+
<p class="note" data-versions-orientation hidden></p>
|
|
338
552
|
<div class="table-wrap">
|
|
339
553
|
<table class="publish-table versions-table">
|
|
340
554
|
<thead>
|
|
@@ -357,6 +571,59 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
357
571
|
</div>
|
|
358
572
|
</div>
|
|
359
573
|
|
|
574
|
+
<!-- b17: branch retention — Admin visibility into the branch-lifecycle
|
|
575
|
+
contract's "Candidate branch retirement" table (P1 items 8/9/11). Reads
|
|
576
|
+
the guarded `candidate_list` classification (protected/active-open/
|
|
577
|
+
stale-open/integrated/closed-or-rejected/orphan) and, for a branch that
|
|
578
|
+
classifies as ready, runs the guarded `candidate_delete` — which
|
|
579
|
+
re-classifies fresh server-side and refuses anything not integrated or
|
|
580
|
+
closed-or-rejected, no matter what this panel last rendered. DISTINCT
|
|
581
|
+
from the "Retire" action in the candidate queue above: retire evicts a
|
|
582
|
+
hosted PREVIEW ARTIFACT (rebuildable); this panel deletes a Git BRANCH
|
|
583
|
+
(transport only, never the durable record — see the note below). An
|
|
584
|
+
orphan is always shown quarantined for review, never one-click deletable. -->
|
|
585
|
+
<div class="panel" data-publish-branches>
|
|
586
|
+
<div class="panel-head">
|
|
587
|
+
<h3><svg><use href="#i-workflows"></use></svg>Branch retention</h3>
|
|
588
|
+
<span class="chip" data-branches-chip>Loading</span>
|
|
589
|
+
</div>
|
|
590
|
+
<div class="panel-body grid">
|
|
591
|
+
<p class="note">
|
|
592
|
+
Cleans up candidate <b>branches</b> once their pull request is merged or
|
|
593
|
+
closed — a different action on a different object than <b>Retire</b> in
|
|
594
|
+
the queue above, which only evicts a hosted preview build (rebuildable
|
|
595
|
+
on demand, never a branch). Every classification below is a fresh
|
|
596
|
+
dry-run; <b>Sweep dead branches</b> lists exactly what it will delete and
|
|
597
|
+
asks once to confirm, then the server re-checks each ref fresh before
|
|
598
|
+
removing it.
|
|
599
|
+
</p>
|
|
600
|
+
<p class="note publish-status" data-branches-status role="status" aria-live="polite"></p>
|
|
601
|
+
<div class="toolbar" data-branches-actions hidden>
|
|
602
|
+
<button type="button" class="btn" data-branches-sweep>Sweep dead branches</button>
|
|
603
|
+
</div>
|
|
604
|
+
<div class="table-wrap">
|
|
605
|
+
<table class="publish-table">
|
|
606
|
+
<thead>
|
|
607
|
+
<tr>
|
|
608
|
+
<th>Branch</th>
|
|
609
|
+
<th>Classification</th>
|
|
610
|
+
<th>PR</th>
|
|
611
|
+
<th>Tip commit</th>
|
|
612
|
+
<th>Age</th>
|
|
613
|
+
<th>Evidence</th>
|
|
614
|
+
<th class="action">Actions</th>
|
|
615
|
+
</tr>
|
|
616
|
+
</thead>
|
|
617
|
+
<tbody data-branches-body>
|
|
618
|
+
<tr data-branches-placeholder>
|
|
619
|
+
<td colspan="7"><span class="subtle">Loading branch classification…</span></td>
|
|
620
|
+
</tr>
|
|
621
|
+
</tbody>
|
|
622
|
+
</table>
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
|
|
360
627
|
{staticPublishEnabled && (
|
|
361
628
|
<div class="panel" data-publish-domain>
|
|
362
629
|
<div class="panel-head">
|
|
@@ -449,1284 +716,194 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
449
716
|
</div>
|
|
450
717
|
</div>
|
|
451
718
|
)}
|
|
719
|
+
|
|
720
|
+
<!-- Accept-to-preview LIFTOFF — the plan confirm + live integration flight +
|
|
721
|
+
outcome ceremony. One dialog, three phases (plan → flight → outcome).
|
|
722
|
+
The flight's stage rail advances ONLY on real signals (the in-flight POST
|
|
723
|
+
plus GET /api/changes polling — includedPrs / activeRun / aggregateSha);
|
|
724
|
+
nothing is simulated. Replaces the old window.confirm() plan popup. -->
|
|
725
|
+
<div class="liftoff-backdrop" data-liftoff hidden>
|
|
726
|
+
<div class="liftoff-card" role="dialog" aria-modal="true" aria-labelledby="liftoff-title" tabindex="-1">
|
|
727
|
+
<div class="liftoff-phase" data-liftoff-plan>
|
|
728
|
+
<p class="liftoff-kicker">Accept to preview</p>
|
|
729
|
+
<h3 class="liftoff-title" id="liftoff-title" data-liftoff-title>Integrate into preview</h3>
|
|
730
|
+
<dl class="liftoff-grid" data-liftoff-grid></dl>
|
|
731
|
+
<p class="note">
|
|
732
|
+
Integrates this candidate into the protected <code>preview</code> aggregate, then
|
|
733
|
+
rebuilds it and re-runs its combined evidence. <b>Nothing goes live</b> —
|
|
734
|
+
publishing the reviewed build stays a separate, explicit step.
|
|
735
|
+
</p>
|
|
736
|
+
<div class="liftoff-actions">
|
|
737
|
+
<button class="btn" type="button" data-liftoff-cancel>Cancel</button>
|
|
738
|
+
<button class="btn primary" type="button" data-liftoff-proceed>
|
|
739
|
+
<svg><use href="#i-check"></use></svg>Integrate into preview
|
|
740
|
+
</button>
|
|
741
|
+
</div>
|
|
742
|
+
</div>
|
|
743
|
+
|
|
744
|
+
<div class="liftoff-phase" data-liftoff-flight hidden>
|
|
745
|
+
<p class="liftoff-kicker">Integration in flight</p>
|
|
746
|
+
<h3 class="liftoff-title" data-liftoff-flight-title>Integrating…</h3>
|
|
747
|
+
<ol class="liftoff-stages">
|
|
748
|
+
<li data-liftoff-stage="merge">
|
|
749
|
+
<span class="liftoff-dot"></span>
|
|
750
|
+
<div><b>Merge</b><span>squash-merge the candidate into <code>preview</code></span></div>
|
|
751
|
+
</li>
|
|
752
|
+
<li data-liftoff-stage="build">
|
|
753
|
+
<span class="liftoff-dot"></span>
|
|
754
|
+
<div><b>Rebuild</b><span>build the combined preview aggregate</span></div>
|
|
755
|
+
</li>
|
|
756
|
+
<li data-liftoff-stage="evidence">
|
|
757
|
+
<span class="liftoff-dot"></span>
|
|
758
|
+
<div><b>Evidence</b><span>re-run the aggregate's combined checks</span></div>
|
|
759
|
+
</li>
|
|
760
|
+
<li data-liftoff-stage="green">
|
|
761
|
+
<span class="liftoff-dot"></span>
|
|
762
|
+
<div><b>Green</b><span>the aggregate becomes the reviewable, shippable build</span></div>
|
|
763
|
+
</li>
|
|
764
|
+
</ol>
|
|
765
|
+
<p class="liftoff-live" data-liftoff-live role="status" aria-live="polite">Working…</p>
|
|
766
|
+
<div class="liftoff-meta">
|
|
767
|
+
<span class="publish-mono" data-liftoff-elapsed>0:00</span>
|
|
768
|
+
<button class="btn" type="button" data-liftoff-hide>Continue in background</button>
|
|
769
|
+
</div>
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
<div class="liftoff-phase liftoff-outcome-phase" data-liftoff-outcome hidden>
|
|
773
|
+
<div class="liftoff-seal" data-liftoff-seal aria-hidden="true"></div>
|
|
774
|
+
<h3 class="liftoff-title" data-liftoff-outcome-title></h3>
|
|
775
|
+
<p class="note" data-liftoff-outcome-detail></p>
|
|
776
|
+
<!-- Merge-conflict recovery (shown ONLY for reason "merge_failed") — decision
|
|
777
|
+
`automate-first-conflict-recovery` (u4): the refusal LEADS with one primary
|
|
778
|
+
action ("Update this change automatically" → /resolve strategy:"merge",
|
|
779
|
+
then auto-re-integrate). The three option cards appear only when diff3 hits
|
|
780
|
+
a genuine same-line overlap (RefreshResult.unresolved); the old terminal +
|
|
781
|
+
AI-agent guidance is demoted VERBATIM into the collapsed "Do it manually
|
|
782
|
+
instead" expert section (nothing removed). Decision logic lives in the
|
|
783
|
+
tested `resolveConflict.ts`; this glue mirrors it (verified on preview). -->
|
|
784
|
+
<div class="liftoff-conflict" data-liftoff-conflict hidden>
|
|
785
|
+
<p class="note liftoff-conflict-summary" data-conflict-summary hidden></p>
|
|
786
|
+
|
|
787
|
+
<!-- PRIMARY ACTION — always first when a candidate is refused not-mergeable. -->
|
|
788
|
+
<div class="liftoff-conflict-primary" data-conflict-primary>
|
|
789
|
+
<p class="note" data-conflict-primary-note>
|
|
790
|
+
We can rebuild your change on top of the store's current version and
|
|
791
|
+
re-integrate it for you — no terminal, no re-submit.
|
|
792
|
+
</p>
|
|
793
|
+
<button class="btn primary" type="button" data-conflict-auto>Update this change automatically</button>
|
|
794
|
+
<p class="note liftoff-conflict-primary-status" data-conflict-primary-status role="status" aria-live="polite" hidden></p>
|
|
795
|
+
</div>
|
|
796
|
+
|
|
797
|
+
<!-- OPTION CARDS — revealed only on a genuine same-line overlap diff3 can't
|
|
798
|
+
safely auto-pick. Static nodes mirror CONFLICT_OPTION_CARDS. -->
|
|
799
|
+
<div class="liftoff-conflict-cards" data-conflict-cards hidden>
|
|
800
|
+
<p class="note" data-conflict-cards-intro>
|
|
801
|
+
Some of the same lines were changed on both sides, so there's no single safe
|
|
802
|
+
answer. Pick how to resolve the overlap:
|
|
803
|
+
</p>
|
|
804
|
+
<ul class="liftoff-conflict-files publish-mono" data-conflict-files></ul>
|
|
805
|
+
<div class="liftoff-conflict-card-grid" role="group" aria-label="Resolve the overlap">
|
|
806
|
+
<button class="liftoff-option-card" type="button" data-conflict-card data-strategy="merge" data-recommended="1">
|
|
807
|
+
<b>Combine both</b>
|
|
808
|
+
<span>Recommended — weave both sets of edits together</span>
|
|
809
|
+
</button>
|
|
810
|
+
<button class="liftoff-option-card" type="button" data-conflict-card data-strategy="ours">
|
|
811
|
+
<b>Keep my change</b>
|
|
812
|
+
<span>Use your edits on the conflicting lines</span>
|
|
813
|
+
</button>
|
|
814
|
+
<button class="liftoff-option-card" type="button" data-conflict-card data-strategy="theirs">
|
|
815
|
+
<b>Keep the store's current version</b>
|
|
816
|
+
<span>Use the store's edits on the conflicting lines</span>
|
|
817
|
+
</button>
|
|
818
|
+
</div>
|
|
819
|
+
<p class="note liftoff-conflict-cards-status" data-conflict-cards-status role="status" aria-live="polite" hidden></p>
|
|
820
|
+
</div>
|
|
821
|
+
|
|
822
|
+
<!-- EXPERT FALLBACK — the ORIGINAL terminal + AI-agent guidance, verbatim,
|
|
823
|
+
collapsed. Nothing deleted; just demoted from primary to fallback. -->
|
|
824
|
+
<details class="liftoff-conflict-expert" data-conflict-expert>
|
|
825
|
+
<summary>Do it manually instead</summary>
|
|
826
|
+
<p class="note" data-conflict-intro>
|
|
827
|
+
From the store checkout that submitted this change, bring it up to date and
|
|
828
|
+
re-submit — the open PR updates in place. Once the new build is ready,
|
|
829
|
+
finish here with <b>Integrate into preview</b>, or from the terminal with
|
|
830
|
+
<code>tot accept --tenant <store> --pr <N></code>.
|
|
831
|
+
Check <code>git status</code> first — abort any half-finished rebase from an
|
|
832
|
+
earlier attempt. Then <code>tot pr</code>: this change should be marked
|
|
833
|
+
<code>(active)</code>, proving you're in the right checkout and branch.
|
|
834
|
+
If git reports an authentication failure, run <code>tot login</code> and retry.
|
|
835
|
+
</p>
|
|
836
|
+
<pre class="liftoff-commands publish-mono" data-conflict-commands></pre>
|
|
837
|
+
<div class="liftoff-conflict-actions">
|
|
838
|
+
<button class="btn" type="button" data-conflict-copy-prompt>Copy prompt for your AI agent</button>
|
|
839
|
+
<button class="btn" type="button" data-conflict-copy>Copy commands</button>
|
|
840
|
+
</div>
|
|
841
|
+
<p class="liftoff-conflict-raw publish-mono" data-conflict-raw></p>
|
|
842
|
+
</details>
|
|
843
|
+
</div>
|
|
844
|
+
<div class="liftoff-actions">
|
|
845
|
+
<a class="btn" data-liftoff-review target="_blank" rel="noopener" hidden>Review the preview build</a>
|
|
846
|
+
<button class="btn primary" type="button" data-liftoff-done>Done</button>
|
|
847
|
+
</div>
|
|
848
|
+
</div>
|
|
849
|
+
|
|
850
|
+
<div class="liftoff-confetti" data-liftoff-confetti aria-hidden="true"></div>
|
|
851
|
+
</div>
|
|
852
|
+
</div>
|
|
853
|
+
|
|
854
|
+
<!-- Publish-live ceremony — the go-live counterpart of the accept liftoff
|
|
855
|
+
(same shell/styles). Plan states the exact digest + no-rebuild contract;
|
|
856
|
+
flight is a single honest in-flight state (one POST, no staged theater);
|
|
857
|
+
outcome celebrates with a real "View your live store" link. Replaces the
|
|
858
|
+
raw window.confirm() publish popup. -->
|
|
859
|
+
<div class="liftoff-backdrop" data-shipoff hidden>
|
|
860
|
+
<div class="liftoff-card" role="dialog" aria-modal="true" aria-labelledby="shipoff-title" tabindex="-1">
|
|
861
|
+
<div class="liftoff-phase" data-shipoff-plan>
|
|
862
|
+
<p class="liftoff-kicker">Publish live</p>
|
|
863
|
+
<h3 class="liftoff-title" id="shipoff-title">Publish the reviewed build to your live store</h3>
|
|
864
|
+
<dl class="liftoff-grid" data-shipoff-grid></dl>
|
|
865
|
+
<p class="note">
|
|
866
|
+
Publishes this <b>exact reviewed digest</b> — no rebuild, nothing rides along.
|
|
867
|
+
Your live store updates in about a second; rolling back re-publishes the prior
|
|
868
|
+
version from the archive.
|
|
869
|
+
</p>
|
|
870
|
+
<div class="liftoff-actions">
|
|
871
|
+
<button class="btn" type="button" data-shipoff-cancel>Cancel</button>
|
|
872
|
+
<button class="btn primary" type="button" data-shipoff-proceed>
|
|
873
|
+
<svg><use href="#i-export"></use></svg>Publish live
|
|
874
|
+
</button>
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
|
|
878
|
+
<div class="liftoff-phase" data-shipoff-flight hidden>
|
|
879
|
+
<p class="liftoff-kicker">Publishing</p>
|
|
880
|
+
<h3 class="liftoff-title">Promoting the reviewed digest…</h3>
|
|
881
|
+
<p class="liftoff-live" data-shipoff-live role="status" aria-live="polite">
|
|
882
|
+
Pinning the exact reviewed build to your live channel…
|
|
883
|
+
</p>
|
|
884
|
+
<div class="liftoff-meta">
|
|
885
|
+
<span class="publish-mono" data-shipoff-elapsed>0:00</span>
|
|
886
|
+
</div>
|
|
887
|
+
</div>
|
|
888
|
+
|
|
889
|
+
<div class="liftoff-phase liftoff-outcome-phase" data-shipoff-outcome hidden>
|
|
890
|
+
<div class="liftoff-seal" data-shipoff-seal aria-hidden="true"></div>
|
|
891
|
+
<h3 class="liftoff-title" data-shipoff-outcome-title></h3>
|
|
892
|
+
<p class="note" data-shipoff-outcome-detail></p>
|
|
893
|
+
<div class="liftoff-actions">
|
|
894
|
+
<a class="btn primary" data-shipoff-view target="_blank" rel="noopener" hidden>View your live store</a>
|
|
895
|
+
<button class="btn" type="button" data-shipoff-done>Done</button>
|
|
896
|
+
</div>
|
|
897
|
+
</div>
|
|
898
|
+
|
|
899
|
+
<div class="liftoff-confetti" data-shipoff-confetti aria-hidden="true"></div>
|
|
900
|
+
</div>
|
|
901
|
+
</div>
|
|
452
902
|
</section>
|
|
453
903
|
|
|
454
|
-
<script
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (!root) return;
|
|
458
|
-
const ds = root.dataset;
|
|
459
|
-
const canShip = ds.canShip === "true";
|
|
460
|
-
const repo = ds.repo || "";
|
|
461
|
-
const appDomain = ds.appDomain || "";
|
|
462
|
-
|
|
463
|
-
const statusEl = root.querySelector("[data-publish-status]");
|
|
464
|
-
const queueBody = root.querySelector("[data-publish-queue-body]");
|
|
465
|
-
const queueChip = root.querySelector("[data-publish-queue-chip]");
|
|
466
|
-
const queueCount = root.querySelector("[data-publish-queue-count]");
|
|
467
|
-
|
|
468
|
-
const setStatus = (el, message, state) => {
|
|
469
|
-
if (!el) return;
|
|
470
|
-
el.textContent = message || "";
|
|
471
|
-
if (state) el.dataset.state = state;
|
|
472
|
-
else delete el.dataset.state;
|
|
473
|
-
};
|
|
474
|
-
|
|
475
|
-
const shortSha = (sha) => (sha ? String(sha).slice(0, 8) : "—");
|
|
476
|
-
|
|
477
|
-
const age = (iso) => {
|
|
478
|
-
if (!iso) return "—";
|
|
479
|
-
const ms = Date.now() - new Date(iso).getTime();
|
|
480
|
-
if (!Number.isFinite(ms) || ms < 0) return "—";
|
|
481
|
-
const mins = Math.floor(ms / 60000);
|
|
482
|
-
if (mins < 1) return "just now";
|
|
483
|
-
if (mins < 60) return mins + "m ago";
|
|
484
|
-
const hours = Math.floor(mins / 60);
|
|
485
|
-
if (hours < 48) return hours + "h ago";
|
|
486
|
-
return Math.floor(hours / 24) + "d ago";
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
const STATUS_CHIP = {
|
|
490
|
-
queued: "",
|
|
491
|
-
reconciling: "blue",
|
|
492
|
-
ready: "green",
|
|
493
|
-
blocked: "red",
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
const chip = (text, tone) => {
|
|
497
|
-
const el = document.createElement("span");
|
|
498
|
-
el.className = "chip" + (tone ? " " + tone : "");
|
|
499
|
-
el.textContent = text;
|
|
500
|
-
return el;
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
const cell = (...children) => {
|
|
504
|
-
const td = document.createElement("td");
|
|
505
|
-
for (const child of children) {
|
|
506
|
-
if (child == null) continue;
|
|
507
|
-
td.append(typeof child === "string" ? document.createTextNode(child) : child);
|
|
508
|
-
}
|
|
509
|
-
return td;
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
const subtle = (text) => {
|
|
513
|
-
const el = document.createElement("span");
|
|
514
|
-
el.className = "subtle";
|
|
515
|
-
el.textContent = text;
|
|
516
|
-
return el;
|
|
517
|
-
};
|
|
518
|
-
|
|
519
|
-
// ---- Open change queue -------------------------------------------------
|
|
520
|
-
|
|
521
|
-
const renderQueue = (payload) => {
|
|
522
|
-
const changes = Array.isArray(payload.changes) ? payload.changes : [];
|
|
523
|
-
queueBody.textContent = "";
|
|
524
|
-
if (queueCount) queueCount.textContent = String(changes.length);
|
|
525
|
-
if (queueChip) {
|
|
526
|
-
queueChip.textContent = changes.length === 1 ? "1 open" : changes.length + " open";
|
|
527
|
-
queueChip.className = "chip" + (changes.length ? " blue" : "");
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (!changes.length) {
|
|
531
|
-
const tr = document.createElement("tr");
|
|
532
|
-
const td = cell(
|
|
533
|
-
subtle(
|
|
534
|
-
"No open changes. New store changes appear here when a developer submits them for review.",
|
|
535
|
-
),
|
|
536
|
-
);
|
|
537
|
-
td.colSpan = 6;
|
|
538
|
-
tr.append(td);
|
|
539
|
-
queueBody.append(tr);
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
for (const change of changes) {
|
|
544
|
-
const tr = document.createElement("tr");
|
|
545
|
-
|
|
546
|
-
// Change id + branch / PR + preview link.
|
|
547
|
-
const idCell = document.createElement("td");
|
|
548
|
-
const idStrong = document.createElement("strong");
|
|
549
|
-
idStrong.textContent = change.changeId || "—";
|
|
550
|
-
idCell.append(idStrong);
|
|
551
|
-
const meta = [];
|
|
552
|
-
if (change.branch) meta.push(change.branch);
|
|
553
|
-
if (change.prNumber != null) meta.push("PR #" + change.prNumber);
|
|
554
|
-
if (meta.length) idCell.append(subtle(meta.join(" · ")));
|
|
555
|
-
if (change.previewUrl) {
|
|
556
|
-
const preview = document.createElement("a");
|
|
557
|
-
preview.href = change.previewUrl;
|
|
558
|
-
preview.target = "_blank";
|
|
559
|
-
preview.rel = "noopener";
|
|
560
|
-
preview.textContent = "Open preview";
|
|
561
|
-
const wrap = document.createElement("span");
|
|
562
|
-
wrap.className = "subtle";
|
|
563
|
-
wrap.append(preview);
|
|
564
|
-
idCell.append(wrap);
|
|
565
|
-
}
|
|
566
|
-
tr.append(idCell);
|
|
567
|
-
|
|
568
|
-
// Reconcile status.
|
|
569
|
-
const statusCell = cell(
|
|
570
|
-
chip(change.status || "unknown", STATUS_CHIP[change.status] ?? "amber"),
|
|
571
|
-
);
|
|
572
|
-
if (change.lastError) statusCell.append(subtle(change.lastError));
|
|
573
|
-
tr.append(statusCell);
|
|
574
|
-
|
|
575
|
-
// Evidence / compliance verdict.
|
|
576
|
-
const checks = (change.evidence && change.evidence.checks) || [];
|
|
577
|
-
const failing = checks.filter((c) => c.status === "fail");
|
|
578
|
-
const promotable = Boolean(change.evidence && change.evidence.promotable);
|
|
579
|
-
const checksCell = cell(
|
|
580
|
-
promotable ? chip("Checks passed", "green") : chip("Checks blocked", "amber"),
|
|
581
|
-
);
|
|
582
|
-
if (failing.length) {
|
|
583
|
-
checksCell.append(subtle("Failing: " + failing.map((c) => c.label || c.id).join(", ")));
|
|
584
|
-
} else if (!checks.length) {
|
|
585
|
-
checksCell.append(subtle("No checks reported yet"));
|
|
586
|
-
}
|
|
587
|
-
tr.append(checksCell);
|
|
588
|
-
|
|
589
|
-
// Head sha + age.
|
|
590
|
-
const headCell = cell(shortSha(change.headSha));
|
|
591
|
-
headCell.classList.add("publish-mono");
|
|
592
|
-
tr.append(headCell);
|
|
593
|
-
tr.append(cell(age(change.updatedAt)));
|
|
594
|
-
|
|
595
|
-
// Actions.
|
|
596
|
-
const actions = document.createElement("td");
|
|
597
|
-
actions.className = "action publish-actions";
|
|
598
|
-
// dg7 Diff: read-only, available to any viewer who can see the queue.
|
|
599
|
-
const diffBtn = document.createElement("button");
|
|
600
|
-
diffBtn.type = "button";
|
|
601
|
-
diffBtn.className = "btn";
|
|
602
|
-
diffBtn.textContent = "Diff";
|
|
603
|
-
diffBtn.title = "Show what changed vs the live version";
|
|
604
|
-
diffBtn.addEventListener("click", () => toggleDiff(change, diffBtn, tr));
|
|
605
|
-
actions.append(diffBtn);
|
|
606
|
-
// b11 SPLIT: the candidate-queue primary action is now ACCEPT TO PREVIEW
|
|
607
|
-
// (b08 integrate = advance the shared preview aggregate), NOT go-live.
|
|
608
|
-
// Integrate flips NO live channel, so it has NO paywall (entitled) gate —
|
|
609
|
-
// it holds only the ship-on-behalf capability floor. Going live is the
|
|
610
|
-
// aggregate card's Publish action, once the aggregate is green.
|
|
611
|
-
const accept = document.createElement("button");
|
|
612
|
-
accept.type = "button";
|
|
613
|
-
accept.className = "btn primary";
|
|
614
|
-
accept.textContent = "Accept to preview";
|
|
615
|
-
accept.disabled = !canShip || !change.mergeable;
|
|
616
|
-
accept.title = !canShip
|
|
617
|
-
? "Requires an owner or admin sign-in"
|
|
618
|
-
: !change.mergeable
|
|
619
|
-
? "This change is not ready to integrate"
|
|
620
|
-
: "Integrate this change into the preview aggregate (does not go live)";
|
|
621
|
-
accept.addEventListener("click", () => integrateCandidate(change, accept));
|
|
622
|
-
actions.append(accept);
|
|
623
|
-
const reject = document.createElement("button");
|
|
624
|
-
reject.type = "button";
|
|
625
|
-
reject.className = "btn";
|
|
626
|
-
reject.textContent = "Reject";
|
|
627
|
-
reject.disabled = !canShip;
|
|
628
|
-
reject.title = canShip ? "Close this change without publishing" : "Requires an owner or admin sign-in";
|
|
629
|
-
reject.addEventListener("click", () => rejectChange(change, reject));
|
|
630
|
-
actions.append(reject);
|
|
631
|
-
|
|
632
|
-
// U7 Retire/GC: evict this candidate's preview environment + version to
|
|
633
|
-
// reclaim space. DISTINCT from Reject — retire is reversible-by-rebuild
|
|
634
|
-
// (U1 build-on-demand + the U2 fallback page rematerialize it), so a
|
|
635
|
-
// retired PR degrades to "not built yet", not a dead 404.
|
|
636
|
-
const retire = document.createElement("button");
|
|
637
|
-
retire.type = "button";
|
|
638
|
-
retire.className = "btn";
|
|
639
|
-
retire.textContent = "Retire";
|
|
640
|
-
retire.disabled = !canShip;
|
|
641
|
-
retire.title = canShip
|
|
642
|
-
? "Evict this preview to reclaim space — rebuildable on demand (not the same as Reject)"
|
|
643
|
-
: "Requires an owner or admin sign-in";
|
|
644
|
-
retire.addEventListener("click", () => retireChange(change, retire));
|
|
645
|
-
actions.append(retire);
|
|
646
|
-
tr.append(actions);
|
|
647
|
-
|
|
648
|
-
queueBody.append(tr);
|
|
649
|
-
}
|
|
650
|
-
};
|
|
651
|
-
|
|
652
|
-
const renderCurrent = (current) => {
|
|
653
|
-
const versionEl = root.querySelector("[data-publish-live-version]");
|
|
654
|
-
const sourceEl = root.querySelector("[data-publish-live-source]");
|
|
655
|
-
const atEl = root.querySelector("[data-publish-live-at]");
|
|
656
|
-
if (current && current.versionId) {
|
|
657
|
-
if (versionEl) versionEl.textContent = shortSha(current.versionId);
|
|
658
|
-
if (sourceEl) {
|
|
659
|
-
sourceEl.textContent =
|
|
660
|
-
(current.source === "commit" ? "Published from a change" : "Published version") +
|
|
661
|
-
(current.artifactCount != null ? " · " + current.artifactCount + " files" : "");
|
|
662
|
-
}
|
|
663
|
-
if (atEl) atEl.textContent = current.createdAt ? age(current.createdAt) : "—";
|
|
664
|
-
} else {
|
|
665
|
-
if (versionEl) versionEl.textContent = "Not live yet";
|
|
666
|
-
if (sourceEl) sourceEl.textContent = "Nothing has been published for this store";
|
|
667
|
-
if (atEl) atEl.textContent = "—";
|
|
668
|
-
}
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
const loadQueue = async () => {
|
|
672
|
-
try {
|
|
673
|
-
const res = await fetch(ds.changesEndpoint, { headers: { accept: "application/json" } });
|
|
674
|
-
if (res.status === 401 || res.status === 403) {
|
|
675
|
-
if (queueChip) queueChip.textContent = "Sign-in required";
|
|
676
|
-
queueBody.textContent = "";
|
|
677
|
-
const tr = document.createElement("tr");
|
|
678
|
-
const td = cell(subtle("Sign in as a store owner or admin to view the publish queue."));
|
|
679
|
-
td.colSpan = 6;
|
|
680
|
-
tr.append(td);
|
|
681
|
-
queueBody.append(tr);
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
const data = await res.json();
|
|
685
|
-
if (!res.ok) {
|
|
686
|
-
setStatus(statusEl, data.error || "Could not load the change queue (" + res.status + ")", "err");
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
renderCurrent(data.current);
|
|
690
|
-
renderQueue(data);
|
|
691
|
-
renderAggregate(data);
|
|
692
|
-
} catch {
|
|
693
|
-
setStatus(statusEl, "Could not load the change queue — network error", "err");
|
|
694
|
-
}
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
// ---- b11: the aggregate card + exact-digest Publish ---------------------
|
|
698
|
-
// Reads the aggregate/runs/plan projection GET /api/changes now returns
|
|
699
|
-
// (AggregateCardView + AggregateShipPlanView, built by aggregateView.ts):
|
|
700
|
-
// - the aggregate's queueState + build sha + reviewed digest,
|
|
701
|
-
// - the COMBINED EVIDENCE = the last integration run's honest state +
|
|
702
|
-
// statusMessage (never re-derived),
|
|
703
|
-
// - the included-PR batch,
|
|
704
|
-
// - the b09 ship PLAN (pinned SHA/digest/PRs/rollback/paywall + run link),
|
|
705
|
-
// shown inline so the confirm never has to POST to preview it.
|
|
706
|
-
|
|
707
|
-
const aggPanel = root.querySelector("[data-publish-aggregate]");
|
|
708
|
-
const AGG_STATE_CHIP = {
|
|
709
|
-
empty: "",
|
|
710
|
-
queued: "amber",
|
|
711
|
-
integrating: "blue",
|
|
712
|
-
green: "green",
|
|
713
|
-
red: "red",
|
|
714
|
-
shipped: "green",
|
|
715
|
-
};
|
|
716
|
-
// The plan we last rendered — the exact-digest guard the Publish POST sends
|
|
717
|
-
// back so a stale plan (queue moved / artifact changed) is refused, never
|
|
718
|
-
// shipped blind.
|
|
719
|
-
let currentPlan = null;
|
|
720
|
-
|
|
721
|
-
const runLink = (revHref, label) => {
|
|
722
|
-
if (!revHref) return null;
|
|
723
|
-
const a = document.createElement("a");
|
|
724
|
-
a.href = revHref;
|
|
725
|
-
a.target = "_blank";
|
|
726
|
-
a.rel = "noopener";
|
|
727
|
-
a.textContent = label || "Open build";
|
|
728
|
-
return a;
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
const renderAggregate = (data) => {
|
|
732
|
-
if (!aggPanel) return;
|
|
733
|
-
const agg = data.aggregate || null;
|
|
734
|
-
const plan = data.plan || null;
|
|
735
|
-
currentPlan = plan && plan.ok ? plan : null;
|
|
736
|
-
|
|
737
|
-
const chipEl = aggPanel.querySelector("[data-aggregate-chip]");
|
|
738
|
-
const shaEl = aggPanel.querySelector("[data-aggregate-sha]");
|
|
739
|
-
const runLinkEl = aggPanel.querySelector("[data-aggregate-run-link]");
|
|
740
|
-
const evEl = aggPanel.querySelector("[data-aggregate-evidence]");
|
|
741
|
-
const evDetailEl = aggPanel.querySelector("[data-aggregate-evidence-detail]");
|
|
742
|
-
const digestEl = aggPanel.querySelector("[data-aggregate-digest]");
|
|
743
|
-
const prsWrap = aggPanel.querySelector("[data-aggregate-prs-wrap]");
|
|
744
|
-
const prsList = aggPanel.querySelector("[data-aggregate-prs]");
|
|
745
|
-
const publishBtn = aggPanel.querySelector("[data-aggregate-publish]");
|
|
746
|
-
const publishDetail = aggPanel.querySelector("[data-aggregate-publish-detail]");
|
|
747
|
-
|
|
748
|
-
const state = (agg && agg.queueState) || "empty";
|
|
749
|
-
if (chipEl) {
|
|
750
|
-
chipEl.textContent = state;
|
|
751
|
-
chipEl.className = "chip " + (AGG_STATE_CHIP[state] || "");
|
|
752
|
-
}
|
|
753
|
-
if (shaEl) shaEl.textContent = agg && agg.aggregateSha ? shortSha(agg.aggregateSha) : "—";
|
|
754
|
-
if (runLinkEl) {
|
|
755
|
-
runLinkEl.textContent = "";
|
|
756
|
-
const link = agg && runLink(agg.revHref, "Open build");
|
|
757
|
-
if (link) runLinkEl.append(link);
|
|
758
|
-
else runLinkEl.append(document.createTextNode("The current green preview build for this store."));
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// Combined evidence = the last (or in-flight) integration run's honest state.
|
|
762
|
-
const lastRun = agg && (agg.activeRun || agg.lastRun);
|
|
763
|
-
if (evEl) {
|
|
764
|
-
if (!lastRun) {
|
|
765
|
-
evEl.textContent = "No runs yet";
|
|
766
|
-
} else if (lastRun.state === "passed") {
|
|
767
|
-
evEl.textContent = "Passed";
|
|
768
|
-
} else if (lastRun.state === "failed") {
|
|
769
|
-
evEl.textContent = "Failed";
|
|
770
|
-
} else {
|
|
771
|
-
evEl.textContent = "Integrating…";
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
if (evDetailEl) {
|
|
775
|
-
evDetailEl.textContent = "";
|
|
776
|
-
if (lastRun) {
|
|
777
|
-
const parts = [];
|
|
778
|
-
if (lastRun.statusMessage) parts.push(lastRun.statusMessage);
|
|
779
|
-
else if (lastRun.state === "passed") parts.push("Combined evidence passed.");
|
|
780
|
-
else if (lastRun.state === "failed") parts.push("Combined evidence did not pass.");
|
|
781
|
-
if (lastRun.finishedAt) parts.push(age(lastRun.finishedAt));
|
|
782
|
-
evDetailEl.append(document.createTextNode(parts.join(" · ")));
|
|
783
|
-
const rl = runLink(lastRun.revHref, "Run build");
|
|
784
|
-
if (rl) {
|
|
785
|
-
evDetailEl.append(document.createTextNode(" · "));
|
|
786
|
-
evDetailEl.append(rl);
|
|
787
|
-
}
|
|
788
|
-
} else {
|
|
789
|
-
evDetailEl.append(document.createTextNode("The last integration run's verdict."));
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// Reviewed digest: only known once the plan pins it (b09). Never fabricated.
|
|
794
|
-
if (digestEl) {
|
|
795
|
-
digestEl.textContent = currentPlan ? shortSha(currentPlan.artifactDigest.replace(/^sha256:/, "")) : "—";
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// Included PRs.
|
|
799
|
-
const prs = (agg && agg.includedPrs) || [];
|
|
800
|
-
if (prsWrap) prsWrap.hidden = prs.length === 0;
|
|
801
|
-
if (prsList) {
|
|
802
|
-
prsList.textContent = "";
|
|
803
|
-
for (const pr of prs) {
|
|
804
|
-
const li = document.createElement("li");
|
|
805
|
-
const label = document.createElement("span");
|
|
806
|
-
label.textContent = pr.prNumber != null ? "PR #" + pr.prNumber : pr.changeId;
|
|
807
|
-
const sha = document.createElement("span");
|
|
808
|
-
sha.className = "publish-mono subtle";
|
|
809
|
-
sha.textContent = shortSha(pr.headSha);
|
|
810
|
-
li.append(label, sha);
|
|
811
|
-
prsList.append(li);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
renderPlan(data.plan);
|
|
816
|
-
|
|
817
|
-
// The Publish button: enabled only for a shippable, paywall-cleared plan and
|
|
818
|
-
// a viewer who can ship. A paywalled plan is SHOWN (upsell), never hidden.
|
|
819
|
-
if (publishBtn) {
|
|
820
|
-
const shippable = Boolean(currentPlan);
|
|
821
|
-
const paywalled = currentPlan && currentPlan.paywall && !currentPlan.paywall.allowed;
|
|
822
|
-
publishBtn.disabled = !canShip || !shippable || paywalled || currentPlan.alreadyShipped;
|
|
823
|
-
publishBtn.title = !canShip
|
|
824
|
-
? "Requires an owner or admin sign-in"
|
|
825
|
-
: !shippable
|
|
826
|
-
? "The aggregate is not green / has no passed run to publish yet"
|
|
827
|
-
: paywalled
|
|
828
|
-
? "Publishing to your live store is part of the paid plan"
|
|
829
|
-
: currentPlan.alreadyShipped
|
|
830
|
-
? "This reviewed build is already live"
|
|
831
|
-
: "Publish the exact reviewed digest to your live store";
|
|
832
|
-
}
|
|
833
|
-
if (publishDetail) {
|
|
834
|
-
if (currentPlan && currentPlan.alreadyShipped) {
|
|
835
|
-
publishDetail.textContent = "This reviewed build (" + shortSha(currentPlan.pinnedSha) + ") is already live.";
|
|
836
|
-
} else if (currentPlan) {
|
|
837
|
-
publishDetail.textContent =
|
|
838
|
-
"Publishes the exact reviewed digest " + shortSha(currentPlan.artifactDigest.replace(/^sha256:/, "")) +
|
|
839
|
-
" (build " + shortSha(currentPlan.pinnedSha) + ") — no rebuild.";
|
|
840
|
-
} else {
|
|
841
|
-
publishDetail.textContent =
|
|
842
|
-
"Publishes the exact reviewed digest — no rebuild. Available once the aggregate is green and its integration run passed.";
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
};
|
|
846
|
-
|
|
847
|
-
const renderPlan = (plan) => {
|
|
848
|
-
const planBox = aggPanel && aggPanel.querySelector("[data-aggregate-plan]");
|
|
849
|
-
if (!planBox) return;
|
|
850
|
-
const chipEl = planBox.querySelector("[data-aggregate-plan-chip]");
|
|
851
|
-
const grid = planBox.querySelector("[data-aggregate-plan-grid]");
|
|
852
|
-
const note = planBox.querySelector("[data-aggregate-plan-note]");
|
|
853
|
-
if (grid) grid.textContent = "";
|
|
854
|
-
|
|
855
|
-
// No plan at all (degenerate: no store bound) — hide the box.
|
|
856
|
-
if (!plan) {
|
|
857
|
-
planBox.hidden = true;
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
planBox.hidden = false;
|
|
861
|
-
|
|
862
|
-
if (!plan.ok) {
|
|
863
|
-
// An honest refusal — show WHY it isn't shippable, never a fake plan.
|
|
864
|
-
if (chipEl) {
|
|
865
|
-
chipEl.textContent = "Not shippable";
|
|
866
|
-
chipEl.className = "chip amber";
|
|
867
|
-
}
|
|
868
|
-
if (note) note.textContent = plan.message || ("Not shippable (" + (plan.reason || "refused") + ").");
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
const paywalled = plan.paywall && !plan.paywall.allowed;
|
|
873
|
-
if (chipEl) {
|
|
874
|
-
chipEl.textContent = plan.alreadyShipped ? "Already live" : paywalled ? "Upgrade required" : "Ready to publish";
|
|
875
|
-
chipEl.className = "chip " + (plan.alreadyShipped ? "green" : paywalled ? "amber" : "green");
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
const row = (term, value, mono) => {
|
|
879
|
-
const dt = document.createElement("dt");
|
|
880
|
-
dt.textContent = term;
|
|
881
|
-
const dd = document.createElement("dd");
|
|
882
|
-
if (value instanceof Node) dd.append(value);
|
|
883
|
-
else {
|
|
884
|
-
dd.textContent = value;
|
|
885
|
-
if (mono) dd.className = "publish-mono";
|
|
886
|
-
}
|
|
887
|
-
grid.append(dt, dd);
|
|
888
|
-
};
|
|
889
|
-
|
|
890
|
-
row("Reviewed build", shortSha(plan.pinnedSha), true);
|
|
891
|
-
row("Reviewed digest", shortSha(plan.artifactDigest.replace(/^sha256:/, "")), true);
|
|
892
|
-
row("Included changes", String((plan.includedPrs || []).length) + " change" + ((plan.includedPrs || []).length === 1 ? "" : "s"));
|
|
893
|
-
const runCell = document.createElement("span");
|
|
894
|
-
runCell.append(document.createTextNode(plan.integrationRunId));
|
|
895
|
-
const planRunLink = runLink(plan.pinnedRevHref, "Open build");
|
|
896
|
-
if (planRunLink) {
|
|
897
|
-
runCell.append(document.createTextNode(" · "));
|
|
898
|
-
runCell.append(planRunLink);
|
|
899
|
-
}
|
|
900
|
-
row("Integration run", runCell);
|
|
901
|
-
if (plan.rollbackTarget) {
|
|
902
|
-
row("Rollback to", shortSha(plan.rollbackTarget.aggregateSha), true);
|
|
903
|
-
} else {
|
|
904
|
-
row("Rollback to", "None (first publish)");
|
|
905
|
-
}
|
|
906
|
-
row("Subscription", plan.paywall && plan.paywall.allowed ? "Enabled" : "Not enabled");
|
|
907
|
-
|
|
908
|
-
if (note) {
|
|
909
|
-
note.textContent = paywalled
|
|
910
|
-
? (plan.paywall.message || "Publishing to your live store requires the paid plan.")
|
|
911
|
-
: plan.alreadyShipped
|
|
912
|
-
? "This reviewed build is already live — nothing to publish."
|
|
913
|
-
: "Publish sends the exact reviewed digest above to your live store. No rebuild.";
|
|
914
|
-
}
|
|
915
|
-
};
|
|
916
|
-
|
|
917
|
-
// The exact-digest Publish (b10 ship = b09 orchestrator.ship). Confirms the
|
|
918
|
-
// pinned plan, then POSTs { confirmGoLive, expectedAggregateSha,
|
|
919
|
-
// expectedArtifactDigest } — the reviewed-digest guard: a stale plan (queue
|
|
920
|
-
// moved / artifact changed) is refused by the server, never shipped blind.
|
|
921
|
-
const buildPublishPlanText = (plan) => {
|
|
922
|
-
const lines = ["Publish plan (exact reviewed digest — no rebuild):"];
|
|
923
|
-
lines.push(" tenant: " + (appDomain || repo));
|
|
924
|
-
lines.push(" build: " + shortSha(plan.pinnedSha));
|
|
925
|
-
lines.push(" digest: " + plan.artifactDigest);
|
|
926
|
-
lines.push(" changes: " + (plan.includedPrs || []).length + " included");
|
|
927
|
-
if (plan.rollbackTarget) lines.push(" rollback: " + shortSha(plan.rollbackTarget.aggregateSha));
|
|
928
|
-
lines.push(" effect: publish this exact digest to your LIVE store.");
|
|
929
|
-
return lines.join("\n");
|
|
930
|
-
};
|
|
931
|
-
|
|
932
|
-
const publishAggregate = async (button) => {
|
|
933
|
-
if (!currentPlan) return;
|
|
934
|
-
const plan = currentPlan;
|
|
935
|
-
if (!window.confirm(buildPublishPlanText(plan) + "\n\nPublish live?")) return;
|
|
936
|
-
button.disabled = true;
|
|
937
|
-
setStatus(aggStatusEl, "Publishing the reviewed build (" + shortSha(plan.pinnedSha) + ") live…");
|
|
938
|
-
try {
|
|
939
|
-
const res = await fetch(ds.shipEndpoint, {
|
|
940
|
-
method: "POST",
|
|
941
|
-
headers: { "content-type": "application/json", "x-tot-capability": "ship-on-behalf" },
|
|
942
|
-
body: JSON.stringify({
|
|
943
|
-
aggregateId: plan.aggregateId,
|
|
944
|
-
// The HUMAN go-live gate + the reviewed-digest guards (b09 ship input).
|
|
945
|
-
confirmGoLive: true,
|
|
946
|
-
expectedAggregateSha: plan.pinnedSha,
|
|
947
|
-
expectedArtifactDigest: plan.artifactDigest,
|
|
948
|
-
}),
|
|
949
|
-
});
|
|
950
|
-
const data = await res.json().catch(() => ({}));
|
|
951
|
-
// b09 AggregateShipResult: the ONLY live claim is state === "shipped".
|
|
952
|
-
if (data.state === "shipped") {
|
|
953
|
-
setStatus(
|
|
954
|
-
aggStatusEl,
|
|
955
|
-
"Published live · build " + shortSha(data.pinnedSha) + " (receipt " + (data.receiptId || "—") + ").",
|
|
956
|
-
"ok",
|
|
957
|
-
);
|
|
958
|
-
} else if (data.state === "promote_failed" || data.state === "record_failed") {
|
|
959
|
-
// Honest partial-failure truth — recoverable, never a false "shipped".
|
|
960
|
-
setStatus(aggStatusEl, data.message || "Publish did not complete (" + data.state + "). Retry.", "err");
|
|
961
|
-
} else if (data.reason === "paywall") {
|
|
962
|
-
setStatus(aggStatusEl, data.message || "Publishing to your live store is part of the paid plan.", "err");
|
|
963
|
-
} else {
|
|
964
|
-
setStatus(
|
|
965
|
-
aggStatusEl,
|
|
966
|
-
data.message || data.error || "Publish refused (" + (data.reason || res.status) + ")",
|
|
967
|
-
"err",
|
|
968
|
-
);
|
|
969
|
-
}
|
|
970
|
-
} catch {
|
|
971
|
-
setStatus(aggStatusEl, "Publish failed — network error", "err");
|
|
972
|
-
} finally {
|
|
973
|
-
button.disabled = false;
|
|
974
|
-
loadQueue();
|
|
975
|
-
loadVersions();
|
|
976
|
-
}
|
|
977
|
-
};
|
|
978
|
-
|
|
979
|
-
const aggStatusEl = aggPanel && aggPanel.querySelector("[data-aggregate-status]");
|
|
980
|
-
aggPanel
|
|
981
|
-
?.querySelector("[data-aggregate-publish]")
|
|
982
|
-
?.addEventListener("click", (event) => publishAggregate(event.currentTarget));
|
|
983
|
-
|
|
984
|
-
// b11 — the ACCEPT-TO-PREVIEW plan affordance (decision
|
|
985
|
-
// operator-verb-and-hosting-model): state EXACTLY what accept does now.
|
|
986
|
-
// `POST /api/changes/integrate` (b08) = integrate this candidate into the
|
|
987
|
-
// protected `preview` aggregate (b07's TenantIntegrationQueue.enqueue), then
|
|
988
|
-
// rebuild + combined-evidence the aggregate (b05). It flips NO live channel —
|
|
989
|
-
// going live is the aggregate card's Publish. So the plan says "integrate into
|
|
990
|
-
// preview", never "merge into main / deploy live".
|
|
991
|
-
const buildIntegratePlan = (change) => {
|
|
992
|
-
const target = change.prNumber != null ? "PR #" + change.prNumber : change.changeId;
|
|
993
|
-
const lines = ["Accept-to-preview plan:", " tenant: " + (appDomain || repo)];
|
|
994
|
-
if (change.prNumber != null) lines.push(" PR: #" + change.prNumber);
|
|
995
|
-
lines.push(" change id: " + change.changeId);
|
|
996
|
-
if (change.headSha) lines.push(" head sha: " + shortSha(change.headSha));
|
|
997
|
-
lines.push(
|
|
998
|
-
" effect: integrate " + target + " into the preview aggregate, then rebuild + " +
|
|
999
|
-
"re-evidence it. Does NOT go live — publish the reviewed build separately.",
|
|
1000
|
-
);
|
|
1001
|
-
return lines.join("\n");
|
|
1002
|
-
};
|
|
1003
|
-
|
|
1004
|
-
// b08 integrate returns an IntegrateOutcome — { ok, queueState, runState,
|
|
1005
|
-
// aggregateSha, pointerMoved, statusMessage, reason? } — NEVER a bare
|
|
1006
|
-
// merged:true. Render it honestly: green advances the aggregate; red left it
|
|
1007
|
-
// exactly where it was and names the failing stage.
|
|
1008
|
-
const integrateCandidate = async (change, button) => {
|
|
1009
|
-
if (!window.confirm(buildIntegratePlan(change) + "\n\nProceed?")) return;
|
|
1010
|
-
button.disabled = true;
|
|
1011
|
-
setStatus(statusEl, "Integrating " + change.changeId + " into preview…");
|
|
1012
|
-
try {
|
|
1013
|
-
const res = await fetch(ds.integrateEndpoint, {
|
|
1014
|
-
method: "POST",
|
|
1015
|
-
headers: {
|
|
1016
|
-
"content-type": "application/json",
|
|
1017
|
-
// Mirror of the server-side capability model (UX): the server
|
|
1018
|
-
// re-derives capability and stays authoritative.
|
|
1019
|
-
"x-tot-capability": "ship-on-behalf",
|
|
1020
|
-
},
|
|
1021
|
-
body: JSON.stringify({
|
|
1022
|
-
repo,
|
|
1023
|
-
changeId: change.changeId,
|
|
1024
|
-
prNumber: change.prNumber ?? undefined,
|
|
1025
|
-
expectedHeadSha: change.headSha || undefined,
|
|
1026
|
-
}),
|
|
1027
|
-
});
|
|
1028
|
-
const data = await res.json().catch(() => ({}));
|
|
1029
|
-
if (res.ok && data.ok) {
|
|
1030
|
-
setStatus(
|
|
1031
|
-
statusEl,
|
|
1032
|
-
"Integrated " + change.changeId + " — preview aggregate is green (" +
|
|
1033
|
-
shortSha(data.aggregateSha) + "). Review the aggregate, then Publish to go live.",
|
|
1034
|
-
"ok",
|
|
1035
|
-
);
|
|
1036
|
-
} else if (res.ok && data.queueState === "red") {
|
|
1037
|
-
// The queue ran but the aggregate did not go green — the candidate did
|
|
1038
|
-
// NOT advance the shippable set. Name the honest failing stage.
|
|
1039
|
-
setStatus(
|
|
1040
|
-
statusEl,
|
|
1041
|
-
"Integration of " + change.changeId + " failed (" + (data.reason || "red") + "): " +
|
|
1042
|
-
(data.statusMessage || "the aggregate did not pass its combined evidence.") +
|
|
1043
|
-
" The aggregate is unchanged.",
|
|
1044
|
-
"err",
|
|
1045
|
-
);
|
|
1046
|
-
} else if (data.reason === "not_preview_base") {
|
|
1047
|
-
setStatus(
|
|
1048
|
-
statusEl,
|
|
1049
|
-
"Not integrated: " + change.changeId + " does not target the preview branch " +
|
|
1050
|
-
"(base is not \"preview\"). " + (data.statusMessage || ""),
|
|
1051
|
-
"err",
|
|
1052
|
-
);
|
|
1053
|
-
} else {
|
|
1054
|
-
setStatus(
|
|
1055
|
-
statusEl,
|
|
1056
|
-
data.statusMessage || data.error || "Integrate failed (" + res.status + ")",
|
|
1057
|
-
"err",
|
|
1058
|
-
);
|
|
1059
|
-
}
|
|
1060
|
-
} catch {
|
|
1061
|
-
setStatus(statusEl, "Integrate failed — network error", "err");
|
|
1062
|
-
} finally {
|
|
1063
|
-
button.disabled = false;
|
|
1064
|
-
loadQueue();
|
|
1065
|
-
}
|
|
1066
|
-
};
|
|
1067
|
-
|
|
1068
|
-
const rejectChange = async (change, button) => {
|
|
1069
|
-
const reason = window.prompt(
|
|
1070
|
-
"Reject " + change.changeId + " — add a short note for the submitter:",
|
|
1071
|
-
);
|
|
1072
|
-
if (reason == null) return;
|
|
1073
|
-
if (!reason.trim()) {
|
|
1074
|
-
setStatus(statusEl, "A reject note is required.", "err");
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
button.disabled = true;
|
|
1078
|
-
setStatus(statusEl, "Rejecting " + change.changeId + "…");
|
|
1079
|
-
try {
|
|
1080
|
-
const res = await fetch(
|
|
1081
|
-
ds.changesEndpoint + "/" + encodeURIComponent(change.changeId) + "/reject",
|
|
1082
|
-
{
|
|
1083
|
-
method: "POST",
|
|
1084
|
-
headers: {
|
|
1085
|
-
"content-type": "application/json",
|
|
1086
|
-
"x-tot-capability": "ship-on-behalf",
|
|
1087
|
-
},
|
|
1088
|
-
body: JSON.stringify({ repo, reason: reason.trim() }),
|
|
1089
|
-
},
|
|
1090
|
-
);
|
|
1091
|
-
const data = await res.json().catch(() => ({}));
|
|
1092
|
-
if (res.ok) {
|
|
1093
|
-
setStatus(statusEl, "Change " + change.changeId + " rejected.", "ok");
|
|
1094
|
-
} else {
|
|
1095
|
-
setStatus(statusEl, data.error || "Reject failed (" + res.status + ")", "err");
|
|
1096
|
-
}
|
|
1097
|
-
} catch {
|
|
1098
|
-
setStatus(statusEl, "Reject failed — network error", "err");
|
|
1099
|
-
} finally {
|
|
1100
|
-
button.disabled = false;
|
|
1101
|
-
loadQueue();
|
|
1102
|
-
}
|
|
1103
|
-
};
|
|
1104
|
-
|
|
1105
|
-
// U7 — retire/GC. Mirror of the accept plan affordance (decision
|
|
1106
|
-
// operator-verb-and-hosting-model): state EXACTLY what retire does — evict the
|
|
1107
|
-
// candidate's preview environment + immutable version — and that it is
|
|
1108
|
-
// REBUILDABLE (degrades to "not built yet", not a 404), so an operator can't
|
|
1109
|
-
// confuse it with Reject. Same shape as `tot retire`'s CLI plan for parity.
|
|
1110
|
-
const buildRetirePlan = (change) => {
|
|
1111
|
-
const target = change.prNumber != null ? "PR #" + change.prNumber : change.changeId;
|
|
1112
|
-
const lines = ["Retire plan:", " tenant: " + (appDomain || repo)];
|
|
1113
|
-
if (change.prNumber != null) lines.push(" PR: #" + change.prNumber);
|
|
1114
|
-
lines.push(" change id: " + change.changeId);
|
|
1115
|
-
if (change.headSha) lines.push(" head sha: " + shortSha(change.headSha));
|
|
1116
|
-
lines.push(
|
|
1117
|
-
" effect: evict " + target + "'s preview environment + version to reclaim space.",
|
|
1118
|
-
);
|
|
1119
|
-
lines.push(
|
|
1120
|
-
" rebuild: REVERSIBLE — rebuild it any time (this is NOT Reject; the change stays open).",
|
|
1121
|
-
);
|
|
1122
|
-
return lines.join("\n");
|
|
1123
|
-
};
|
|
1124
|
-
|
|
1125
|
-
const retireChange = async (change, button) => {
|
|
1126
|
-
if (!window.confirm(buildRetirePlan(change) + "\n\nProceed?")) return;
|
|
1127
|
-
button.disabled = true;
|
|
1128
|
-
setStatus(statusEl, "Retiring " + change.changeId + "…");
|
|
1129
|
-
try {
|
|
1130
|
-
const res = await fetch(ds.retireEndpoint, {
|
|
1131
|
-
method: "POST",
|
|
1132
|
-
headers: {
|
|
1133
|
-
"content-type": "application/json",
|
|
1134
|
-
// Mirror of the server-side capability model (UX); the server
|
|
1135
|
-
// re-derives capability and stays authoritative.
|
|
1136
|
-
"x-tot-capability": "ship-on-behalf",
|
|
1137
|
-
},
|
|
1138
|
-
body: JSON.stringify({
|
|
1139
|
-
changeId: change.changeId,
|
|
1140
|
-
pr: change.prNumber ?? undefined,
|
|
1141
|
-
}),
|
|
1142
|
-
});
|
|
1143
|
-
const data = await res.json().catch(() => ({}));
|
|
1144
|
-
if (res.ok && data.ok) {
|
|
1145
|
-
setStatus(
|
|
1146
|
-
statusEl,
|
|
1147
|
-
data.evicted
|
|
1148
|
-
? "Retired " + change.changeId + " — the preview was evicted (rebuildable on demand)."
|
|
1149
|
-
: "Retire " + change.changeId + ": nothing to evict (already retired).",
|
|
1150
|
-
"ok",
|
|
1151
|
-
);
|
|
1152
|
-
} else {
|
|
1153
|
-
setStatus(statusEl, data.error || "Retire failed (" + res.status + ")", "err");
|
|
1154
|
-
}
|
|
1155
|
-
} catch {
|
|
1156
|
-
setStatus(statusEl, "Retire failed — network error", "err");
|
|
1157
|
-
} finally {
|
|
1158
|
-
button.disabled = false;
|
|
1159
|
-
loadQueue();
|
|
1160
|
-
}
|
|
1161
|
-
};
|
|
1162
|
-
|
|
1163
|
-
// ---- dg7: diff view ----------------------------------------------------
|
|
1164
|
-
// For a pending candidate, show what changed vs the current live version,
|
|
1165
|
-
// tied to the exact tenant Gitea head SHA, with the u5 immutable /rev link
|
|
1166
|
-
// and the dg3 evidence AUTHORITY (authoritative / stale / missing). Reads
|
|
1167
|
-
// GET /api/changes/<id>/diff; never invents a diff for a non-renderable
|
|
1168
|
-
// candidate — it shows the honest blocked reason.
|
|
1169
|
-
|
|
1170
|
-
const diffBase = ds.diffEndpointBase;
|
|
1171
|
-
|
|
1172
|
-
const diffRowFor = (tr) => {
|
|
1173
|
-
const next = tr.nextElementSibling;
|
|
1174
|
-
return next && next.dataset && next.dataset.publishDiffRow === tr.dataset.changeRowId
|
|
1175
|
-
? next
|
|
1176
|
-
: null;
|
|
1177
|
-
};
|
|
1178
|
-
|
|
1179
|
-
const closeDiff = (tr, button) => {
|
|
1180
|
-
const existing = diffRowFor(tr);
|
|
1181
|
-
if (existing) existing.remove();
|
|
1182
|
-
if (button) button.textContent = "Diff";
|
|
1183
|
-
};
|
|
1184
|
-
|
|
1185
|
-
const toggleDiff = async (change, button, tr) => {
|
|
1186
|
-
if (!tr.dataset.changeRowId) tr.dataset.changeRowId = change.changeId;
|
|
1187
|
-
if (diffRowFor(tr)) {
|
|
1188
|
-
closeDiff(tr, button);
|
|
1189
|
-
return;
|
|
1190
|
-
}
|
|
1191
|
-
button.disabled = true;
|
|
1192
|
-
button.textContent = "Loading…";
|
|
1193
|
-
const detailTr = document.createElement("tr");
|
|
1194
|
-
detailTr.dataset.publishDiffRow = tr.dataset.changeRowId;
|
|
1195
|
-
const td = document.createElement("td");
|
|
1196
|
-
td.colSpan = 6;
|
|
1197
|
-
td.className = "publish-diff-cell";
|
|
1198
|
-
detailTr.append(td);
|
|
1199
|
-
tr.after(detailTr);
|
|
1200
|
-
try {
|
|
1201
|
-
const res = await fetch(diffBase + "/" + encodeURIComponent(change.changeId) + "/diff", {
|
|
1202
|
-
headers: { accept: "application/json" },
|
|
1203
|
-
});
|
|
1204
|
-
const data = await res.json().catch(() => ({}));
|
|
1205
|
-
if (!res.ok) {
|
|
1206
|
-
td.append(subtle(data.error || "Could not load the diff (" + res.status + ")"));
|
|
1207
|
-
} else {
|
|
1208
|
-
renderDiff(td, data.diff || {});
|
|
1209
|
-
}
|
|
1210
|
-
button.textContent = "Hide diff";
|
|
1211
|
-
} catch {
|
|
1212
|
-
td.append(subtle("Could not load the diff — network error"));
|
|
1213
|
-
button.textContent = "Hide diff";
|
|
1214
|
-
} finally {
|
|
1215
|
-
button.disabled = false;
|
|
1216
|
-
}
|
|
1217
|
-
};
|
|
1218
|
-
|
|
1219
|
-
const CHANGE_MARK = { added: "+", changed: "~", removed: "−" };
|
|
1220
|
-
|
|
1221
|
-
const renderDiff = (td, diff) => {
|
|
1222
|
-
td.textContent = "";
|
|
1223
|
-
const head = document.createElement("div");
|
|
1224
|
-
head.className = "publish-diff-head";
|
|
1225
|
-
|
|
1226
|
-
// Authority: honest label of whether the diff describes the exact head.
|
|
1227
|
-
const authority = diff.authority || {};
|
|
1228
|
-
const authChip = chip(
|
|
1229
|
-
authority.authoritative
|
|
1230
|
-
? "Reviewed at head"
|
|
1231
|
-
: authority.stale
|
|
1232
|
-
? "Last-good preview (stale vs head)"
|
|
1233
|
-
: authority.missing
|
|
1234
|
-
? "No evidence yet"
|
|
1235
|
-
: "Head",
|
|
1236
|
-
authority.authoritative ? "green" : authority.stale ? "amber" : "",
|
|
1237
|
-
);
|
|
1238
|
-
head.append(authChip);
|
|
1239
|
-
|
|
1240
|
-
if (diff.headSha) {
|
|
1241
|
-
const sha = document.createElement("span");
|
|
1242
|
-
sha.className = "publish-mono subtle";
|
|
1243
|
-
sha.textContent = "head " + shortSha(diff.headSha);
|
|
1244
|
-
head.append(sha);
|
|
1245
|
-
}
|
|
1246
|
-
if (diff.revHref) {
|
|
1247
|
-
const rev = document.createElement("a");
|
|
1248
|
-
rev.href = diff.revHref;
|
|
1249
|
-
rev.target = "_blank";
|
|
1250
|
-
rev.rel = "noopener";
|
|
1251
|
-
rev.textContent = "Open reviewed revision";
|
|
1252
|
-
head.append(rev);
|
|
1253
|
-
}
|
|
1254
|
-
td.append(head);
|
|
1255
|
-
|
|
1256
|
-
if (!diff.renderable) {
|
|
1257
|
-
// Honest: a candidate that can't render has NO diff — say why (dg3), no fake.
|
|
1258
|
-
const reason = document.createElement("p");
|
|
1259
|
-
reason.className = "note publish-diff-blocked";
|
|
1260
|
-
reason.textContent =
|
|
1261
|
-
diff.blockedReason || "This candidate has no renderable version to diff yet.";
|
|
1262
|
-
td.append(reason);
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
const files = Array.isArray(diff.files) ? diff.files : [];
|
|
1267
|
-
if (!files.length) {
|
|
1268
|
-
td.append(subtle("No file changes vs the current live version."));
|
|
1269
|
-
return;
|
|
1270
|
-
}
|
|
1271
|
-
const list = document.createElement("ul");
|
|
1272
|
-
list.className = "publish-diff-list";
|
|
1273
|
-
for (const f of files) {
|
|
1274
|
-
const li = document.createElement("li");
|
|
1275
|
-
li.className = "publish-diff-" + f.change;
|
|
1276
|
-
const mark = document.createElement("span");
|
|
1277
|
-
mark.className = "publish-diff-mark";
|
|
1278
|
-
mark.textContent = CHANGE_MARK[f.change] || "•";
|
|
1279
|
-
const path = document.createElement("code");
|
|
1280
|
-
path.textContent = f.path;
|
|
1281
|
-
li.append(mark, path);
|
|
1282
|
-
list.append(li);
|
|
1283
|
-
}
|
|
1284
|
-
td.append(list);
|
|
1285
|
-
};
|
|
1286
|
-
|
|
1287
|
-
// ---- dg7: versions / promotion history + rollback ----------------------
|
|
1288
|
-
// The promotion history (u2/u3 promotion_status) with the tenant Gitea SHA,
|
|
1289
|
-
// its /rev link (u5), and the current-live marker; a prior version rolls back
|
|
1290
|
-
// THROUGH the one orchestrator (/api/changes/rollback → u8), rendering only the
|
|
1291
|
-
// HONEST terminal state (rolled_back only when verified).
|
|
1292
|
-
|
|
1293
|
-
const versionsPanel = root.querySelector("[data-publish-versions]");
|
|
1294
|
-
const versionsBody = versionsPanel?.querySelector("[data-versions-body]");
|
|
1295
|
-
const versionsChip = versionsPanel?.querySelector("[data-versions-chip]");
|
|
1296
|
-
const versionsStatus = versionsPanel?.querySelector("[data-versions-status]");
|
|
1297
|
-
const versionsRollbackNote = versionsPanel?.querySelector("[data-versions-rollback-note]");
|
|
1298
|
-
let rollbackMode = "control-plane";
|
|
1299
|
-
|
|
1300
|
-
const renderVersions = (view) => {
|
|
1301
|
-
if (!versionsBody) return;
|
|
1302
|
-
const versions = Array.isArray(view.versions) ? view.versions : [];
|
|
1303
|
-
rollbackMode = view.rollbackMode || "control-plane";
|
|
1304
|
-
versionsBody.textContent = "";
|
|
1305
|
-
|
|
1306
|
-
if (versionsChip) {
|
|
1307
|
-
versionsChip.textContent = versions.length === 1 ? "1 version" : versions.length + " versions";
|
|
1308
|
-
versionsChip.className = "chip" + (versions.length ? " blue" : "");
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
// Static tenants: per-version rollback is honestly deferred (the console
|
|
1312
|
-
// can't enumerate the S3 versions archive digest /api/changes/rollback needs).
|
|
1313
|
-
if (versionsRollbackNote) {
|
|
1314
|
-
if (rollbackMode === "static-archive") {
|
|
1315
|
-
versionsRollbackNote.hidden = false;
|
|
1316
|
-
versionsRollbackNote.textContent =
|
|
1317
|
-
"This store publishes to a static public site. Rolling back to a prior version " +
|
|
1318
|
-
"resolves the target from the versions archive — run the ship CLI (tot rollback), " +
|
|
1319
|
-
"which enumerates it; the console can't list archived object digests.";
|
|
1320
|
-
} else {
|
|
1321
|
-
versionsRollbackNote.hidden = true;
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
if (!versions.length) {
|
|
1326
|
-
const tr = document.createElement("tr");
|
|
1327
|
-
const td = cell(subtle("No versions have been promoted to live yet."));
|
|
1328
|
-
td.colSpan = 6;
|
|
1329
|
-
tr.append(td);
|
|
1330
|
-
versionsBody.append(tr);
|
|
1331
|
-
return;
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
for (const v of versions) {
|
|
1335
|
-
const tr = document.createElement("tr");
|
|
1336
|
-
|
|
1337
|
-
// Version identity: the tenant Gitea SHA (content authority) + source.
|
|
1338
|
-
const idCell = document.createElement("td");
|
|
1339
|
-
const strong = document.createElement("strong");
|
|
1340
|
-
strong.className = "publish-mono";
|
|
1341
|
-
strong.textContent = shortSha(v.versionId);
|
|
1342
|
-
idCell.append(strong);
|
|
1343
|
-
if (v.source) idCell.append(subtle(v.source === "commit" ? "From a commit" : "Content hash"));
|
|
1344
|
-
tr.append(idCell);
|
|
1345
|
-
|
|
1346
|
-
// Digest (static current only — never fabricated for priors).
|
|
1347
|
-
const digestCell = cell(v.digest ? shortSha(v.digest) : "—");
|
|
1348
|
-
digestCell.classList.add("publish-mono");
|
|
1349
|
-
if (!v.digest) digestCell.classList.add("subtle");
|
|
1350
|
-
tr.append(digestCell);
|
|
1351
|
-
|
|
1352
|
-
tr.append(cell(age(v.promotedAt)));
|
|
1353
|
-
|
|
1354
|
-
// Preview: the u5 immutable /rev/<sha> link.
|
|
1355
|
-
const revCell = document.createElement("td");
|
|
1356
|
-
if (v.revHref) {
|
|
1357
|
-
const a = document.createElement("a");
|
|
1358
|
-
a.href = v.revHref;
|
|
1359
|
-
a.target = "_blank";
|
|
1360
|
-
a.rel = "noopener";
|
|
1361
|
-
a.textContent = "Revision";
|
|
1362
|
-
revCell.append(a);
|
|
1363
|
-
} else {
|
|
1364
|
-
revCell.append(subtle("—"));
|
|
1365
|
-
}
|
|
1366
|
-
tr.append(revCell);
|
|
1367
|
-
|
|
1368
|
-
// State: current live vs prior.
|
|
1369
|
-
tr.append(cell(v.isCurrent ? chip("Current live", "green") : chip("Prior", "")));
|
|
1370
|
-
|
|
1371
|
-
// Action: roll back to a PRIOR version (never the current one).
|
|
1372
|
-
const act = document.createElement("td");
|
|
1373
|
-
act.className = "action publish-actions";
|
|
1374
|
-
if (!v.isCurrent) {
|
|
1375
|
-
const rollbackBtn = document.createElement("button");
|
|
1376
|
-
rollbackBtn.type = "button";
|
|
1377
|
-
rollbackBtn.className = "btn";
|
|
1378
|
-
rollbackBtn.textContent = "Roll back";
|
|
1379
|
-
const staticDeferred = rollbackMode === "static-archive";
|
|
1380
|
-
rollbackBtn.disabled = !canShip || staticDeferred;
|
|
1381
|
-
rollbackBtn.title = !canShip
|
|
1382
|
-
? "Requires an owner or ship-on-behalf sign-in"
|
|
1383
|
-
: staticDeferred
|
|
1384
|
-
? "Static rollback runs through the versions archive — use the ship CLI (tot rollback)"
|
|
1385
|
-
: "Roll the live site back to this version";
|
|
1386
|
-
rollbackBtn.addEventListener("click", () => rollbackVersion(v, rollbackBtn));
|
|
1387
|
-
act.append(rollbackBtn);
|
|
1388
|
-
}
|
|
1389
|
-
tr.append(act);
|
|
1390
|
-
|
|
1391
|
-
versionsBody.append(tr);
|
|
1392
|
-
}
|
|
1393
|
-
};
|
|
1394
|
-
|
|
1395
|
-
const loadVersions = async () => {
|
|
1396
|
-
if (!versionsBody) return;
|
|
1397
|
-
try {
|
|
1398
|
-
const res = await fetch(ds.versionsEndpoint, { headers: { accept: "application/json" } });
|
|
1399
|
-
if (res.status === 401 || res.status === 403) {
|
|
1400
|
-
if (versionsChip) versionsChip.textContent = "Sign-in required";
|
|
1401
|
-
versionsBody.textContent = "";
|
|
1402
|
-
const tr = document.createElement("tr");
|
|
1403
|
-
const td = cell(subtle("Sign in as a store owner or admin to view published versions."));
|
|
1404
|
-
td.colSpan = 6;
|
|
1405
|
-
tr.append(td);
|
|
1406
|
-
versionsBody.append(tr);
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
const data = await res.json().catch(() => ({}));
|
|
1410
|
-
if (!res.ok) {
|
|
1411
|
-
setStatus(versionsStatus, data.error || "Could not load versions (" + res.status + ")", "err");
|
|
1412
|
-
return;
|
|
1413
|
-
}
|
|
1414
|
-
renderVersions(data.view || {});
|
|
1415
|
-
} catch {
|
|
1416
|
-
setStatus(versionsStatus, "Could not load versions — network error", "err");
|
|
1417
|
-
}
|
|
1418
|
-
};
|
|
1419
|
-
|
|
1420
|
-
// Interpret the rollback response into ONE honest terminal state (mirrors the
|
|
1421
|
-
// pure lib/publish/shipWorkspace `interpretRollbackResponse`; duplicated in
|
|
1422
|
-
// plain JS because is:inline scripts can't import modules). `rolledBack` is
|
|
1423
|
-
// true ONLY for a verified `rolled_back` — a `publish_pending` is NEVER a
|
|
1424
|
-
// rolled-back claim.
|
|
1425
|
-
const interpretRollback = (httpOk, body) => {
|
|
1426
|
-
const toVersionId = typeof body.toVersionId === "string" ? body.toVersionId : null;
|
|
1427
|
-
if (typeof body.state === "string") {
|
|
1428
|
-
const state = ["rolled_back", "publish_pending", "publish_failed", "blocked"].includes(body.state)
|
|
1429
|
-
? body.state
|
|
1430
|
-
: "error";
|
|
1431
|
-
return {
|
|
1432
|
-
state,
|
|
1433
|
-
rolledBack: state === "rolled_back",
|
|
1434
|
-
message: body.message || body.error || "Rollback returned state " + state + ".",
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
if (httpOk && body.rolledBack === true) {
|
|
1438
|
-
return {
|
|
1439
|
-
state: "rolled_back",
|
|
1440
|
-
rolledBack: true,
|
|
1441
|
-
message: toVersionId ? "Rolled back live to " + shortSha(toVersionId) + "." : "Rolled back live.",
|
|
1442
|
-
};
|
|
1443
|
-
}
|
|
1444
|
-
return {
|
|
1445
|
-
state: "blocked",
|
|
1446
|
-
rolledBack: false,
|
|
1447
|
-
message: (body.error || "Rollback failed") + (body.reason ? " (" + body.reason + ")" : ""),
|
|
1448
|
-
};
|
|
1449
|
-
};
|
|
1450
|
-
|
|
1451
|
-
const rollbackVersion = async (version, button) => {
|
|
1452
|
-
if (
|
|
1453
|
-
!window.confirm(
|
|
1454
|
-
"Roll the live store back to version " + shortSha(version.versionId) +
|
|
1455
|
-
"? This publishes that prior version to the live site.",
|
|
1456
|
-
)
|
|
1457
|
-
) {
|
|
1458
|
-
return;
|
|
1459
|
-
}
|
|
1460
|
-
button.disabled = true;
|
|
1461
|
-
setStatus(versionsStatus, "Rolling back to " + shortSha(version.versionId) + "…");
|
|
1462
|
-
try {
|
|
1463
|
-
const payload = { targetVersionId: version.versionId };
|
|
1464
|
-
// A static tenant additionally needs the archive digest — the button is
|
|
1465
|
-
// disabled in that mode, but pass it through when we honestly have it.
|
|
1466
|
-
if (version.digest) payload.targetDigest = version.digest;
|
|
1467
|
-
const res = await fetch(ds.rollbackEndpoint, {
|
|
1468
|
-
method: "POST",
|
|
1469
|
-
headers: { "content-type": "application/json", "x-tot-capability": "ship-on-behalf" },
|
|
1470
|
-
body: JSON.stringify(payload),
|
|
1471
|
-
});
|
|
1472
|
-
const data = await res.json().catch(() => ({}));
|
|
1473
|
-
const outcome = interpretRollback(res.ok, data);
|
|
1474
|
-
setStatus(versionsStatus, outcome.message, outcome.rolledBack ? "ok" : outcome.state === "publish_pending" ? "ok" : "err");
|
|
1475
|
-
} catch {
|
|
1476
|
-
setStatus(versionsStatus, "Rollback failed — network error", "err");
|
|
1477
|
-
} finally {
|
|
1478
|
-
button.disabled = false;
|
|
1479
|
-
loadVersions();
|
|
1480
|
-
loadQueue();
|
|
1481
|
-
}
|
|
1482
|
-
};
|
|
1483
|
-
|
|
1484
|
-
// ---- Domain section ----------------------------------------------------
|
|
1485
|
-
|
|
1486
|
-
const domainPanel = root.querySelector("[data-publish-domain]");
|
|
1487
|
-
let domainPollTimer = null;
|
|
1488
|
-
|
|
1489
|
-
const renderDomain = (payload) => {
|
|
1490
|
-
if (!domainPanel) return;
|
|
1491
|
-
const domain = payload.domain || {};
|
|
1492
|
-
const stateEl = domainPanel.querySelector("[data-domain-state]");
|
|
1493
|
-
const stateChip = domainPanel.querySelector("[data-domain-state-chip]");
|
|
1494
|
-
const sinceEl = domainPanel.querySelector("[data-domain-since]");
|
|
1495
|
-
const captureEl = domainPanel.querySelector("[data-domain-capture]");
|
|
1496
|
-
const runEl = domainPanel.querySelector("[data-domain-run]");
|
|
1497
|
-
const runDetailEl = domainPanel.querySelector("[data-domain-run-detail]");
|
|
1498
|
-
const rollbackBtn = domainPanel.querySelector("[data-domain-rollback]");
|
|
1499
|
-
const rollbackDetail = domainPanel.querySelector("[data-domain-rollback-detail]");
|
|
1500
|
-
const connected = domain.state === "cloudfront";
|
|
1501
|
-
|
|
1502
|
-
if (stateEl) {
|
|
1503
|
-
stateEl.textContent = connected
|
|
1504
|
-
? "Storefront CDN (cloudfront)"
|
|
1505
|
-
: "Previous host (pantheon)";
|
|
1506
|
-
}
|
|
1507
|
-
if (stateChip) {
|
|
1508
|
-
stateChip.textContent = connected ? "Connected" : "Not connected";
|
|
1509
|
-
stateChip.className = "chip " + (connected ? "green" : "amber");
|
|
1510
|
-
}
|
|
1511
|
-
if (sinceEl) {
|
|
1512
|
-
sinceEl.textContent = domain.since
|
|
1513
|
-
? "Since " + new Date(domain.since).toLocaleString()
|
|
1514
|
-
: "Current host for " + (payload.appDomain || ds.appDomain);
|
|
1515
|
-
}
|
|
1516
|
-
if (captureEl) {
|
|
1517
|
-
captureEl.textContent = domain.priorCapturedAt
|
|
1518
|
-
? "Captured " + new Date(domain.priorCapturedAt).toLocaleString()
|
|
1519
|
-
: "None captured";
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
const run = domain.lastRun;
|
|
1523
|
-
// "dispatched" = awaiting the CI completion callback (terminal-only wire).
|
|
1524
|
-
const inFlight = run && run.status === "dispatched";
|
|
1525
|
-
if (runEl) {
|
|
1526
|
-
runEl.textContent = run
|
|
1527
|
-
? run.action + (run.rehearsal ? " (rehearsal)" : "") + " · " + run.status
|
|
1528
|
-
: "None";
|
|
1529
|
-
}
|
|
1530
|
-
if (runDetailEl) {
|
|
1531
|
-
runDetailEl.textContent = "";
|
|
1532
|
-
if (run) {
|
|
1533
|
-
const parts = [];
|
|
1534
|
-
if (run.actor) parts.push("by " + run.actor);
|
|
1535
|
-
if (run.startedAt) parts.push(age(run.startedAt));
|
|
1536
|
-
runDetailEl.append(document.createTextNode(parts.join(" · ")));
|
|
1537
|
-
if (run.runUrl) {
|
|
1538
|
-
const link = document.createElement("a");
|
|
1539
|
-
link.href = run.runUrl;
|
|
1540
|
-
link.target = "_blank";
|
|
1541
|
-
link.rel = "noopener";
|
|
1542
|
-
link.textContent = "View run";
|
|
1543
|
-
runDetailEl.append(document.createTextNode(" · "));
|
|
1544
|
-
runDetailEl.append(link);
|
|
1545
|
-
}
|
|
1546
|
-
if (run.status === "failed" && run.error) {
|
|
1547
|
-
const err = document.createElement("span");
|
|
1548
|
-
err.className = "publish-error-detail";
|
|
1549
|
-
err.textContent = run.error;
|
|
1550
|
-
runDetailEl.append(document.createElement("br"));
|
|
1551
|
-
runDetailEl.append(err);
|
|
1552
|
-
}
|
|
1553
|
-
} else {
|
|
1554
|
-
runDetailEl.textContent = "Connect and rollback runs appear here.";
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
if (rollbackBtn) {
|
|
1558
|
-
rollbackBtn.disabled =
|
|
1559
|
-
!canShip || inFlight || !domain.priorRecordsKey;
|
|
1560
|
-
}
|
|
1561
|
-
if (rollbackDetail && domain.priorCapturedAt) {
|
|
1562
|
-
rollbackDetail.textContent =
|
|
1563
|
-
"Restores the DNS records captured " +
|
|
1564
|
-
new Date(domain.priorCapturedAt).toLocaleString() + ".";
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
// Poll while a run is in flight so completion lands without a manual refresh.
|
|
1568
|
-
if (domainPollTimer) {
|
|
1569
|
-
clearTimeout(domainPollTimer);
|
|
1570
|
-
domainPollTimer = null;
|
|
1571
|
-
}
|
|
1572
|
-
if (inFlight) domainPollTimer = setTimeout(loadDomain, 15000);
|
|
1573
|
-
};
|
|
1574
|
-
|
|
1575
|
-
const loadDomain = async () => {
|
|
1576
|
-
if (!domainPanel) return;
|
|
1577
|
-
try {
|
|
1578
|
-
const res = await fetch(ds.domainStatusEndpoint, {
|
|
1579
|
-
headers: { accept: "application/json" },
|
|
1580
|
-
});
|
|
1581
|
-
if (!res.ok) return;
|
|
1582
|
-
renderDomain(await res.json());
|
|
1583
|
-
} catch {
|
|
1584
|
-
/* leave the last rendered state */
|
|
1585
|
-
}
|
|
1586
|
-
// u9: refresh the readiness itemization alongside the status.
|
|
1587
|
-
loadReadiness();
|
|
1588
|
-
};
|
|
1589
|
-
|
|
1590
|
-
// ---- Cutover readiness (u9) --------------------------------------------
|
|
1591
|
-
// Reads the SAME server gate `tot go-live` reads (/api/domain/readiness) and
|
|
1592
|
-
// itemizes the preconditions. The Connect button additionally requires the
|
|
1593
|
-
// server to report ready — the display and the enforcement can't diverge.
|
|
1594
|
-
const readinessBox = domainPanel?.querySelector("[data-domain-readiness]");
|
|
1595
|
-
const connectBtn = domainPanel?.querySelector("[data-domain-connect]");
|
|
1596
|
-
let domainReady = false;
|
|
1597
|
-
|
|
1598
|
-
const renderReadiness = (payload) => {
|
|
1599
|
-
if (!readinessBox) return;
|
|
1600
|
-
const readiness = (payload && payload.readiness) || {};
|
|
1601
|
-
const checks = Array.isArray(readiness.checks) ? readiness.checks : [];
|
|
1602
|
-
domainReady = readiness.ready === true;
|
|
1603
|
-
readinessBox.hidden = false;
|
|
1604
|
-
|
|
1605
|
-
const chip = readinessBox.querySelector("[data-domain-readiness-chip]");
|
|
1606
|
-
if (chip) {
|
|
1607
|
-
chip.textContent = domainReady ? "Ready" : "Not ready";
|
|
1608
|
-
chip.className = "chip " + (domainReady ? "green" : "amber");
|
|
1609
|
-
}
|
|
1610
|
-
const list = readinessBox.querySelector("[data-domain-readiness-list]");
|
|
1611
|
-
if (list) {
|
|
1612
|
-
list.textContent = "";
|
|
1613
|
-
for (const c of checks) {
|
|
1614
|
-
const li = document.createElement("li");
|
|
1615
|
-
li.className = c.ok ? "ok" : "blocked";
|
|
1616
|
-
const mark = document.createElement("span");
|
|
1617
|
-
mark.className = "domain-readiness-mark";
|
|
1618
|
-
mark.textContent = c.ok ? "✓" : "✗";
|
|
1619
|
-
const label = document.createElement("b");
|
|
1620
|
-
label.textContent = c.label || c.id || "";
|
|
1621
|
-
li.append(mark, label);
|
|
1622
|
-
if (c.detail) {
|
|
1623
|
-
const detail = document.createElement("span");
|
|
1624
|
-
detail.className = "domain-readiness-detail";
|
|
1625
|
-
detail.textContent = c.detail;
|
|
1626
|
-
li.append(detail);
|
|
1627
|
-
}
|
|
1628
|
-
list.append(li);
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
const note = readinessBox.querySelector("[data-domain-readiness-note]");
|
|
1632
|
-
if (note) {
|
|
1633
|
-
note.textContent = payload && payload.ownerCapable === false
|
|
1634
|
-
? "Apex cutover is owner-only — sign in as the store owner to connect the domain."
|
|
1635
|
-
: domainReady
|
|
1636
|
-
? "All preconditions are green — the cutover is permitted."
|
|
1637
|
-
: "Clear the failing preconditions above before connecting the apex.";
|
|
1638
|
-
}
|
|
1639
|
-
// Gate the Connect button on server readiness in ADDITION to owner (the
|
|
1640
|
-
// markup already disables it for non-owners); never enable it when blocked.
|
|
1641
|
-
if (connectBtn && !connectBtn.dataset.ownerLocked) {
|
|
1642
|
-
connectBtn.disabled = !domainReady;
|
|
1643
|
-
}
|
|
1644
|
-
};
|
|
1645
|
-
|
|
1646
|
-
const loadReadiness = async () => {
|
|
1647
|
-
if (!readinessBox || !ds.domainReadinessEndpoint) return;
|
|
1648
|
-
try {
|
|
1649
|
-
const res = await fetch(ds.domainReadinessEndpoint, {
|
|
1650
|
-
headers: { accept: "application/json", "x-tot-capability": "ship-on-behalf" },
|
|
1651
|
-
});
|
|
1652
|
-
if (!res.ok) return;
|
|
1653
|
-
renderReadiness(await res.json());
|
|
1654
|
-
} catch {
|
|
1655
|
-
/* leave the last rendered readiness */
|
|
1656
|
-
}
|
|
1657
|
-
};
|
|
1658
|
-
|
|
1659
|
-
const domainStatusEl = domainPanel?.querySelector("[data-domain-status]");
|
|
1660
|
-
|
|
1661
|
-
const domainAction = async (body, button) => {
|
|
1662
|
-
button.disabled = true;
|
|
1663
|
-
setStatus(domainStatusEl, (body.action === "connect" ? "Connecting" : "Rolling back") + "…");
|
|
1664
|
-
try {
|
|
1665
|
-
const res = await fetch(ds.domainDispatchEndpoint, {
|
|
1666
|
-
method: "POST",
|
|
1667
|
-
headers: {
|
|
1668
|
-
"content-type": "application/json",
|
|
1669
|
-
"x-tot-capability": "ship-on-behalf",
|
|
1670
|
-
},
|
|
1671
|
-
body: JSON.stringify(body),
|
|
1672
|
-
});
|
|
1673
|
-
const data = await res.json().catch(() => ({}));
|
|
1674
|
-
if (res.ok && data.dispatched) {
|
|
1675
|
-
setStatus(
|
|
1676
|
-
domainStatusEl,
|
|
1677
|
-
(body.action === "connect" ? "Connect" : "Rollback") +
|
|
1678
|
-
(body.rehearsal ? " rehearsal" : "") +
|
|
1679
|
-
" dispatched — the run status updates below as it progresses.",
|
|
1680
|
-
"ok",
|
|
1681
|
-
);
|
|
1682
|
-
} else {
|
|
1683
|
-
setStatus(domainStatusEl, data.error || "Request failed (" + res.status + ")", "err");
|
|
1684
|
-
}
|
|
1685
|
-
} catch {
|
|
1686
|
-
setStatus(domainStatusEl, "Request failed — network error", "err");
|
|
1687
|
-
} finally {
|
|
1688
|
-
button.disabled = false;
|
|
1689
|
-
loadDomain();
|
|
1690
|
-
}
|
|
1691
|
-
};
|
|
1692
|
-
|
|
1693
|
-
domainPanel?.querySelector("[data-domain-connect]")?.addEventListener("click", (event) => {
|
|
1694
|
-
const confirmInput = domainPanel.querySelector("[data-domain-confirm]");
|
|
1695
|
-
const rehearsal = Boolean(domainPanel.querySelector("[data-domain-rehearsal]")?.checked);
|
|
1696
|
-
domainAction(
|
|
1697
|
-
{
|
|
1698
|
-
action: "connect",
|
|
1699
|
-
confirmDomain: (confirmInput?.value || "").trim(),
|
|
1700
|
-
rehearsal,
|
|
1701
|
-
},
|
|
1702
|
-
event.currentTarget,
|
|
1703
|
-
);
|
|
1704
|
-
});
|
|
1705
|
-
|
|
1706
|
-
domainPanel?.querySelector("[data-domain-rollback]")?.addEventListener("click", (event) => {
|
|
1707
|
-
if (
|
|
1708
|
-
!window.confirm(
|
|
1709
|
-
"Roll back " + ds.appDomain + " to the previous host? DNS returns to the captured records in about a minute.",
|
|
1710
|
-
)
|
|
1711
|
-
) {
|
|
1712
|
-
return;
|
|
1713
|
-
}
|
|
1714
|
-
domainAction({ action: "rollback" }, event.currentTarget);
|
|
1715
|
-
});
|
|
1716
|
-
|
|
1717
|
-
// ---- Wiring ------------------------------------------------------------
|
|
1718
|
-
|
|
1719
|
-
root.querySelector("[data-publish-refresh]")?.addEventListener("click", () => {
|
|
1720
|
-
setStatus(statusEl, "");
|
|
1721
|
-
loadQueue();
|
|
1722
|
-
loadVersions();
|
|
1723
|
-
loadDomain();
|
|
1724
|
-
});
|
|
1725
|
-
|
|
1726
|
-
loadQueue();
|
|
1727
|
-
loadVersions();
|
|
1728
|
-
loadDomain();
|
|
1729
|
-
})();
|
|
904
|
+
<script>
|
|
905
|
+
import { initAdminPublishTab } from "@/lib/publish/adminPublishTab";
|
|
906
|
+
initAdminPublishTab();
|
|
1730
907
|
</script>
|
|
1731
908
|
|
|
1732
909
|
<style>
|
|
@@ -1747,6 +924,19 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
1747
924
|
margin-left: 6px;
|
|
1748
925
|
}
|
|
1749
926
|
|
|
927
|
+
/* The per-row orphan "Delete this branch" action — a deliberate, destructive,
|
|
928
|
+
one-at-a-time HUMAN delete, styled distinct from the neutral "Clean up" and the
|
|
929
|
+
bulk "Sweep dead branches" so it never reads as a routine cleanup. */
|
|
930
|
+
.btn.branch-delete-orphan {
|
|
931
|
+
border-color: var(--admin-danger, #b3261e);
|
|
932
|
+
color: var(--admin-danger, #b3261e);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.btn.branch-delete-orphan:not(:disabled):hover {
|
|
936
|
+
background: var(--admin-danger, #b3261e);
|
|
937
|
+
color: #fff;
|
|
938
|
+
}
|
|
939
|
+
|
|
1750
940
|
.publish-status {
|
|
1751
941
|
min-height: 0;
|
|
1752
942
|
}
|
|
@@ -1763,126 +953,606 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
1763
953
|
color: var(--admin-red);
|
|
1764
954
|
}
|
|
1765
955
|
|
|
956
|
+
/* Public-site status card, verified-current tone — .risk-alert defaults to an
|
|
957
|
+
amber/warning look (admin.astro), wrong for "this is fine, confirmed fresh". */
|
|
958
|
+
.risk-alert.public-site-ok {
|
|
959
|
+
border-color: var(--admin-green);
|
|
960
|
+
background: var(--admin-green-soft);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.risk-alert.public-site-ok svg {
|
|
964
|
+
color: var(--admin-green);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/* ?pr=N deep link: "not in the queue" callout + row flash */
|
|
968
|
+
.publish-pr-callout {
|
|
969
|
+
border: 1px solid var(--admin-line-strong);
|
|
970
|
+
border-left: 3px solid var(--admin-amber);
|
|
971
|
+
background: var(--admin-surface-soft);
|
|
972
|
+
padding: 8px 12px;
|
|
973
|
+
border-radius: var(--admin-radius-sm);
|
|
974
|
+
color: var(--admin-ink);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
@keyframes publish-row-flash {
|
|
978
|
+
0%,
|
|
979
|
+
25% {
|
|
980
|
+
background-color: var(--admin-teal-soft);
|
|
981
|
+
}
|
|
982
|
+
100% {
|
|
983
|
+
background-color: transparent;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
.publish-row-highlight > td {
|
|
988
|
+
animation: publish-row-flash 2.4s ease-out;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
@media (prefers-reduced-motion: reduce) {
|
|
992
|
+
.publish-row-highlight > td {
|
|
993
|
+
animation: none;
|
|
994
|
+
background-color: var(--admin-teal-soft);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
1766
998
|
.publish-error-detail {
|
|
1767
999
|
color: var(--admin-red);
|
|
1768
1000
|
font-size: 12px;
|
|
1769
1001
|
}
|
|
1770
1002
|
|
|
1771
|
-
|
|
1003
|
+
/* ── Accept-to-preview LIFTOFF ─────────────────────────────────────────── */
|
|
1004
|
+
.liftoff-backdrop {
|
|
1005
|
+
position: fixed;
|
|
1006
|
+
inset: 0;
|
|
1007
|
+
z-index: 60;
|
|
1772
1008
|
display: grid;
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1009
|
+
place-items: center;
|
|
1010
|
+
padding: 24px;
|
|
1011
|
+
background: color-mix(in srgb, var(--admin-ink) 32%, transparent);
|
|
1012
|
+
backdrop-filter: blur(6px);
|
|
1013
|
+
-webkit-backdrop-filter: blur(6px);
|
|
1776
1014
|
}
|
|
1777
1015
|
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1016
|
+
/* `display: grid` above beats the UA's [hidden] rule — restate it, or the
|
|
1017
|
+
invisible backdrop click-blocks the entire page (caught by live test). */
|
|
1018
|
+
.liftoff-backdrop[hidden] {
|
|
1019
|
+
display: none;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.liftoff-card {
|
|
1023
|
+
position: relative;
|
|
1024
|
+
width: min(520px, 100%);
|
|
1025
|
+
max-height: min(84vh, 640px);
|
|
1026
|
+
overflow: auto;
|
|
1782
1027
|
border: 1px solid var(--admin-line-strong);
|
|
1783
|
-
border-radius: var(--admin-radius
|
|
1028
|
+
border-radius: var(--admin-radius, 12px);
|
|
1784
1029
|
background: var(--admin-surface);
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1030
|
+
box-shadow: 0 24px 80px rgba(8, 32, 30, 0.35);
|
|
1031
|
+
padding: 24px 26px;
|
|
1032
|
+
animation: liftoff-in 0.22s ease-out;
|
|
1788
1033
|
}
|
|
1789
1034
|
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1035
|
+
@keyframes liftoff-in {
|
|
1036
|
+
from { transform: translateY(10px) scale(0.985); opacity: 0; }
|
|
1037
|
+
to { transform: none; opacity: 1; }
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/* In-flight shimmer: a live gradient beam along the card's top edge. */
|
|
1041
|
+
.liftoff-card::before {
|
|
1042
|
+
content: "";
|
|
1043
|
+
position: absolute;
|
|
1044
|
+
inset: 0 0 auto 0;
|
|
1045
|
+
height: 3px;
|
|
1046
|
+
border-radius: inherit;
|
|
1047
|
+
opacity: 0;
|
|
1048
|
+
background: linear-gradient(90deg, transparent, var(--admin-teal, #0f9d8f), #f4b942, var(--admin-teal, #0f9d8f), transparent);
|
|
1049
|
+
background-size: 220% 100%;
|
|
1050
|
+
transition: opacity 0.3s ease;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.liftoff-card[data-mode="flight"]::before {
|
|
1054
|
+
opacity: 1;
|
|
1055
|
+
animation: liftoff-beam 1.6s linear infinite;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
@keyframes liftoff-beam {
|
|
1059
|
+
from { background-position: 200% 0; }
|
|
1060
|
+
to { background-position: -20% 0; }
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
.liftoff-kicker {
|
|
1064
|
+
margin: 0 0 2px;
|
|
1065
|
+
font-size: 11px;
|
|
1066
|
+
font-weight: 700;
|
|
1067
|
+
letter-spacing: 0.08em;
|
|
1068
|
+
text-transform: uppercase;
|
|
1069
|
+
color: var(--admin-teal, #0f9d8f);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.liftoff-title {
|
|
1073
|
+
margin: 0 0 14px;
|
|
1074
|
+
font-size: 19px;
|
|
1075
|
+
line-height: 1.25;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.liftoff-grid {
|
|
1079
|
+
display: grid;
|
|
1080
|
+
grid-template-columns: auto 1fr;
|
|
1081
|
+
gap: 6px 16px;
|
|
1082
|
+
margin: 0 0 12px;
|
|
1083
|
+
padding: 12px 14px;
|
|
1084
|
+
border: 1px solid var(--admin-line-strong);
|
|
1085
|
+
border-radius: var(--admin-radius-sm);
|
|
1086
|
+
background: var(--admin-surface-soft);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
.liftoff-grid dt {
|
|
1794
1090
|
color: var(--admin-muted);
|
|
1795
1091
|
font-size: 12px;
|
|
1796
1092
|
}
|
|
1797
1093
|
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
min-width: 0;
|
|
1802
|
-
width: 100%;
|
|
1803
|
-
}
|
|
1094
|
+
.liftoff-grid dd {
|
|
1095
|
+
margin: 0;
|
|
1096
|
+
font-size: 13px;
|
|
1804
1097
|
}
|
|
1805
1098
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
border-radius: 0.5rem;
|
|
1099
|
+
.liftoff-actions {
|
|
1100
|
+
display: flex;
|
|
1101
|
+
justify-content: flex-end;
|
|
1102
|
+
gap: 8px;
|
|
1103
|
+
margin-top: 16px;
|
|
1812
1104
|
}
|
|
1813
|
-
|
|
1105
|
+
|
|
1106
|
+
/* Merge-conflict guidance (outcome phase, reason "merge_failed" only) —
|
|
1107
|
+
directions, not mood: the terminal recovery front and center, the forge's
|
|
1108
|
+
raw refusal kept small underneath for support/diagnosis. Left-aligned
|
|
1109
|
+
inside the centered outcome phase: commands and prose read as
|
|
1110
|
+
instructions, not ceremony. */
|
|
1111
|
+
.liftoff-conflict {
|
|
1112
|
+
margin-top: 10px;
|
|
1113
|
+
text-align: left;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.liftoff-commands {
|
|
1117
|
+
margin: 10px 0 8px;
|
|
1118
|
+
padding: 10px 12px;
|
|
1119
|
+
border: 1px solid var(--admin-line-strong);
|
|
1120
|
+
border-radius: var(--admin-radius-sm);
|
|
1121
|
+
background: var(--admin-surface-soft);
|
|
1122
|
+
font-size: 12px;
|
|
1123
|
+
line-height: 1.55;
|
|
1124
|
+
overflow-x: auto;
|
|
1125
|
+
white-space: pre;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
.liftoff-conflict-actions {
|
|
1814
1129
|
display: flex;
|
|
1815
|
-
|
|
1816
|
-
gap:
|
|
1817
|
-
|
|
1130
|
+
justify-content: flex-end;
|
|
1131
|
+
gap: 8px;
|
|
1132
|
+
flex-wrap: wrap;
|
|
1818
1133
|
}
|
|
1819
|
-
|
|
1134
|
+
|
|
1135
|
+
.liftoff-conflict-raw {
|
|
1136
|
+
margin: 10px 0 0;
|
|
1137
|
+
font-size: 11px;
|
|
1138
|
+
color: var(--admin-muted);
|
|
1139
|
+
overflow-wrap: anywhere;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/* u4 — automate-first recovery: the primary action leads, option cards appear
|
|
1143
|
+
only on a genuine overlap, the terminal guidance is a collapsed fallback. */
|
|
1144
|
+
.liftoff-conflict-summary {
|
|
1145
|
+
margin: 0 0 10px;
|
|
1146
|
+
}
|
|
1147
|
+
.liftoff-conflict-primary {
|
|
1148
|
+
display: flex;
|
|
1149
|
+
flex-direction: column;
|
|
1150
|
+
align-items: flex-start;
|
|
1151
|
+
gap: 8px;
|
|
1152
|
+
margin-bottom: 12px;
|
|
1153
|
+
}
|
|
1154
|
+
.liftoff-conflict-primary .btn.primary {
|
|
1155
|
+
align-self: flex-start;
|
|
1156
|
+
}
|
|
1157
|
+
.liftoff-conflict-primary-status[data-state="err"],
|
|
1158
|
+
.liftoff-conflict-cards-status[data-state="err"] {
|
|
1159
|
+
color: var(--admin-danger, #b3261e);
|
|
1160
|
+
}
|
|
1161
|
+
.liftoff-conflict-cards {
|
|
1162
|
+
margin: 4px 0 12px;
|
|
1163
|
+
padding: 12px;
|
|
1164
|
+
border: 1px solid var(--admin-line-strong);
|
|
1165
|
+
border-radius: var(--admin-radius-sm);
|
|
1166
|
+
background: var(--admin-surface-soft);
|
|
1167
|
+
}
|
|
1168
|
+
.liftoff-conflict-files {
|
|
1820
1169
|
list-style: none;
|
|
1821
|
-
margin: 0;
|
|
1822
|
-
padding:
|
|
1170
|
+
margin: 8px 0 12px;
|
|
1171
|
+
padding: 8px 10px;
|
|
1172
|
+
border-radius: var(--admin-radius-sm);
|
|
1173
|
+
background: var(--admin-surface, #fff);
|
|
1174
|
+
font-size: 12px;
|
|
1175
|
+
line-height: 1.6;
|
|
1176
|
+
overflow-x: auto;
|
|
1177
|
+
}
|
|
1178
|
+
.liftoff-conflict-card-grid {
|
|
1823
1179
|
display: grid;
|
|
1824
|
-
|
|
1180
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
1181
|
+
gap: 8px;
|
|
1825
1182
|
}
|
|
1826
|
-
.
|
|
1183
|
+
.liftoff-option-card {
|
|
1827
1184
|
display: flex;
|
|
1828
|
-
|
|
1829
|
-
gap:
|
|
1830
|
-
|
|
1185
|
+
flex-direction: column;
|
|
1186
|
+
gap: 4px;
|
|
1187
|
+
text-align: left;
|
|
1188
|
+
padding: 12px;
|
|
1189
|
+
border: 1px solid var(--admin-line-strong);
|
|
1190
|
+
border-radius: var(--admin-radius-sm);
|
|
1191
|
+
background: var(--admin-surface, #fff);
|
|
1192
|
+
cursor: pointer;
|
|
1193
|
+
font: inherit;
|
|
1194
|
+
color: inherit;
|
|
1831
1195
|
}
|
|
1832
|
-
.
|
|
1833
|
-
|
|
1834
|
-
width: 1em;
|
|
1196
|
+
.liftoff-option-card:hover:not(:disabled) {
|
|
1197
|
+
border-color: var(--admin-accent, #0f9d8f);
|
|
1835
1198
|
}
|
|
1836
|
-
.
|
|
1837
|
-
|
|
1199
|
+
.liftoff-option-card:disabled {
|
|
1200
|
+
opacity: 0.55;
|
|
1201
|
+
cursor: default;
|
|
1838
1202
|
}
|
|
1839
|
-
.
|
|
1840
|
-
color: var(--
|
|
1203
|
+
.liftoff-option-card[data-recommended="1"] {
|
|
1204
|
+
border-color: var(--admin-accent, #0f9d8f);
|
|
1205
|
+
box-shadow: inset 0 0 0 1px var(--admin-accent, #0f9d8f);
|
|
1841
1206
|
}
|
|
1842
|
-
.
|
|
1843
|
-
|
|
1207
|
+
.liftoff-option-card b {
|
|
1208
|
+
font-size: 13px;
|
|
1844
1209
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1210
|
+
.liftoff-option-card span {
|
|
1211
|
+
font-size: 11px;
|
|
1212
|
+
color: var(--admin-muted);
|
|
1213
|
+
}
|
|
1214
|
+
.liftoff-conflict-expert {
|
|
1215
|
+
margin-top: 10px;
|
|
1216
|
+
border-top: 1px solid var(--admin-line, #e5e5e5);
|
|
1217
|
+
padding-top: 8px;
|
|
1218
|
+
}
|
|
1219
|
+
.liftoff-conflict-expert > summary {
|
|
1220
|
+
cursor: pointer;
|
|
1221
|
+
font-size: 12px;
|
|
1222
|
+
color: var(--admin-muted);
|
|
1223
|
+
user-select: none;
|
|
1850
1224
|
}
|
|
1851
1225
|
|
|
1852
|
-
.
|
|
1226
|
+
.liftoff-stages {
|
|
1853
1227
|
list-style: none;
|
|
1854
|
-
margin: 0;
|
|
1228
|
+
margin: 6px 0 4px;
|
|
1855
1229
|
padding: 0;
|
|
1856
1230
|
display: grid;
|
|
1857
|
-
gap:
|
|
1231
|
+
gap: 0;
|
|
1858
1232
|
}
|
|
1859
1233
|
|
|
1860
|
-
.
|
|
1234
|
+
.liftoff-stages li {
|
|
1235
|
+
position: relative;
|
|
1861
1236
|
display: flex;
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1237
|
+
gap: 12px;
|
|
1238
|
+
align-items: flex-start;
|
|
1239
|
+
padding: 9px 0;
|
|
1865
1240
|
}
|
|
1866
1241
|
|
|
1867
|
-
.
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1242
|
+
/* Connecting rail between the dots. */
|
|
1243
|
+
.liftoff-stages li + li::before {
|
|
1244
|
+
content: "";
|
|
1245
|
+
position: absolute;
|
|
1246
|
+
left: 10px;
|
|
1247
|
+
top: -8px;
|
|
1248
|
+
height: 16px;
|
|
1249
|
+
width: 2px;
|
|
1250
|
+
background: var(--admin-line-strong);
|
|
1873
1251
|
}
|
|
1874
1252
|
|
|
1875
|
-
.
|
|
1876
|
-
display:
|
|
1877
|
-
|
|
1878
|
-
gap: 0.5rem;
|
|
1253
|
+
.liftoff-stages li > div {
|
|
1254
|
+
display: grid;
|
|
1255
|
+
gap: 1px;
|
|
1879
1256
|
}
|
|
1880
1257
|
|
|
1881
|
-
.
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1258
|
+
.liftoff-stages b {
|
|
1259
|
+
font-size: 13px;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
.liftoff-stages span:not(.liftoff-dot) {
|
|
1263
|
+
font-size: 12px;
|
|
1264
|
+
color: var(--admin-muted);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
.liftoff-dot {
|
|
1268
|
+
flex: none;
|
|
1269
|
+
width: 21px;
|
|
1270
|
+
height: 21px;
|
|
1271
|
+
margin-top: 1px;
|
|
1272
|
+
border-radius: 50%;
|
|
1273
|
+
border: 2px solid var(--admin-line-strong);
|
|
1274
|
+
display: grid;
|
|
1275
|
+
place-items: center;
|
|
1276
|
+
font-size: 12px;
|
|
1277
|
+
font-weight: 700;
|
|
1278
|
+
color: #fff;
|
|
1279
|
+
background: transparent;
|
|
1280
|
+
transition: all 0.25s ease;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
.liftoff-stages li[data-state="active"] .liftoff-dot {
|
|
1284
|
+
border-color: var(--admin-teal, #0f9d8f);
|
|
1285
|
+
box-shadow: 0 0 0 0 color-mix(in srgb, var(--admin-teal, #0f9d8f) 45%, transparent);
|
|
1286
|
+
animation: liftoff-pulse 1.3s ease-out infinite;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
.liftoff-stages li[data-state="active"] b {
|
|
1290
|
+
color: var(--admin-teal, #0f9d8f);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
@keyframes liftoff-pulse {
|
|
1294
|
+
from { box-shadow: 0 0 0 0 color-mix(in srgb, var(--admin-teal, #0f9d8f) 45%, transparent); }
|
|
1295
|
+
to { box-shadow: 0 0 0 10px transparent; }
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.liftoff-stages li[data-state="done"] .liftoff-dot {
|
|
1299
|
+
border-color: var(--admin-green);
|
|
1300
|
+
background: var(--admin-green);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
.liftoff-stages li[data-state="done"] .liftoff-dot::after {
|
|
1304
|
+
content: "✓";
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
.liftoff-stages li[data-state="fail"] .liftoff-dot {
|
|
1308
|
+
border-color: var(--admin-red);
|
|
1309
|
+
background: var(--admin-red);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
.liftoff-stages li[data-state="fail"] .liftoff-dot::after {
|
|
1313
|
+
content: "✕";
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
.liftoff-live {
|
|
1317
|
+
margin: 8px 0 0;
|
|
1318
|
+
min-height: 18px;
|
|
1319
|
+
font-size: 13px;
|
|
1320
|
+
color: var(--admin-ink);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
.liftoff-meta {
|
|
1324
|
+
display: flex;
|
|
1325
|
+
justify-content: space-between;
|
|
1326
|
+
align-items: center;
|
|
1327
|
+
margin-top: 14px;
|
|
1328
|
+
color: var(--admin-muted);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
.liftoff-outcome-phase {
|
|
1332
|
+
text-align: center;
|
|
1333
|
+
padding-top: 6px;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
.liftoff-outcome-phase .liftoff-actions {
|
|
1337
|
+
justify-content: center;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
.liftoff-seal {
|
|
1341
|
+
width: 56px;
|
|
1342
|
+
height: 56px;
|
|
1343
|
+
margin: 4px auto 12px;
|
|
1344
|
+
border-radius: 50%;
|
|
1345
|
+
display: grid;
|
|
1346
|
+
place-items: center;
|
|
1347
|
+
font-size: 28px;
|
|
1348
|
+
font-weight: 700;
|
|
1349
|
+
color: #fff;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
.liftoff-seal[data-state="ok"] {
|
|
1353
|
+
background: var(--admin-green);
|
|
1354
|
+
animation: liftoff-seal-pop 0.45s cubic-bezier(0.2, 1.6, 0.4, 1);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
.liftoff-seal[data-state="ok"]::after {
|
|
1358
|
+
content: "✓";
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.liftoff-seal[data-state="err"] {
|
|
1362
|
+
background: var(--admin-red);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
.liftoff-seal[data-state="err"]::after {
|
|
1366
|
+
content: "✕";
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
@keyframes liftoff-seal-pop {
|
|
1370
|
+
from { transform: scale(0.4); opacity: 0; }
|
|
1371
|
+
60% { transform: scale(1.12); }
|
|
1372
|
+
to { transform: none; opacity: 1; }
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
.liftoff-confetti {
|
|
1376
|
+
pointer-events: none;
|
|
1377
|
+
position: absolute;
|
|
1378
|
+
inset: 0;
|
|
1379
|
+
overflow: hidden;
|
|
1380
|
+
border-radius: inherit;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.liftoff-confetti i {
|
|
1384
|
+
position: absolute;
|
|
1385
|
+
top: -12px;
|
|
1386
|
+
width: 7px;
|
|
1387
|
+
height: 11px;
|
|
1388
|
+
border-radius: 2px;
|
|
1389
|
+
opacity: 0;
|
|
1390
|
+
animation-name: liftoff-confetti-fall;
|
|
1391
|
+
animation-timing-function: ease-in;
|
|
1392
|
+
animation-fill-mode: forwards;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
@keyframes liftoff-confetti-fall {
|
|
1396
|
+
0% { opacity: 1; transform: translate(0, 0) rotate(0deg); }
|
|
1397
|
+
100% {
|
|
1398
|
+
opacity: 0.9;
|
|
1399
|
+
transform: translate(var(--liftoff-drift, 0px), 660px) rotate(var(--liftoff-spin, 240deg));
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1404
|
+
.liftoff-card,
|
|
1405
|
+
.liftoff-card[data-mode="flight"]::before,
|
|
1406
|
+
.liftoff-stages li[data-state="active"] .liftoff-dot,
|
|
1407
|
+
.liftoff-seal[data-state="ok"] {
|
|
1408
|
+
animation: none;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
.domain-connect-controls {
|
|
1413
|
+
display: grid;
|
|
1414
|
+
gap: 8px;
|
|
1415
|
+
justify-items: end;
|
|
1416
|
+
min-width: 220px;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
.domain-connect-controls input[type="text"] {
|
|
1420
|
+
width: 100%;
|
|
1421
|
+
min-height: 36px;
|
|
1422
|
+
padding: 0 10px;
|
|
1423
|
+
border: 1px solid var(--admin-line-strong);
|
|
1424
|
+
border-radius: var(--admin-radius-sm);
|
|
1425
|
+
background: var(--admin-surface);
|
|
1426
|
+
color: var(--admin-ink);
|
|
1427
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1428
|
+
font-size: 12px;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
.domain-rehearsal {
|
|
1432
|
+
display: inline-flex;
|
|
1433
|
+
align-items: center;
|
|
1434
|
+
gap: 6px;
|
|
1435
|
+
color: var(--admin-muted);
|
|
1436
|
+
font-size: 12px;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
@media (max-width: 720px) {
|
|
1440
|
+
.domain-connect-controls {
|
|
1441
|
+
justify-items: stretch;
|
|
1442
|
+
min-width: 0;
|
|
1443
|
+
width: 100%;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/* u9: apex-cutover readiness itemization */
|
|
1448
|
+
.domain-readiness {
|
|
1449
|
+
margin: 0.75rem 0;
|
|
1450
|
+
padding: 0.75rem 1rem;
|
|
1451
|
+
border: 1px solid var(--hairline, rgba(0, 0, 0, 0.12));
|
|
1452
|
+
border-radius: 0.5rem;
|
|
1453
|
+
}
|
|
1454
|
+
.domain-readiness-head {
|
|
1455
|
+
display: flex;
|
|
1456
|
+
align-items: center;
|
|
1457
|
+
gap: 0.5rem;
|
|
1458
|
+
margin-bottom: 0.5rem;
|
|
1459
|
+
}
|
|
1460
|
+
.domain-readiness-list {
|
|
1461
|
+
list-style: none;
|
|
1462
|
+
margin: 0;
|
|
1463
|
+
padding: 0;
|
|
1464
|
+
display: grid;
|
|
1465
|
+
gap: 0.25rem;
|
|
1466
|
+
}
|
|
1467
|
+
.domain-readiness-list li {
|
|
1468
|
+
display: grid;
|
|
1469
|
+
gap: 0.1rem;
|
|
1470
|
+
font-size: 0.9em;
|
|
1471
|
+
}
|
|
1472
|
+
.domain-readiness-item-head {
|
|
1473
|
+
display: flex;
|
|
1474
|
+
align-items: baseline;
|
|
1475
|
+
gap: 0.4rem;
|
|
1476
|
+
}
|
|
1477
|
+
.domain-readiness-mark {
|
|
1478
|
+
font-weight: 700;
|
|
1479
|
+
width: 1em;
|
|
1480
|
+
flex: 0 0 auto;
|
|
1481
|
+
}
|
|
1482
|
+
.domain-readiness-list li.ok .domain-readiness-mark {
|
|
1483
|
+
color: var(--good, #1a7f37);
|
|
1484
|
+
}
|
|
1485
|
+
.domain-readiness-list li.blocked .domain-readiness-mark {
|
|
1486
|
+
color: var(--bad, #b42318);
|
|
1487
|
+
}
|
|
1488
|
+
/* detail + remedy sit on their OWN lines, indented under the label (past the
|
|
1489
|
+
mark) so neither runs together with the bold label above it. */
|
|
1490
|
+
.domain-readiness-detail,
|
|
1491
|
+
.domain-readiness-remedy {
|
|
1492
|
+
margin: 0;
|
|
1493
|
+
padding-left: 1.4em;
|
|
1494
|
+
}
|
|
1495
|
+
.domain-readiness-detail {
|
|
1496
|
+
opacity: 0.75;
|
|
1497
|
+
}
|
|
1498
|
+
.domain-readiness-remedy {
|
|
1499
|
+
color: var(--accent, #0b6bcb);
|
|
1500
|
+
opacity: 0.95;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
/* b11: aggregate card + ship plan */
|
|
1504
|
+
.aggregate-prs {
|
|
1505
|
+
display: grid;
|
|
1506
|
+
gap: 6px;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.aggregate-prs-list {
|
|
1510
|
+
list-style: none;
|
|
1511
|
+
margin: 0;
|
|
1512
|
+
padding: 0;
|
|
1513
|
+
display: grid;
|
|
1514
|
+
gap: 4px;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
.aggregate-prs-list li {
|
|
1518
|
+
display: flex;
|
|
1519
|
+
align-items: center;
|
|
1520
|
+
gap: 8px;
|
|
1521
|
+
font-size: 13px;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/* u14 — provenance ("integrated <age> by <email>") sits after the sha, subtle. */
|
|
1525
|
+
.aggregate-pr-meta {
|
|
1526
|
+
font-size: 12px;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/* u14 — the per-change Undo (revert) button is pushed to the row's trailing edge
|
|
1530
|
+
and kept compact so the batch reads as a list, not a stack of buttons. */
|
|
1531
|
+
.aggregate-prs-list .btn-compact {
|
|
1532
|
+
margin-left: auto;
|
|
1533
|
+
padding: 2px 10px;
|
|
1534
|
+
font-size: 12px;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
.aggregate-plan {
|
|
1538
|
+
padding: 0.75rem 1rem;
|
|
1539
|
+
border: 1px solid var(--hairline, rgba(0, 0, 0, 0.12));
|
|
1540
|
+
border-radius: 0.5rem;
|
|
1541
|
+
display: grid;
|
|
1542
|
+
gap: 8px;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
.aggregate-plan-head {
|
|
1546
|
+
display: flex;
|
|
1547
|
+
align-items: center;
|
|
1548
|
+
gap: 0.5rem;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
.aggregate-plan-grid {
|
|
1552
|
+
display: grid;
|
|
1553
|
+
grid-template-columns: max-content 1fr;
|
|
1554
|
+
gap: 4px 16px;
|
|
1555
|
+
margin: 0;
|
|
1886
1556
|
}
|
|
1887
1557
|
|
|
1888
1558
|
.aggregate-plan-grid dt {
|
|
@@ -1896,16 +1566,720 @@ const domainReadinessEndpoint = withBase(basePath || "", "/api/domain/readiness"
|
|
|
1896
1566
|
overflow-wrap: anywhere;
|
|
1897
1567
|
}
|
|
1898
1568
|
|
|
1569
|
+
/* ── Preview-aggregate console — the go-live surface ─────────────────────
|
|
1570
|
+
Everything here is scoped under [data-publish-aggregate] / .agg-* so it
|
|
1571
|
+
never leaks into the other panels (candidate queue, versions, branches)
|
|
1572
|
+
that share .panel / .field / .check-row / .chip. */
|
|
1573
|
+
.agg-console .panel-body {
|
|
1574
|
+
gap: 16px;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
[data-publish-aggregate] .sha-token {
|
|
1578
|
+
display: inline-flex;
|
|
1579
|
+
align-items: center;
|
|
1580
|
+
padding: 2px 8px;
|
|
1581
|
+
border: 1px solid var(--admin-line-strong);
|
|
1582
|
+
border-radius: 5px;
|
|
1583
|
+
background: var(--admin-surface-soft);
|
|
1584
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1585
|
+
font-size: 12.5px;
|
|
1586
|
+
color: var(--admin-ink);
|
|
1587
|
+
}
|
|
1588
|
+
[data-publish-aggregate] .sha-token.ghost {
|
|
1589
|
+
background: transparent;
|
|
1590
|
+
color: var(--admin-muted);
|
|
1591
|
+
border-style: dashed;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/* AI-assembled release summary */
|
|
1595
|
+
.agg-summary {
|
|
1596
|
+
display: grid;
|
|
1597
|
+
gap: 11px;
|
|
1598
|
+
padding: 15px 16px;
|
|
1599
|
+
border: 1px solid var(--admin-line);
|
|
1600
|
+
border-radius: var(--admin-radius);
|
|
1601
|
+
background:
|
|
1602
|
+
linear-gradient(180deg, var(--admin-teal-soft), transparent 62%),
|
|
1603
|
+
var(--admin-surface);
|
|
1604
|
+
}
|
|
1605
|
+
.agg-summary[hidden] {
|
|
1606
|
+
display: none;
|
|
1607
|
+
}
|
|
1608
|
+
.agg-summary-head {
|
|
1609
|
+
display: flex;
|
|
1610
|
+
align-items: center;
|
|
1611
|
+
gap: 10px;
|
|
1612
|
+
}
|
|
1613
|
+
.agg-summary-glyph {
|
|
1614
|
+
flex: none;
|
|
1615
|
+
width: 26px;
|
|
1616
|
+
height: 26px;
|
|
1617
|
+
display: grid;
|
|
1618
|
+
place-items: center;
|
|
1619
|
+
border-radius: 7px;
|
|
1620
|
+
background: var(--admin-surface);
|
|
1621
|
+
color: var(--admin-teal-dark);
|
|
1622
|
+
border: 1px solid color-mix(in srgb, var(--admin-teal) 30%, transparent);
|
|
1623
|
+
}
|
|
1624
|
+
/* The sparkle glyphs are filled shapes; opt out of the global stroked-svg rule. */
|
|
1625
|
+
.agg-summary-glyph svg,
|
|
1626
|
+
.agg-ai-chip svg {
|
|
1627
|
+
fill: currentColor;
|
|
1628
|
+
stroke: none;
|
|
1629
|
+
}
|
|
1630
|
+
.agg-summary-glyph svg {
|
|
1631
|
+
width: 15px;
|
|
1632
|
+
height: 15px;
|
|
1633
|
+
}
|
|
1634
|
+
.agg-summary-label {
|
|
1635
|
+
flex: 1;
|
|
1636
|
+
font-size: 11px;
|
|
1637
|
+
font-weight: 760;
|
|
1638
|
+
letter-spacing: 0.08em;
|
|
1639
|
+
text-transform: uppercase;
|
|
1640
|
+
color: var(--admin-muted);
|
|
1641
|
+
}
|
|
1642
|
+
.agg-ai-chip {
|
|
1643
|
+
display: inline-flex;
|
|
1644
|
+
align-items: center;
|
|
1645
|
+
gap: 5px;
|
|
1646
|
+
padding: 3px 9px;
|
|
1647
|
+
border-radius: 999px;
|
|
1648
|
+
background: var(--admin-teal-soft);
|
|
1649
|
+
color: #007973;
|
|
1650
|
+
font-size: 10px;
|
|
1651
|
+
font-weight: 760;
|
|
1652
|
+
letter-spacing: 0.06em;
|
|
1653
|
+
text-transform: uppercase;
|
|
1654
|
+
}
|
|
1655
|
+
.agg-ai-chip svg {
|
|
1656
|
+
width: 10px;
|
|
1657
|
+
height: 10px;
|
|
1658
|
+
}
|
|
1659
|
+
.agg-summary-lead {
|
|
1660
|
+
margin: 0;
|
|
1661
|
+
font-size: 15px;
|
|
1662
|
+
line-height: 1.55;
|
|
1663
|
+
color: var(--admin-ink);
|
|
1664
|
+
max-width: 68ch;
|
|
1665
|
+
}
|
|
1666
|
+
.agg-summary-points {
|
|
1667
|
+
list-style: none;
|
|
1668
|
+
margin: 0;
|
|
1669
|
+
padding: 0;
|
|
1670
|
+
display: grid;
|
|
1671
|
+
gap: 7px;
|
|
1672
|
+
}
|
|
1673
|
+
.agg-summary-points[hidden] {
|
|
1674
|
+
display: none;
|
|
1675
|
+
}
|
|
1676
|
+
.agg-summary-points li {
|
|
1677
|
+
display: flex;
|
|
1678
|
+
align-items: flex-start;
|
|
1679
|
+
gap: 9px;
|
|
1680
|
+
font-size: 13px;
|
|
1681
|
+
color: var(--admin-muted);
|
|
1682
|
+
line-height: 1.45;
|
|
1683
|
+
}
|
|
1684
|
+
.agg-summary-points .agg-tick {
|
|
1685
|
+
flex: none;
|
|
1686
|
+
margin-top: 1px;
|
|
1687
|
+
width: 16px;
|
|
1688
|
+
height: 16px;
|
|
1689
|
+
border-radius: 50%;
|
|
1690
|
+
display: grid;
|
|
1691
|
+
place-items: center;
|
|
1692
|
+
background: var(--admin-green-soft);
|
|
1693
|
+
color: var(--admin-green);
|
|
1694
|
+
}
|
|
1695
|
+
.agg-summary-points .agg-tick svg {
|
|
1696
|
+
width: 10px;
|
|
1697
|
+
height: 10px;
|
|
1698
|
+
}
|
|
1699
|
+
.agg-summary-points b {
|
|
1700
|
+
color: var(--admin-ink);
|
|
1701
|
+
font-weight: 640;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/* Chain of custody: build → evidence → digest */
|
|
1705
|
+
.agg-chain {
|
|
1706
|
+
display: grid;
|
|
1707
|
+
grid-template-columns: 1fr auto 1fr auto 1fr;
|
|
1708
|
+
align-items: stretch;
|
|
1709
|
+
padding: 16px;
|
|
1710
|
+
border: 1px solid var(--admin-line);
|
|
1711
|
+
border-radius: var(--admin-radius);
|
|
1712
|
+
background: var(--admin-surface-soft);
|
|
1713
|
+
}
|
|
1714
|
+
.agg-stop {
|
|
1715
|
+
display: flex;
|
|
1716
|
+
flex-direction: column;
|
|
1717
|
+
gap: 7px;
|
|
1718
|
+
padding: 2px 12px;
|
|
1719
|
+
min-width: 0;
|
|
1720
|
+
}
|
|
1721
|
+
.agg-stop:first-child {
|
|
1722
|
+
padding-left: 2px;
|
|
1723
|
+
}
|
|
1724
|
+
.agg-stop:last-child {
|
|
1725
|
+
padding-right: 2px;
|
|
1726
|
+
}
|
|
1727
|
+
.agg-stop-label {
|
|
1728
|
+
font-size: 10.5px;
|
|
1729
|
+
font-weight: 760;
|
|
1730
|
+
letter-spacing: 0.08em;
|
|
1731
|
+
text-transform: uppercase;
|
|
1732
|
+
color: var(--admin-muted);
|
|
1733
|
+
}
|
|
1734
|
+
.agg-stop-val {
|
|
1735
|
+
display: flex;
|
|
1736
|
+
align-items: center;
|
|
1737
|
+
gap: 8px;
|
|
1738
|
+
font-size: 15px;
|
|
1739
|
+
font-weight: 680;
|
|
1740
|
+
}
|
|
1741
|
+
.agg-stop-val strong {
|
|
1742
|
+
font-size: 15px;
|
|
1743
|
+
}
|
|
1744
|
+
.agg-stop-cap {
|
|
1745
|
+
margin: 0;
|
|
1746
|
+
font-size: 12px;
|
|
1747
|
+
color: var(--admin-muted);
|
|
1748
|
+
line-height: 1.45;
|
|
1749
|
+
}
|
|
1750
|
+
.agg-stop-cap a {
|
|
1751
|
+
white-space: nowrap;
|
|
1752
|
+
}
|
|
1753
|
+
.agg-digest {
|
|
1754
|
+
padding: 2px 8px;
|
|
1755
|
+
border: 1px solid color-mix(in srgb, var(--admin-teal) 55%, var(--admin-line-strong));
|
|
1756
|
+
border-radius: 5px;
|
|
1757
|
+
background: var(--admin-teal-soft);
|
|
1758
|
+
color: #007973;
|
|
1759
|
+
}
|
|
1760
|
+
[data-publish-aggregate] [data-aggregate-evidence][data-state="passed"] {
|
|
1761
|
+
color: var(--admin-green);
|
|
1762
|
+
}
|
|
1763
|
+
[data-publish-aggregate] [data-aggregate-evidence][data-state="failed"] {
|
|
1764
|
+
color: var(--admin-red);
|
|
1765
|
+
}
|
|
1766
|
+
.agg-link {
|
|
1767
|
+
display: flex;
|
|
1768
|
+
flex-direction: column;
|
|
1769
|
+
align-items: center;
|
|
1770
|
+
justify-content: center;
|
|
1771
|
+
gap: 6px;
|
|
1772
|
+
padding: 0 2px;
|
|
1773
|
+
align-self: center;
|
|
1774
|
+
}
|
|
1775
|
+
.agg-flowlabel {
|
|
1776
|
+
font-size: 9px;
|
|
1777
|
+
font-weight: 720;
|
|
1778
|
+
letter-spacing: 0.06em;
|
|
1779
|
+
text-transform: uppercase;
|
|
1780
|
+
color: var(--admin-muted-2);
|
|
1781
|
+
}
|
|
1782
|
+
.agg-arrow {
|
|
1783
|
+
display: grid;
|
|
1784
|
+
place-items: center;
|
|
1785
|
+
color: var(--admin-teal);
|
|
1786
|
+
}
|
|
1787
|
+
.agg-arrow svg {
|
|
1788
|
+
width: 32px;
|
|
1789
|
+
height: 10px;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/* Staged → live delta strip */
|
|
1793
|
+
.agg-delta-strip {
|
|
1794
|
+
display: flex;
|
|
1795
|
+
align-items: center;
|
|
1796
|
+
gap: 12px;
|
|
1797
|
+
flex-wrap: wrap;
|
|
1798
|
+
padding: 10px 13px;
|
|
1799
|
+
border: 1px solid var(--admin-line);
|
|
1800
|
+
border-radius: var(--admin-radius-sm);
|
|
1801
|
+
background: var(--admin-surface-soft);
|
|
1802
|
+
}
|
|
1803
|
+
.agg-delta-strip[hidden] {
|
|
1804
|
+
display: none;
|
|
1805
|
+
}
|
|
1806
|
+
.agg-delta-node {
|
|
1807
|
+
display: flex;
|
|
1808
|
+
flex-direction: column;
|
|
1809
|
+
gap: 3px;
|
|
1810
|
+
}
|
|
1811
|
+
.agg-delta-mid {
|
|
1812
|
+
display: flex;
|
|
1813
|
+
align-items: center;
|
|
1814
|
+
gap: 8px;
|
|
1815
|
+
color: var(--admin-teal);
|
|
1816
|
+
}
|
|
1817
|
+
.agg-delta-mid svg {
|
|
1818
|
+
width: 24px;
|
|
1819
|
+
height: 10px;
|
|
1820
|
+
}
|
|
1821
|
+
.agg-delta-count {
|
|
1822
|
+
padding: 2px 9px;
|
|
1823
|
+
border-radius: 999px;
|
|
1824
|
+
background: var(--admin-teal-soft);
|
|
1825
|
+
color: #007973;
|
|
1826
|
+
font-size: 11.5px;
|
|
1827
|
+
font-weight: 720;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
/* Staged change rows (richer than the plain PR list) */
|
|
1831
|
+
.agg-changes li.agg-change {
|
|
1832
|
+
display: grid;
|
|
1833
|
+
grid-template-columns: 1fr auto;
|
|
1834
|
+
align-items: center;
|
|
1835
|
+
gap: 12px;
|
|
1836
|
+
padding: 10px 12px;
|
|
1837
|
+
border: 1px solid var(--admin-line);
|
|
1838
|
+
border-radius: var(--admin-radius-sm);
|
|
1839
|
+
background: var(--admin-surface);
|
|
1840
|
+
}
|
|
1841
|
+
.agg-change-main {
|
|
1842
|
+
display: grid;
|
|
1843
|
+
gap: 4px;
|
|
1844
|
+
min-width: 0;
|
|
1845
|
+
}
|
|
1846
|
+
.agg-change-line {
|
|
1847
|
+
display: flex;
|
|
1848
|
+
align-items: center;
|
|
1849
|
+
gap: 10px;
|
|
1850
|
+
flex-wrap: wrap;
|
|
1851
|
+
}
|
|
1852
|
+
.agg-change-pr {
|
|
1853
|
+
font-weight: 720;
|
|
1854
|
+
font-size: 13px;
|
|
1855
|
+
}
|
|
1856
|
+
.agg-change-title {
|
|
1857
|
+
font-weight: 600;
|
|
1858
|
+
font-size: 13.5px;
|
|
1859
|
+
}
|
|
1860
|
+
.agg-change-sub {
|
|
1861
|
+
color: var(--admin-muted);
|
|
1862
|
+
font-size: 12px;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
/* Publish plan → launch manifest */
|
|
1866
|
+
[data-publish-aggregate] .agg-manifest {
|
|
1867
|
+
display: block;
|
|
1868
|
+
padding: 0;
|
|
1869
|
+
overflow: hidden;
|
|
1870
|
+
background:
|
|
1871
|
+
linear-gradient(180deg, var(--admin-surface-tint), transparent 46px),
|
|
1872
|
+
var(--admin-surface);
|
|
1873
|
+
}
|
|
1874
|
+
.agg-manifest-head {
|
|
1875
|
+
padding: 12px 15px;
|
|
1876
|
+
border-bottom: 1px dashed var(--admin-line-strong);
|
|
1877
|
+
}
|
|
1878
|
+
.agg-manifest-head b {
|
|
1879
|
+
display: flex;
|
|
1880
|
+
align-items: center;
|
|
1881
|
+
gap: 8px;
|
|
1882
|
+
font-size: 13px;
|
|
1883
|
+
}
|
|
1884
|
+
.agg-manifest-head b svg {
|
|
1885
|
+
width: 15px;
|
|
1886
|
+
height: 15px;
|
|
1887
|
+
color: var(--admin-teal);
|
|
1888
|
+
}
|
|
1889
|
+
[data-publish-aggregate] .agg-manifest .aggregate-plan-grid {
|
|
1890
|
+
padding: 8px 15px 12px;
|
|
1891
|
+
}
|
|
1892
|
+
[data-publish-aggregate] .agg-manifest > .note {
|
|
1893
|
+
margin: 0;
|
|
1894
|
+
padding: 11px 15px;
|
|
1895
|
+
border-top: 1px dashed var(--admin-line-strong);
|
|
1896
|
+
background: var(--admin-surface-soft);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/* Openable "Included changes" */
|
|
1900
|
+
.agg-included {
|
|
1901
|
+
border-top: 1px dashed var(--admin-line-strong);
|
|
1902
|
+
}
|
|
1903
|
+
.agg-included[hidden] {
|
|
1904
|
+
display: none;
|
|
1905
|
+
}
|
|
1906
|
+
.agg-included-details > summary {
|
|
1907
|
+
list-style: none;
|
|
1908
|
+
cursor: pointer;
|
|
1909
|
+
display: flex;
|
|
1910
|
+
align-items: center;
|
|
1911
|
+
gap: 10px;
|
|
1912
|
+
padding: 11px 15px;
|
|
1913
|
+
font-size: 12px;
|
|
1914
|
+
color: var(--admin-muted);
|
|
1915
|
+
}
|
|
1916
|
+
.agg-included-details > summary::-webkit-details-marker {
|
|
1917
|
+
display: none;
|
|
1918
|
+
}
|
|
1919
|
+
.agg-included-details > summary::marker {
|
|
1920
|
+
content: "";
|
|
1921
|
+
}
|
|
1922
|
+
.agg-included-details > summary:focus-visible {
|
|
1923
|
+
outline: 2px solid var(--admin-teal);
|
|
1924
|
+
outline-offset: -2px;
|
|
1925
|
+
}
|
|
1926
|
+
.agg-included-lead {
|
|
1927
|
+
font-size: 11px;
|
|
1928
|
+
font-weight: 740;
|
|
1929
|
+
letter-spacing: 0.03em;
|
|
1930
|
+
text-transform: uppercase;
|
|
1931
|
+
color: var(--admin-ink);
|
|
1932
|
+
}
|
|
1933
|
+
.agg-included-badge {
|
|
1934
|
+
padding: 1px 8px;
|
|
1935
|
+
border-radius: 999px;
|
|
1936
|
+
background: var(--admin-surface-tint);
|
|
1937
|
+
color: #3f4d49;
|
|
1938
|
+
font-size: 11.5px;
|
|
1939
|
+
font-weight: 720;
|
|
1940
|
+
}
|
|
1941
|
+
.agg-included-hint {
|
|
1942
|
+
color: var(--admin-muted-2);
|
|
1943
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1944
|
+
font-size: 11.5px;
|
|
1945
|
+
overflow: hidden;
|
|
1946
|
+
text-overflow: ellipsis;
|
|
1947
|
+
white-space: nowrap;
|
|
1948
|
+
}
|
|
1949
|
+
.agg-chev {
|
|
1950
|
+
margin-left: auto;
|
|
1951
|
+
display: inline-flex;
|
|
1952
|
+
color: var(--admin-muted-2);
|
|
1953
|
+
transition: transform 0.18s ease;
|
|
1954
|
+
}
|
|
1955
|
+
.agg-chev svg {
|
|
1956
|
+
width: 14px;
|
|
1957
|
+
height: 14px;
|
|
1958
|
+
}
|
|
1959
|
+
.agg-included-details[open] > summary .agg-chev {
|
|
1960
|
+
transform: rotate(90deg);
|
|
1961
|
+
}
|
|
1962
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1963
|
+
.agg-chev {
|
|
1964
|
+
transition: none;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
.agg-included-list {
|
|
1968
|
+
list-style: none;
|
|
1969
|
+
margin: 0;
|
|
1970
|
+
padding: 0 15px 14px;
|
|
1971
|
+
display: grid;
|
|
1972
|
+
gap: 8px;
|
|
1973
|
+
}
|
|
1974
|
+
.agg-included-item {
|
|
1975
|
+
display: grid;
|
|
1976
|
+
gap: 5px;
|
|
1977
|
+
padding: 11px 12px;
|
|
1978
|
+
border: 1px solid var(--admin-line);
|
|
1979
|
+
border-radius: var(--admin-radius-sm);
|
|
1980
|
+
background: var(--admin-surface-soft);
|
|
1981
|
+
}
|
|
1982
|
+
.agg-included-top {
|
|
1983
|
+
display: flex;
|
|
1984
|
+
align-items: center;
|
|
1985
|
+
gap: 10px;
|
|
1986
|
+
flex-wrap: wrap;
|
|
1987
|
+
}
|
|
1988
|
+
.agg-included-pr {
|
|
1989
|
+
font-weight: 720;
|
|
1990
|
+
font-size: 13px;
|
|
1991
|
+
}
|
|
1992
|
+
.agg-included-title {
|
|
1993
|
+
font-weight: 600;
|
|
1994
|
+
font-size: 13.5px;
|
|
1995
|
+
}
|
|
1996
|
+
.agg-included-desc {
|
|
1997
|
+
margin: 0;
|
|
1998
|
+
color: var(--admin-muted);
|
|
1999
|
+
font-size: 12.5px;
|
|
2000
|
+
line-height: 1.5;
|
|
2001
|
+
}
|
|
2002
|
+
.agg-included-meta {
|
|
2003
|
+
display: flex;
|
|
2004
|
+
align-items: center;
|
|
2005
|
+
gap: 6px;
|
|
2006
|
+
flex-wrap: wrap;
|
|
2007
|
+
color: var(--admin-muted-2);
|
|
2008
|
+
font-size: 11.5px;
|
|
2009
|
+
}
|
|
2010
|
+
.agg-included-meta > span + span::before {
|
|
2011
|
+
content: "·";
|
|
2012
|
+
padding-right: 6px;
|
|
2013
|
+
color: var(--admin-line-strong);
|
|
2014
|
+
}
|
|
2015
|
+
.agg-included-meta .sha-token {
|
|
2016
|
+
font-size: 11px;
|
|
2017
|
+
padding: 1px 6px;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
/* Published-versions "what shipped": the release summary lead + openable
|
|
2021
|
+
per-change list, inline in the Version cell. Reuses the .agg-included-*
|
|
2022
|
+
item markup; only the container chrome is retuned for a table cell. */
|
|
2023
|
+
.ver-summary {
|
|
2024
|
+
margin: 6px 0 0;
|
|
2025
|
+
color: var(--admin-muted);
|
|
2026
|
+
font-size: 12.5px;
|
|
2027
|
+
line-height: 1.5;
|
|
2028
|
+
max-width: 48ch;
|
|
2029
|
+
}
|
|
2030
|
+
.ver-changes {
|
|
2031
|
+
margin-top: 8px;
|
|
2032
|
+
border: 1px solid var(--admin-line);
|
|
2033
|
+
border-radius: var(--admin-radius-sm);
|
|
2034
|
+
background: var(--admin-surface-soft);
|
|
2035
|
+
}
|
|
2036
|
+
.ver-changes > summary {
|
|
2037
|
+
padding: 8px 10px;
|
|
2038
|
+
}
|
|
2039
|
+
.ver-changes .agg-included-list {
|
|
2040
|
+
padding: 0 10px 10px;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
/* Branch-retention "what this branch changes": the PR title + one-line
|
|
2044
|
+
description + scope meta, inline under the branch ref. */
|
|
2045
|
+
.branch-pr-title {
|
|
2046
|
+
margin: 6px 0 0;
|
|
2047
|
+
font-weight: 600;
|
|
2048
|
+
font-size: 13px;
|
|
2049
|
+
color: var(--admin-ink);
|
|
2050
|
+
max-width: 46ch;
|
|
2051
|
+
}
|
|
2052
|
+
.branch-pr-desc {
|
|
2053
|
+
margin: 3px 0 0;
|
|
2054
|
+
color: var(--admin-muted);
|
|
2055
|
+
font-size: 12.5px;
|
|
2056
|
+
line-height: 1.5;
|
|
2057
|
+
max-width: 46ch;
|
|
2058
|
+
}
|
|
2059
|
+
.branch-pr-meta {
|
|
2060
|
+
margin-top: 5px;
|
|
2061
|
+
}
|
|
2062
|
+
.branch-pr-ai {
|
|
2063
|
+
color: var(--admin-teal-dark);
|
|
2064
|
+
font-weight: 640;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
/* Scope-of-change: lines added/removed (green/red), + the top-level readout */
|
|
2068
|
+
.agg-lines {
|
|
2069
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
2070
|
+
font-variant-numeric: tabular-nums;
|
|
2071
|
+
white-space: nowrap;
|
|
2072
|
+
}
|
|
2073
|
+
.agg-add {
|
|
2074
|
+
color: var(--admin-green);
|
|
2075
|
+
}
|
|
2076
|
+
.agg-del {
|
|
2077
|
+
color: var(--admin-red);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
/* "View diff" disclosure for a trivial change (the diff IS its summary). */
|
|
2081
|
+
.agg-diff {
|
|
2082
|
+
margin-top: 2px;
|
|
2083
|
+
}
|
|
2084
|
+
.agg-diff > summary {
|
|
2085
|
+
cursor: pointer;
|
|
2086
|
+
font-size: 11.5px;
|
|
2087
|
+
font-weight: 640;
|
|
2088
|
+
color: var(--admin-teal-dark);
|
|
2089
|
+
width: max-content;
|
|
2090
|
+
}
|
|
2091
|
+
.agg-diff[open] > summary {
|
|
2092
|
+
margin-bottom: 6px;
|
|
2093
|
+
}
|
|
2094
|
+
.agg-diff-file {
|
|
2095
|
+
margin-top: 6px;
|
|
2096
|
+
}
|
|
2097
|
+
.agg-diff-path {
|
|
2098
|
+
font-size: 11px;
|
|
2099
|
+
color: var(--admin-muted);
|
|
2100
|
+
margin-bottom: 3px;
|
|
2101
|
+
}
|
|
2102
|
+
.agg-diff-patch {
|
|
2103
|
+
margin: 0;
|
|
2104
|
+
padding: 8px 10px;
|
|
2105
|
+
border: 1px solid var(--admin-line);
|
|
2106
|
+
border-radius: var(--admin-radius-sm);
|
|
2107
|
+
background: var(--admin-surface);
|
|
2108
|
+
font-size: 11.5px;
|
|
2109
|
+
line-height: 1.5;
|
|
2110
|
+
overflow-x: auto;
|
|
2111
|
+
white-space: pre;
|
|
2112
|
+
}
|
|
2113
|
+
.agg-diff-add {
|
|
2114
|
+
color: var(--admin-green);
|
|
2115
|
+
background: var(--admin-green-soft);
|
|
2116
|
+
}
|
|
2117
|
+
.agg-diff-del {
|
|
2118
|
+
color: var(--admin-red);
|
|
2119
|
+
background: var(--admin-red-soft);
|
|
2120
|
+
}
|
|
2121
|
+
.agg-diff-hunk {
|
|
2122
|
+
color: var(--admin-muted-2);
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
.agg-staged-head {
|
|
2126
|
+
display: flex;
|
|
2127
|
+
align-items: baseline;
|
|
2128
|
+
justify-content: space-between;
|
|
2129
|
+
gap: 12px;
|
|
2130
|
+
flex-wrap: wrap;
|
|
2131
|
+
}
|
|
2132
|
+
.agg-scope {
|
|
2133
|
+
display: inline-flex;
|
|
2134
|
+
align-items: center;
|
|
2135
|
+
gap: 8px;
|
|
2136
|
+
padding: 3px 10px;
|
|
2137
|
+
border: 1px solid var(--admin-line);
|
|
2138
|
+
border-radius: 999px;
|
|
2139
|
+
background: var(--admin-surface-soft);
|
|
2140
|
+
font-size: 11.5px;
|
|
2141
|
+
color: var(--admin-ink);
|
|
2142
|
+
}
|
|
2143
|
+
.agg-scope[hidden] {
|
|
2144
|
+
display: none;
|
|
2145
|
+
}
|
|
2146
|
+
.agg-scope > span + span::before {
|
|
2147
|
+
content: "·";
|
|
2148
|
+
padding-right: 8px;
|
|
2149
|
+
color: var(--admin-line-strong);
|
|
2150
|
+
}
|
|
2151
|
+
.agg-scope-label {
|
|
2152
|
+
font-size: 10px;
|
|
2153
|
+
font-weight: 760;
|
|
2154
|
+
letter-spacing: 0.08em;
|
|
2155
|
+
text-transform: uppercase;
|
|
2156
|
+
color: var(--admin-muted);
|
|
2157
|
+
}
|
|
2158
|
+
.agg-scope-partial {
|
|
2159
|
+
color: var(--admin-muted-2);
|
|
2160
|
+
font-style: italic;
|
|
2161
|
+
cursor: help;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
/* Publish action — stack the copy above a full-width, weighted CTA */
|
|
2165
|
+
[data-publish-aggregate] .agg-launch {
|
|
2166
|
+
display: grid;
|
|
2167
|
+
grid-template-columns: 1fr;
|
|
2168
|
+
gap: 12px;
|
|
2169
|
+
padding: 14px 0 2px;
|
|
2170
|
+
}
|
|
2171
|
+
/* Extra selector weight so the gradient/shadow beat the .btn.primary base. */
|
|
2172
|
+
[data-publish-aggregate] .btn.agg-publish-btn {
|
|
2173
|
+
width: 100%;
|
|
2174
|
+
justify-content: center;
|
|
2175
|
+
min-height: 50px;
|
|
2176
|
+
gap: 9px;
|
|
2177
|
+
font-size: 15px;
|
|
2178
|
+
border-radius: var(--admin-radius-sm);
|
|
2179
|
+
background: linear-gradient(178deg, var(--admin-teal), var(--admin-teal-dark));
|
|
2180
|
+
box-shadow: 0 8px 22px color-mix(in srgb, var(--admin-teal) 26%, transparent);
|
|
2181
|
+
}
|
|
2182
|
+
.agg-publish-btn svg {
|
|
2183
|
+
width: 18px;
|
|
2184
|
+
height: 18px;
|
|
2185
|
+
}
|
|
2186
|
+
[data-publish-aggregate] .btn.agg-publish-btn:not(:disabled):hover {
|
|
2187
|
+
filter: brightness(1.05);
|
|
2188
|
+
}
|
|
2189
|
+
[data-publish-aggregate] .btn.agg-publish-btn:disabled {
|
|
2190
|
+
background: var(--admin-surface-tint);
|
|
2191
|
+
border-color: var(--admin-line-strong);
|
|
2192
|
+
color: var(--admin-muted);
|
|
2193
|
+
box-shadow: none;
|
|
2194
|
+
cursor: not-allowed;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
@media (max-width: 760px) {
|
|
2198
|
+
.agg-chain {
|
|
2199
|
+
grid-template-columns: 1fr;
|
|
2200
|
+
gap: 4px;
|
|
2201
|
+
}
|
|
2202
|
+
.agg-link {
|
|
2203
|
+
flex-direction: row;
|
|
2204
|
+
gap: 10px;
|
|
2205
|
+
padding: 4px 12px;
|
|
2206
|
+
}
|
|
2207
|
+
.agg-link .agg-arrow svg {
|
|
2208
|
+
transform: rotate(90deg);
|
|
2209
|
+
}
|
|
2210
|
+
.agg-stop {
|
|
2211
|
+
padding: 8px 2px;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
|
|
1899
2215
|
/* dg7: versions + diff */
|
|
1900
2216
|
.versions-table {
|
|
1901
2217
|
min-width: 780px;
|
|
1902
2218
|
}
|
|
1903
2219
|
|
|
1904
|
-
|
|
2220
|
+
/* rux6: unified review pane */
|
|
2221
|
+
.publish-review-cell {
|
|
1905
2222
|
padding: 12px 14px;
|
|
1906
2223
|
background: var(--admin-surface-soft);
|
|
1907
2224
|
}
|
|
1908
2225
|
|
|
2226
|
+
.review-pane {
|
|
2227
|
+
display: grid;
|
|
2228
|
+
grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
|
|
2229
|
+
gap: 16px;
|
|
2230
|
+
align-items: start;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
@media (max-width: 900px) {
|
|
2234
|
+
.review-pane {
|
|
2235
|
+
grid-template-columns: 1fr;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
.review-preview {
|
|
2240
|
+
display: grid;
|
|
2241
|
+
gap: 8px;
|
|
2242
|
+
min-width: 0;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
.review-pane-head {
|
|
2246
|
+
display: flex;
|
|
2247
|
+
align-items: center;
|
|
2248
|
+
justify-content: space-between;
|
|
2249
|
+
gap: 10px;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
.review-frame {
|
|
2253
|
+
width: 100%;
|
|
2254
|
+
height: 420px;
|
|
2255
|
+
border: 1px solid var(--admin-line-strong);
|
|
2256
|
+
border-radius: var(--admin-radius-sm);
|
|
2257
|
+
background: var(--admin-surface);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
.review-detail {
|
|
2261
|
+
display: grid;
|
|
2262
|
+
gap: 14px;
|
|
2263
|
+
min-width: 0;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
.review-section {
|
|
2267
|
+
display: grid;
|
|
2268
|
+
gap: 6px;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
.review-chips {
|
|
2272
|
+
display: flex;
|
|
2273
|
+
flex-wrap: wrap;
|
|
2274
|
+
gap: 6px;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
.review-actions {
|
|
2278
|
+
display: flex;
|
|
2279
|
+
flex-wrap: wrap;
|
|
2280
|
+
gap: 8px;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
1909
2283
|
.publish-diff-head {
|
|
1910
2284
|
display: flex;
|
|
1911
2285
|
flex-wrap: wrap;
|