@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.
Files changed (193) hide show
  1. package/apps/storefront/astro.config.mjs +17 -1
  2. package/apps/storefront/integrations/materialize-guard.mjs +56 -0
  3. package/apps/storefront/migrations-apps/0001_woozy_lyja.sql +22 -0
  4. package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
  5. package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
  6. package/apps/storefront/migrations-apps/meta/0001_snapshot.json +990 -0
  7. package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
  8. package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
  9. package/apps/storefront/migrations-apps/meta/_journal.json +21 -0
  10. package/apps/storefront/package.json +0 -1
  11. package/apps/storefront/public/js/dashboard-team.js +38 -2
  12. package/apps/storefront/src/components/Seo.astro +65 -1
  13. package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
  14. package/apps/storefront/src/components/admin/AdminBlogTab.astro +1432 -0
  15. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
  16. package/apps/storefront/src/components/admin/AdminPublishTab.astro +1756 -1382
  17. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
  18. package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
  19. package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
  20. package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
  21. package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
  22. package/apps/storefront/src/layouts/Layout.astro +48 -3
  23. package/apps/storefront/src/lib/activity/alerts.ts +426 -0
  24. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
  25. package/apps/storefront/src/lib/activity/deployVersion.ts +120 -0
  26. package/apps/storefront/src/lib/activity/ingest.ts +188 -0
  27. package/apps/storefront/src/lib/activity/ingestAuth.ts +105 -0
  28. package/apps/storefront/src/lib/activity/killSwitch.ts +80 -0
  29. package/apps/storefront/src/lib/activity/query.ts +403 -0
  30. package/apps/storefront/src/lib/activity/recordActivity.ts +101 -0
  31. package/apps/storefront/src/lib/activity/store.ts +120 -0
  32. package/apps/storefront/src/lib/activity/uiActor.ts +150 -0
  33. package/apps/storefront/src/lib/activity/workerCommit.ts +70 -0
  34. package/apps/storefront/src/lib/admin/adminShell.ts +290 -0
  35. package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
  36. package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
  37. package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
  38. package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
  39. package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
  40. package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
  41. package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
  42. package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
  43. package/apps/storefront/src/lib/apps/adminService.ts +4 -4
  44. package/apps/storefront/src/lib/apps/adminSession.ts +53 -44
  45. package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
  46. package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
  47. package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
  48. package/apps/storefront/src/lib/apps/credentials.ts +2 -2
  49. package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
  50. package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
  51. package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
  52. package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
  53. package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
  54. package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
  55. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +1 -1
  56. package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
  57. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +6 -6
  58. package/apps/storefront/src/lib/apps/registryService.ts +6 -6
  59. package/apps/storefront/src/lib/apps/scopes.ts +2 -2
  60. package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
  61. package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
  62. package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
  63. package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
  64. package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
  65. package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
  66. package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
  67. package/apps/storefront/src/lib/auth/adminEntry.ts +9 -9
  68. package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
  69. package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
  70. package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +6 -4
  71. package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
  72. package/apps/storefront/src/lib/auth/route.ts +44 -4
  73. package/apps/storefront/src/lib/auth/session.ts +7 -8
  74. package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
  75. package/apps/storefront/src/lib/basePath.ts +9 -2
  76. package/apps/storefront/src/lib/blog/access.ts +29 -0
  77. package/apps/storefront/src/lib/blog/collection.ts +313 -0
  78. package/apps/storefront/src/lib/blog/markdown.ts +174 -0
  79. package/apps/storefront/src/lib/blog/provider.ts +5 -0
  80. package/apps/storefront/src/lib/blog/reactions.ts +87 -0
  81. package/apps/storefront/src/lib/blog/rss.ts +92 -0
  82. package/apps/storefront/src/lib/blog/teaser.ts +83 -0
  83. package/apps/storefront/src/lib/blog/types.ts +34 -0
  84. package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
  85. package/apps/storefront/src/lib/d1/catalog.ts +2 -2
  86. package/apps/storefront/src/lib/d1/schema-apps.ts +147 -14
  87. package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
  88. package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
  89. package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
  90. package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
  91. package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
  92. package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
  93. package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
  94. package/apps/storefront/src/lib/edgeCache.ts +17 -0
  95. package/apps/storefront/src/lib/env.ts +51 -0
  96. package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
  97. package/apps/storefront/src/lib/i18n.ts +25 -0
  98. package/apps/storefront/src/lib/jsonld.ts +118 -0
  99. package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
  100. package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
  101. package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
  102. package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
  103. package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
  104. package/apps/storefront/src/lib/newsletter/send.ts +265 -0
  105. package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
  106. package/apps/storefront/src/lib/publish/adminPublishTab.ts +3519 -0
  107. package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
  108. package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
  109. package/apps/storefront/src/lib/publish/domainState.ts +22 -13
  110. package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
  111. package/apps/storefront/src/lib/publish/shipWorkspace.ts +100 -42
  112. package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
  113. package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
  114. package/apps/storefront/src/lib/search/index.ts +2 -2
  115. package/apps/storefront/src/lib/seo/alternates.ts +42 -0
  116. package/apps/storefront/src/lib/seo/meta.ts +65 -0
  117. package/apps/storefront/src/lib/social/golive.ts +212 -0
  118. package/apps/storefront/src/lib/social/notify.ts +32 -0
  119. package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
  120. package/apps/storefront/src/lib/the-build/provider.ts +27 -0
  121. package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
  122. package/apps/storefront/src/lib/the-build/toc.ts +95 -0
  123. package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
  124. package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
  125. package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
  126. package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
  127. package/apps/storefront/src/middleware/index.ts +130 -2
  128. package/apps/storefront/src/pages/admin/ops-timeline.astro +420 -0
  129. package/apps/storefront/src/pages/admin.astro +52 -1206
  130. package/apps/storefront/src/pages/api/activity.ts +137 -0
  131. package/apps/storefront/src/pages/api/admin/activity-alerts.ts +73 -0
  132. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +27 -1
  133. package/apps/storefront/src/pages/api/apps/admin/install.ts +26 -1
  134. package/apps/storefront/src/pages/api/apps/admin/resume.ts +24 -1
  135. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +24 -1
  136. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +25 -1
  137. package/apps/storefront/src/pages/api/apps/admin/update.ts +24 -1
  138. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +11 -0
  139. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +11 -0
  140. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +20 -0
  141. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +12 -0
  142. package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
  143. package/apps/storefront/src/pages/api/cache-purge.ts +11 -0
  144. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -0
  145. package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
  146. package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
  147. package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
  148. package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
  149. package/apps/storefront/src/pages/api/rum/vitals.ts +21 -2
  150. package/apps/storefront/src/pages/auth/login.astro +5 -4
  151. package/apps/storefront/src/pages/blog/[slug].astro +96 -16
  152. package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
  153. package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
  154. package/apps/storefront/src/pages/blog/index.astro +2 -0
  155. package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
  156. package/apps/storefront/src/pages/cockpit.astro +74 -12
  157. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
  158. package/apps/storefront/src/pages/search.astro +1 -1
  159. package/apps/storefront/src/pages/sitemap.xml.ts +20 -0
  160. package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
  161. package/apps/storefront/src/pages/the-build/index.astro +146 -0
  162. package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
  163. package/apps/storefront/src/styles/admin.css +1080 -0
  164. package/apps/storefront/tsconfig.json +5 -0
  165. package/package.json +1 -1
  166. package/packages/public-runtime/package.json +1 -0
  167. package/packages/public-runtime/src/activity/README.md +146 -0
  168. package/packages/public-runtime/src/activity/catalog.ts +553 -0
  169. package/packages/public-runtime/src/activity/event.ts +168 -0
  170. package/packages/public-runtime/src/activity/index.ts +16 -0
  171. package/packages/public-runtime/src/activity/redaction.ts +263 -0
  172. package/packages/public-runtime/src/candidate-index.ts +13 -0
  173. package/packages/public-runtime/src/catalog-d1.ts +1 -1
  174. package/packages/public-runtime/src/customization-preview.ts +25 -2
  175. package/packages/public-runtime/src/customization-runtime.ts +125 -3
  176. package/packages/public-runtime/src/customization-versioning.ts +10 -0
  177. package/packages/public-runtime/src/index.ts +7 -0
  178. package/packages/public-runtime/src/static-bundle.ts +357 -0
  179. package/packages/public-runtime/src/tenant.ts +36 -8
  180. package/scripts/build/copy-tenant-assets.mjs +14 -4
  181. package/scripts/dev/ai-edit.mjs +1 -1
  182. package/scripts/dev/checkout-watch.mjs +2 -2
  183. package/scripts/dev/file-browser.mjs +2 -4
  184. package/scripts/dev/file-writer.mjs +1 -1
  185. package/scripts/dev/git-status.mjs +2 -2
  186. package/scripts/dev/locate-handler.mjs +3 -3
  187. package/scripts/dev/port-check.mjs +3 -3
  188. package/scripts/dev/publish.mjs +5 -9
  189. package/scripts/dev/shot.mjs +1 -1
  190. package/scripts/dev/unified-diff.mjs +8 -3
  191. package/scripts/tot-dev.mjs +6 -9
  192. package/apps/storefront/src/lib/webhooks/signing.ts +0 -29
  193. package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +0 -146
@@ -37,11 +37,13 @@ import OrdersWorkspace from "../components/admin/orders/OrdersWorkspace.astro";
37
37
  import AdminProductsTab from "../components/admin/AdminProductsTab.astro";
38
38
  import AdminSubscriptionsTab from "../components/admin/AdminSubscriptionsTab.astro";
39
39
  import AdminReportingTab from "../components/admin/AdminReportingTab.astro";
40
+ import AdminBlogTab from "../components/admin/AdminBlogTab.astro";
40
41
  import AdminPublishTab from "../components/admin/AdminPublishTab.astro";
41
42
  import AdminSettingsTab from "../components/admin/AdminSettingsTab.astro";
42
43
  import { readViewerSession } from "@/lib/auth/route";
43
44
  import { resolveAdminTenantMode } from "@/lib/adminTenantEnvelope";
44
45
  import { resolveAdminEntry } from "@/lib/auth/adminEntry";
46
+ import "@/styles/admin.css";
45
47
 
46
48
  const { tenant, basePath, cspNonce } = Astro.locals;
47
49
  const adminPath = `${basePath || ""}/admin`;
@@ -100,10 +102,12 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
100
102
  <title>{tenantLabel} Admin</title>
101
103
  <script is:inline nonce={cspNonce}>
102
104
  (() => {
103
- const adminTabs = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "publish", "settings"];
105
+ const adminTabs = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "blog", "publish", "settings"];
104
106
  const canViewInternalGuide = document.documentElement.dataset.internalAdminGuide === "true";
105
107
  const defaultTab = canViewInternalGuide ? "orders" : "settings";
106
- const hash = window.location.hash.slice(1);
108
+ // A deep link may carry a query suffix in the hash (e.g. #publish?pr=42);
109
+ // the tab id is only the part before "?".
110
+ const hash = window.location.hash.slice(1).split("?")[0];
107
111
  const activeTab = adminTabs.includes(hash) ? hash : defaultTab;
108
112
  const mode = canViewInternalGuide && hash && !adminTabs.includes(hash) ? "guide" : "work-area";
109
113
 
@@ -112,983 +116,13 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
112
116
  document.documentElement.dataset.adminMode = mode;
113
117
  })();
114
118
  </script>
115
- <style is:global nonce={cspNonce}>
116
- :root {
117
- color-scheme: light;
118
- --admin-bg: #f5f7f6;
119
- --admin-surface: #ffffff;
120
- --admin-surface-soft: #f9fbfa;
121
- --admin-surface-tint: #eef6f4;
122
- --admin-ink: #16211f;
123
- --admin-muted: #64716d;
124
- --admin-muted-2: #8c9894;
125
- --admin-line: #dce5e2;
126
- --admin-line-strong: #bdcbc7;
127
- --admin-teal: #007f79;
128
- --admin-teal-dark: #005d58;
129
- --admin-teal-soft: #dff4f1;
130
- --admin-green: #137a55;
131
- --admin-green-soft: #e3f5eb;
132
- --admin-blue: #2458c7;
133
- --admin-blue-soft: #e7efff;
134
- --admin-amber: #a45d00;
135
- --admin-amber-soft: #fff0d5;
136
- --admin-red: #b42318;
137
- --admin-red-soft: #fde7e4;
138
- --admin-violet: #6941c6;
139
- --admin-violet-soft: #efe9ff;
140
- --admin-shadow: 0 18px 50px rgba(23, 33, 31, 0.08);
141
- --admin-radius: 8px;
142
- --admin-radius-sm: 6px;
143
- --admin-scroll-offset: 104px;
144
- --admin-font:
145
- Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
146
- "Segoe UI", sans-serif;
147
- }
148
-
149
- * {
150
- box-sizing: border-box;
151
- }
152
-
153
- html {
154
- max-width: 100%;
155
- height: 100%;
156
- overflow: hidden;
157
- }
158
-
159
- body {
160
- margin: 0;
161
- height: 100%;
162
- min-height: 100vh;
163
- background: var(--admin-bg);
164
- color: var(--admin-ink);
165
- font: 14px/1.5 var(--admin-font);
166
- overflow: hidden;
167
- }
168
-
169
- button,
170
- input,
171
- select {
172
- font: inherit;
173
- }
174
-
175
- button {
176
- cursor: pointer;
177
- }
178
-
179
- svg {
180
- width: 18px;
181
- height: 18px;
182
- stroke: currentColor;
183
- stroke-width: 1.9;
184
- stroke-linecap: round;
185
- stroke-linejoin: round;
186
- fill: none;
187
- flex: 0 0 auto;
188
- }
189
-
190
- .shell {
191
- display: grid;
192
- grid-template-columns: 248px minmax(0, 1fr);
193
- height: 100vh;
194
- min-height: 0;
195
- overflow: hidden;
196
- }
197
-
198
- .sidebar {
199
- position: sticky;
200
- top: 0;
201
- height: 100vh;
202
- min-height: 0;
203
- display: flex;
204
- flex-direction: column;
205
- gap: 22px;
206
- padding: 20px 16px;
207
- overflow-y: auto;
208
- overscroll-behavior: contain;
209
- background: #101b18;
210
- color: #eef7f5;
211
- }
212
-
213
- .brand {
214
- display: flex;
215
- align-items: center;
216
- gap: 12px;
217
- min-height: 42px;
218
- }
219
-
220
- .brand-mark {
221
- width: 36px;
222
- height: 36px;
223
- display: grid;
224
- place-items: center;
225
- border-radius: var(--admin-radius);
226
- background: #00a991;
227
- color: #06231e;
228
- font-weight: 800;
229
- }
230
-
231
- .brand b,
232
- .store strong {
233
- display: block;
234
- font-size: 14px;
235
- line-height: 1.1;
236
- }
237
-
238
- .brand span,
239
- .store span {
240
- display: block;
241
- margin-top: 3px;
242
- color: rgba(238, 247, 245, 0.7);
243
- font-size: 12px;
244
- }
245
-
246
- .store {
247
- display: grid;
248
- gap: 4px;
249
- padding: 12px;
250
- border: 1px solid rgba(255, 255, 255, 0.12);
251
- border-radius: var(--admin-radius);
252
- background: rgba(255, 255, 255, 0.06);
253
- }
254
-
255
- .tenant-picker {
256
- display: grid;
257
- gap: 4px;
258
- padding: 10px 12px;
259
- border: 1px solid rgba(0, 169, 145, 0.4);
260
- border-radius: var(--admin-radius);
261
- background: rgba(0, 169, 145, 0.1);
262
- }
263
-
264
- .tenant-picker label {
265
- color: rgba(238, 247, 245, 0.7);
266
- font-size: 11px;
267
- font-weight: 760;
268
- text-transform: uppercase;
269
- }
270
-
271
- .tenant-picker select {
272
- min-height: 34px;
273
- border: 1px solid rgba(255, 255, 255, 0.2);
274
- border-radius: var(--admin-radius-sm);
275
- background: #101b18;
276
- color: #eef7f5;
277
- }
278
-
279
- .tenant-picker-denied {
280
- padding: 10px 12px;
281
- border: 1px solid var(--admin-amber);
282
- border-radius: var(--admin-radius);
283
- background: rgba(164, 93, 0, 0.16);
284
- color: #eef7f5;
285
- font-size: 12px;
286
- }
287
-
288
- .tenant-picker-denied code {
289
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
290
- }
291
-
292
- .nav {
293
- display: grid;
294
- gap: 4px;
295
- }
296
-
297
- .nav a {
298
- display: flex;
299
- align-items: center;
300
- gap: 10px;
301
- min-height: 40px;
302
- padding: 0 10px;
303
- border-radius: var(--admin-radius-sm);
304
- color: rgba(238, 247, 245, 0.74);
305
- text-decoration: none;
306
- }
307
-
308
- .nav a.active {
309
- background: rgba(0, 169, 145, 0.18);
310
- color: #ffffff;
311
- }
312
-
313
- .nav-group {
314
- display: grid;
315
- gap: 3px;
316
- }
317
-
318
- .nav-subnav {
319
- display: grid;
320
- gap: 2px;
321
- margin: 0 0 3px 28px;
322
- padding-left: 10px;
323
- border-left: 1px solid rgba(255, 255, 255, 0.14);
324
- }
325
-
326
- .nav-subnav a {
327
- min-height: 32px;
328
- padding: 0 8px;
329
- gap: 8px;
330
- font-size: 12px;
331
- }
332
-
333
- .nav-subnav svg {
334
- width: 16px;
335
- height: 16px;
336
- }
337
-
338
- .health {
339
- margin-top: auto;
340
- display: grid;
341
- gap: 9px;
342
- padding: 12px;
343
- border: 1px solid rgba(255, 255, 255, 0.12);
344
- border-radius: var(--admin-radius);
345
- background: rgba(255, 255, 255, 0.05);
346
- }
347
-
348
- .health-row {
349
- display: flex;
350
- align-items: center;
351
- justify-content: space-between;
352
- gap: 10px;
353
- font-size: 12px;
354
- color: rgba(238, 247, 245, 0.72);
355
- }
356
-
357
- .health-row strong {
358
- color: #ffffff;
359
- }
360
-
361
- .main {
362
- min-width: 0;
363
- min-height: 0;
364
- height: 100vh;
365
- overflow-x: hidden;
366
- overflow-y: auto;
367
- overscroll-behavior: contain;
368
- }
369
-
370
- .content,
371
- .band,
372
- .panel,
373
- .panel-body,
374
- .table-wrap {
375
- min-width: 0;
376
- max-width: 100%;
377
- }
378
-
379
- .topbar {
380
- position: sticky;
381
- top: 0;
382
- z-index: 30;
383
- display: flex;
384
- align-items: center;
385
- justify-content: space-between;
386
- gap: 18px;
387
- min-height: 68px;
388
- padding: 14px 24px;
389
- border-bottom: 1px solid var(--admin-line);
390
- background: rgba(245, 247, 246, 0.94);
391
- backdrop-filter: blur(16px);
392
- }
393
-
394
- .page-title h1 {
395
- margin: 0;
396
- font-size: 22px;
397
- font-weight: 760;
398
- }
399
-
400
- .page-title p {
401
- margin: 2px 0 0;
402
- color: var(--admin-muted);
403
- font-size: 13px;
404
- }
405
-
406
- .page-title,
407
- .page-title p,
408
- .note,
409
- code {
410
- min-width: 0;
411
- overflow-wrap: anywhere;
412
- }
413
-
414
- .actions {
415
- display: flex;
416
- flex-wrap: wrap;
417
- gap: 8px;
418
- }
419
-
420
- .btn,
421
- .icon-btn {
422
- min-height: 38px;
423
- display: inline-flex;
424
- align-items: center;
425
- justify-content: center;
426
- gap: 8px;
427
- border: 1px solid var(--admin-line-strong);
428
- border-radius: var(--admin-radius-sm);
429
- background: var(--admin-surface);
430
- color: var(--admin-ink);
431
- text-decoration: none;
432
- white-space: nowrap;
433
- }
434
-
435
- .btn {
436
- padding: 0 13px;
437
- font-weight: 680;
438
- max-width: 100%;
439
- }
440
-
441
- .icon-btn {
442
- width: 38px;
443
- padding: 0;
444
- }
445
-
446
- .btn.primary {
447
- border-color: var(--admin-teal-dark);
448
- background: var(--admin-teal);
449
- color: #ffffff;
450
- }
451
-
452
- .floating-ask {
453
- position: fixed;
454
- right: 24px;
455
- bottom: 24px;
456
- z-index: 80;
457
- min-height: 58px;
458
- max-width: min(280px, calc(100vw - 32px));
459
- display: inline-flex;
460
- align-items: center;
461
- gap: 10px;
462
- padding: 9px 15px 9px 10px;
463
- border: 1px solid rgba(0, 93, 88, 0.24);
464
- border-radius: 999px;
465
- background: var(--admin-teal);
466
- color: #ffffff;
467
- box-shadow: 0 16px 38px rgba(22, 33, 31, 0.22);
468
- }
469
-
470
- .floating-ask-icon {
471
- width: 40px;
472
- height: 40px;
473
- display: grid;
474
- place-items: center;
475
- border-radius: 50%;
476
- background: rgba(255, 255, 255, 0.16);
477
- }
478
-
479
- .floating-ask-label {
480
- display: grid;
481
- gap: 1px;
482
- min-width: 0;
483
- text-align: left;
484
- }
485
-
486
- .floating-ask-label strong {
487
- font-size: 14px;
488
- line-height: 1.1;
489
- }
490
-
491
- .floating-ask-label small {
492
- max-width: 160px;
493
- overflow: hidden;
494
- color: rgba(255, 255, 255, 0.78);
495
- font-size: 11px;
496
- font-weight: 680;
497
- line-height: 1.2;
498
- text-overflow: ellipsis;
499
- white-space: nowrap;
500
- }
501
-
502
- .content {
503
- display: grid;
504
- grid-template-columns: minmax(0, 1fr);
505
- gap: 18px;
506
- padding: 20px 24px 40px;
507
- }
508
-
509
- .band {
510
- display: grid;
511
- grid-template-columns: minmax(0, 1fr);
512
- gap: 14px;
513
- }
514
-
515
- [data-admin-panel],
516
- [data-admin-guide-section] {
517
- scroll-margin-top: var(--admin-scroll-offset);
518
- }
519
-
520
- html.admin-tabs-ready [data-admin-panel] {
521
- display: none;
522
- }
523
-
524
- html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="orders"] #orders,
525
- html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="products"] #products,
526
- html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="subscriptions"] #subscriptions,
527
- html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="customers"] #customers,
528
- html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="fulfillment"] #fulfillment,
529
- html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="reporting"] #reporting,
530
- html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="ai-workflows"] #ai-workflows,
531
- html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="publish"] #publish,
532
- html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="settings"] #settings {
533
- display: grid;
534
- }
535
-
536
- html.admin-tabs-ready[data-admin-mode="work-area"] [data-admin-guide-section],
537
- html.admin-tabs-ready[data-admin-mode="work-area"] [data-admin-tab-help] {
538
- display: none;
539
- }
540
-
541
- html[data-internal-admin-guide="false"] [data-admin-internal-only] {
542
- display: none !important;
543
- }
544
-
545
- .section-title {
546
- display: flex;
547
- align-items: center;
548
- justify-content: space-between;
549
- gap: 12px;
550
- }
551
-
552
- .section-title h2 {
553
- margin: 0;
554
- font-size: 16px;
555
- }
556
-
557
- .section-title p {
558
- margin: 4px 0 0;
559
- color: var(--admin-muted);
560
- font-size: 13px;
561
- }
562
-
563
- .grid {
564
- display: grid;
565
- grid-template-columns: minmax(0, 1fr);
566
- gap: 12px;
567
- }
568
-
569
- .grid.cols-2 {
570
- grid-template-columns: repeat(2, minmax(0, 1fr));
571
- }
572
-
573
- .grid.cols-3 {
574
- grid-template-columns: repeat(3, minmax(0, 1fr));
575
- }
576
-
577
- .grid.cols-4 {
578
- grid-template-columns: repeat(4, minmax(0, 1fr));
579
- }
580
-
581
- .panel {
582
- border: 1px solid var(--admin-line);
583
- border-radius: var(--admin-radius);
584
- background: var(--admin-surface);
585
- }
586
-
587
- .panel-head {
588
- display: flex;
589
- align-items: center;
590
- justify-content: space-between;
591
- gap: 12px;
592
- min-height: 50px;
593
- padding: 13px 14px;
594
- border-bottom: 1px solid var(--admin-line);
595
- }
596
-
597
- .panel-head h3 {
598
- margin: 0;
599
- display: flex;
600
- align-items: center;
601
- gap: 8px;
602
- font-size: 15px;
603
- }
604
-
605
- .panel-body {
606
- display: grid;
607
- grid-template-columns: minmax(0, 1fr);
608
- gap: 12px;
609
- padding: 14px;
610
- }
611
-
612
- .metric {
613
- min-height: 84px;
614
- padding: 14px;
615
- border: 1px solid var(--admin-line);
616
- border-radius: var(--admin-radius);
617
- background: var(--admin-surface);
618
- }
619
-
620
- .metric span,
621
- .field label {
622
- display: block;
623
- color: var(--admin-muted);
624
- font-size: 11px;
625
- font-weight: 760;
626
- text-transform: uppercase;
627
- }
628
-
629
- .metric strong {
630
- display: block;
631
- margin-top: 9px;
632
- font-size: 24px;
633
- }
634
-
635
- .metric small {
636
- display: block;
637
- margin-top: 3px;
638
- color: var(--admin-muted);
639
- font-size: 12px;
640
- }
641
-
642
- .chip {
643
- display: inline-flex;
644
- align-items: center;
645
- gap: 5px;
646
- min-height: 23px;
647
- padding: 0 8px;
648
- border-radius: 999px;
649
- background: var(--admin-surface-tint);
650
- color: #3f4d49;
651
- font-size: 12px;
652
- font-weight: 740;
653
- white-space: nowrap;
654
- }
655
-
656
- .chip.green {
657
- background: var(--admin-green-soft);
658
- color: var(--admin-green);
659
- }
660
-
661
- .chip.teal {
662
- background: var(--admin-teal-soft);
663
- /* Slightly darker than --admin-teal (#007f79): the shared token only
664
- reaches ~4.26:1 against --admin-teal-soft, just under WCAG AA's
665
- 4.5:1 for this chip's 12px text (hardening-qa a11y pass). Scoped to
666
- this rule only — --admin-teal itself stays unchanged since it is
667
- also used as a background (e.g. .btn.primary) where the pairing
668
- and contrast requirement are different. */
669
- color: #007973;
670
- }
671
-
672
- .chip.blue {
673
- background: var(--admin-blue-soft);
674
- color: var(--admin-blue);
675
- }
676
-
677
- .chip.amber {
678
- background: var(--admin-amber-soft);
679
- color: var(--admin-amber);
680
- }
681
-
682
- .chip.red {
683
- background: var(--admin-red-soft);
684
- color: var(--admin-red);
685
- }
686
-
687
- .chip.violet {
688
- background: var(--admin-violet-soft);
689
- color: var(--admin-violet);
690
- }
691
-
692
- .swatches {
693
- display: grid;
694
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
695
- gap: 10px;
696
- }
697
-
698
- .swatch {
699
- overflow: hidden;
700
- border: 1px solid var(--admin-line);
701
- border-radius: var(--admin-radius-sm);
702
- background: var(--admin-surface);
703
- }
704
-
705
- .swatch-color {
706
- min-height: 58px;
707
- }
708
-
709
- .swatch-meta {
710
- padding: 10px;
711
- }
712
-
713
- .swatch-meta b {
714
- display: block;
715
- font-size: 13px;
716
- }
717
-
718
- .swatch-meta code,
719
- .note code {
720
- color: var(--admin-muted);
721
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
722
- font-size: 12px;
723
- }
724
-
725
- .field {
726
- min-width: 0;
727
- padding: 11px;
728
- border: 1px solid var(--admin-line);
729
- border-radius: var(--admin-radius-sm);
730
- background: #fcfdfc;
731
- }
732
-
733
- .field strong {
734
- display: block;
735
- margin-top: 6px;
736
- font-size: 13px;
737
- }
738
-
739
- .field p {
740
- margin: 5px 0 0;
741
- color: var(--admin-muted);
742
- font-size: 12px;
743
- }
744
-
745
- .ship-gate {
746
- display: flex;
747
- align-items: center;
748
- justify-content: space-between;
749
- gap: 14px;
750
- padding: 14px;
751
- border: 1px solid #a6d8c0;
752
- border-radius: var(--admin-radius);
753
- background: #eefaf3;
754
- }
755
-
756
- .ship-gate.amber {
757
- border-color: #f1c883;
758
- background: #fff6e7;
759
- }
760
-
761
- .ship-gate-main,
762
- .risk-alert-main {
763
- display: flex;
764
- align-items: center;
765
- gap: 12px;
766
- min-width: 0;
767
- }
768
-
769
- .seal {
770
- width: 42px;
771
- height: 42px;
772
- display: grid;
773
- place-items: center;
774
- border-radius: 50%;
775
- background: var(--admin-green);
776
- color: #ffffff;
777
- }
778
-
779
- .ship-gate.amber .seal {
780
- background: var(--admin-amber);
781
- }
782
-
783
- .ship-gate b,
784
- .risk-alert b {
785
- display: block;
786
- font-size: 15px;
787
- }
788
-
789
- .ship-gate span,
790
- .risk-alert span {
791
- display: block;
792
- margin-top: 3px;
793
- color: var(--admin-muted);
794
- font-size: 12px;
795
- }
796
-
797
- .risk-alert {
798
- display: flex;
799
- align-items: center;
800
- justify-content: space-between;
801
- gap: 14px;
802
- padding: 12px 14px;
803
- border: 1px solid #f1c883;
804
- border-radius: var(--admin-radius);
805
- background: #fff6e7;
806
- }
807
-
808
- .risk-alert svg {
809
- color: var(--admin-amber);
810
- }
811
-
812
- .order-assist-grid {
813
- display: grid;
814
- grid-template-columns: repeat(3, minmax(0, 1fr));
815
- gap: 10px;
816
- }
817
-
818
- .order-assist-card {
819
- display: grid;
820
- gap: 7px;
821
- min-width: 0;
822
- padding: 12px;
823
- border: 1px solid var(--admin-line);
824
- border-radius: var(--admin-radius-sm);
825
- background: var(--admin-surface);
826
- }
827
-
828
- .order-assist-card strong {
829
- font-size: 14px;
830
- }
831
-
832
- .order-assist-card p {
833
- margin: 0;
834
- color: var(--admin-muted);
835
- font-size: 12px;
836
- }
837
-
838
- .tabs {
839
- display: flex;
840
- gap: 6px;
841
- max-width: 100%;
842
- overflow-x: auto;
843
- border-bottom: 1px solid var(--admin-line);
844
- }
845
-
846
- .mobile-tabbar {
847
- display: none;
848
- }
849
-
850
- .tab {
851
- flex: 0 0 auto;
852
- min-height: 38px;
853
- display: inline-flex;
854
- align-items: center;
855
- padding: 0 12px;
856
- border: 0;
857
- border-bottom: 2px solid transparent;
858
- background: transparent;
859
- color: var(--admin-muted);
860
- font-weight: 760;
861
- text-decoration: none;
862
- }
863
-
864
- .tab.active {
865
- border-color: var(--admin-teal);
866
- color: var(--admin-ink);
867
- }
868
-
869
- .table-wrap {
870
- overflow-x: auto;
871
- border: 1px solid var(--admin-line);
872
- border-radius: var(--admin-radius);
873
- background: var(--admin-surface);
874
- }
875
-
876
- table {
877
- width: 100%;
878
- min-width: 860px;
879
- border-collapse: collapse;
880
- font-size: 13px;
881
- }
882
-
883
- th {
884
- height: 38px;
885
- padding: 0 12px;
886
- border-bottom: 1px solid var(--admin-line);
887
- background: var(--admin-surface-soft);
888
- color: var(--admin-muted);
889
- font-size: 11px;
890
- font-weight: 760;
891
- text-align: left;
892
- text-transform: uppercase;
893
- white-space: nowrap;
894
- }
895
-
896
- td {
897
- height: 46px;
898
- padding: 7px 12px;
899
- border-bottom: 1px solid var(--admin-line);
900
- vertical-align: middle;
901
- }
902
-
903
- tr:last-child td {
904
- border-bottom: 0;
905
- }
906
-
907
- .subtle {
908
- display: block;
909
- margin-top: 2px;
910
- color: var(--admin-muted);
911
- font-size: 12px;
912
- }
913
-
914
- .money,
915
- .action {
916
- text-align: right;
917
- }
918
-
919
- .check-row {
920
- display: grid;
921
- grid-template-columns: minmax(120px, 0.36fr) minmax(0, 1fr) auto;
922
- gap: 10px;
923
- align-items: center;
924
- padding: 10px 0;
925
- border-bottom: 1px solid var(--admin-line);
926
- }
927
-
928
- .check-row:last-child {
929
- border-bottom: 0;
930
- }
931
-
932
- .check-row.blocking {
933
- margin: 0 -8px;
934
- padding: 10px 8px;
935
- border: 1px solid #f1c883;
936
- border-radius: var(--admin-radius-sm);
937
- background: #fff8ec;
938
- }
939
-
940
- .check-row b {
941
- font-size: 13px;
942
- }
943
-
944
- .check-row p {
945
- margin: 2px 0 0;
946
- color: var(--admin-muted);
947
- font-size: 12px;
948
- }
949
-
950
- .type-sample h3 {
951
- margin: 0 0 8px;
952
- font-size: 25px;
953
- line-height: 1.12;
954
- }
955
-
956
- .type-sample p {
957
- margin: 0;
958
- color: var(--admin-muted);
959
- }
960
-
961
- .note {
962
- margin: 0;
963
- color: var(--admin-muted);
964
- font-size: 13px;
965
- }
966
-
967
- @media (max-width: 1120px) {
968
- .shell {
969
- grid-template-columns: 76px minmax(0, 1fr);
970
- }
971
-
972
- .brand b,
973
- .brand span,
974
- .store,
975
- .nav span,
976
- .nav-subnav,
977
- .health {
978
- display: none;
979
- }
980
-
981
- .sidebar {
982
- align-items: center;
983
- padding-inline: 12px;
984
- }
985
-
986
- .nav a {
987
- justify-content: center;
988
- padding: 0;
989
- }
990
-
991
- .grid.cols-4,
992
- .grid.cols-3,
993
- .order-assist-grid {
994
- grid-template-columns: repeat(2, minmax(0, 1fr));
995
- }
996
- }
997
-
998
- @media (max-width: 760px) {
999
- html,
1000
- body {
1001
- height: auto;
1002
- min-height: 100%;
1003
- overflow-x: hidden;
1004
- overflow-y: auto;
1005
- }
1006
-
1007
- .shell {
1008
- grid-template-columns: 1fr;
1009
- height: auto;
1010
- min-height: 100vh;
1011
- overflow: visible;
1012
- }
1013
-
1014
- .main {
1015
- height: auto;
1016
- min-height: 0;
1017
- overflow: visible;
1018
- }
1019
-
1020
- .sidebar {
1021
- position: static;
1022
- height: auto;
1023
- overflow: visible;
1024
- flex-direction: row;
1025
- align-items: center;
1026
- }
1027
-
1028
- .nav {
1029
- display: none;
1030
- }
1031
-
1032
- .mobile-tabbar {
1033
- position: sticky;
1034
- top: 0;
1035
- z-index: 29;
1036
- display: flex;
1037
- padding-inline: 16px;
1038
- background: rgba(245, 247, 246, 0.94);
1039
- backdrop-filter: blur(16px);
1040
- }
1041
-
1042
- .topbar,
1043
- .ship-gate,
1044
- .risk-alert,
1045
- .section-title,
1046
- .panel-head {
1047
- align-items: flex-start;
1048
- flex-direction: column;
1049
- }
1050
-
1051
- .content,
1052
- .topbar {
1053
- padding-inline: 16px;
1054
- }
1055
-
1056
- .floating-ask {
1057
- right: 16px;
1058
- bottom: 16px;
1059
- }
1060
-
1061
- [data-admin-panel],
1062
- [data-admin-guide-section] {
1063
- scroll-margin-top: 16px;
1064
- }
1065
-
1066
- .actions {
1067
- width: 100%;
1068
- }
1069
-
1070
- .actions .btn {
1071
- flex: 1 1 150px;
1072
- }
1073
-
1074
- .grid.cols-4,
1075
- .grid.cols-3,
1076
- .grid.cols-2,
1077
- .order-assist-grid {
1078
- grid-template-columns: 1fr;
1079
- }
1080
-
1081
- .check-row {
1082
- grid-template-columns: 1fr;
1083
- }
1084
- }
1085
- </style>
1086
119
  </head>
1087
120
  <body>
1088
121
  <svg aria-hidden="true" style="position:absolute;width:0;height:0;overflow:hidden">
1089
122
  <symbol id="i-orders" viewBox="0 0 24 24"><path d="M7 4h10a2 2 0 0 1 2 2v14l-3-2-2 2-2-2-2 2-3 2V6a2 2 0 0 1 2-2Z"></path><path d="M9 9h6M9 13h6"></path></symbol>
1090
123
  <symbol id="i-box" viewBox="0 0 24 24"><path d="m3 7 9-4 9 4-9 4-9-4Z"></path><path d="M3 7v10l9 4 9-4V7"></path><path d="M12 11v10"></path></symbol>
1091
124
  <symbol id="i-users" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2"></path><circle cx="9.5" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.9"></path><path d="M16 3.1a4 4 0 0 1 0 7.8"></path></symbol>
125
+ <symbol id="i-user" viewBox="0 0 24 24"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></symbol>
1092
126
  <symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 20 6v6c0 5-3.4 8.4-8 9-4.6-.6-8-4-8-9V6l8-3Z"></path><path d="m8.8 12 2.2 2.2L15.8 9"></path></symbol>
1093
127
  <symbol id="i-chart" viewBox="0 0 24 24"><path d="M4 19V5"></path><path d="M4 19h16"></path><path d="M8 16v-5"></path><path d="M12 16V8"></path><path d="M16 16v-3"></path></symbol>
1094
128
  <symbol id="i-workflows" viewBox="0 0 24 24"><path d="M6 5h6a4 4 0 0 1 4 4v1"></path><path d="m19 7-3 3-3-3"></path><path d="M18 19h-6a4 4 0 0 1-4-4v-1"></path><path d="m5 17 3-3 3 3"></path><circle cx="6" cy="5" r="2"></circle><circle cx="18" cy="19" r="2"></circle></symbol>
@@ -1154,6 +188,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1154
188
  <a href="#fulfillment" data-admin-tab-link><svg><use href="#i-truck"></use></svg><span>Fulfillment</span></a>
1155
189
  <a href="#reporting" data-admin-tab-link><svg><use href="#i-chart"></use></svg><span>Reporting</span></a>
1156
190
  <a href="#ai-workflows" data-admin-tab-link><svg><use href="#i-workflows"></use></svg><span>AI Workflows</span></a>
191
+ <a href="#blog" data-admin-tab-link><svg><use href="#i-file"></use></svg><span>Blog</span></a>
1157
192
  <a href="#publish" data-admin-tab-link><svg><use href="#i-export"></use></svg><span>Publish</span></a>
1158
193
  <a href="#settings" data-admin-tab-link><svg><use href="#i-gear"></use></svg><span>Settings</span></a>
1159
194
  </nav>
@@ -1163,6 +198,44 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1163
198
  <div class="health-row"><span>Payments</span><strong>OK</strong></div>
1164
199
  <div class="health-row"><span>Risk checks</span><strong>OK</strong></div>
1165
200
  </div>
201
+
202
+ <!-- Cross-surface hop: the developer cockpit is a SIBLING surface (dev
203
+ setup + connection status), not an admin tab — link, don't embed.
204
+ basePath-aware so it stays inside the tenant prefix. -->
205
+ <a class="cockpit-link" href={`${basePath || ""}/cockpit`}>
206
+ <svg><use href="#i-gear"></use></svg><span>Developer cockpit</span>
207
+ </a>
208
+
209
+ <div class="account" data-admin-account>
210
+ <span class="account-avatar" aria-hidden="true"><svg><use href="#i-user"></use></svg></span>
211
+ <span class="account-meta">
212
+ {viewerEmail ? (
213
+ <>
214
+ <span class="account-email" title={viewerEmail}>{viewerEmail}</span>
215
+ <span
216
+ class="account-standing"
217
+ data-standing={adminEntry.isOwnerViewer ? "owner" : isStaff ? "staff" : "none"}
218
+ >
219
+ {adminEntry.isOwnerViewer
220
+ ? `owner/admin of ${tenantEnvelope.targetTenant}`
221
+ : isStaff
222
+ ? `ToT staff · not an owner of ${tenantEnvelope.targetTenant}`
223
+ : `signed in · no owner/admin standing for ${tenantEnvelope.targetTenant}`}
224
+ </span>
225
+ <form class="account-signout" method="post" action="/api/auth/logout">
226
+ <button type="submit">Sign out</button>
227
+ </form>
228
+ </>
229
+ ) : (
230
+ <>
231
+ <span class="account-email">Not signed in</span>
232
+ <span class="account-standing" data-standing="none">
233
+ <a href="/auth/login">sign in</a> as an owner/admin of {tenantEnvelope.targetTenant} to publish
234
+ </span>
235
+ </>
236
+ )}
237
+ </span>
238
+ </div>
1166
239
  </aside>
1167
240
 
1168
241
  <main class="main">
@@ -1181,6 +254,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1181
254
  <a class="tab" href="#fulfillment" role="tab" aria-controls="fulfillment" aria-selected="false" data-admin-tab-link>Fulfillment</a>
1182
255
  <a class="tab" href="#reporting" role="tab" aria-controls="reporting" aria-selected="false" data-admin-tab-link>Reporting</a>
1183
256
  <a class="tab" href="#ai-workflows" role="tab" aria-controls="ai-workflows" aria-selected="false" data-admin-tab-link>AI Workflows</a>
257
+ <a class="tab" href="#blog" role="tab" aria-controls="blog" aria-selected="false" data-admin-tab-link>Blog</a>
1184
258
  <a class="tab" href="#publish" role="tab" aria-controls="publish" aria-selected="false" data-admin-tab-link>Publish</a>
1185
259
  <a class="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="false" data-admin-tab-link>Settings</a>
1186
260
  </div>
@@ -1229,6 +303,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1229
303
  <a id="admin-tab-fulfillment" class="tab" href="#fulfillment" role="tab" aria-controls="fulfillment" aria-selected="false" data-admin-tab-link>Fulfillment</a>
1230
304
  <a id="admin-tab-reporting" class="tab" href="#reporting" role="tab" aria-controls="reporting" aria-selected="false" data-admin-tab-link>Reporting</a>
1231
305
  <a id="admin-tab-ai-workflows" class="tab" href="#ai-workflows" role="tab" aria-controls="ai-workflows" aria-selected="false" data-admin-tab-link>AI Workflows</a>
306
+ <a id="admin-tab-blog" class="tab" href="#blog" role="tab" aria-controls="blog" aria-selected="false" data-admin-tab-link>Blog</a>
1232
307
  <a id="admin-tab-publish" class="tab" href="#publish" role="tab" aria-controls="publish" aria-selected="false" data-admin-tab-link>Publish</a>
1233
308
  <a id="admin-tab-settings" class="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="false" data-admin-tab-link>Settings</a>
1234
309
  </div>
@@ -1355,6 +430,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1355
430
  <AdminFulfillmentTab envelope={tenantEnvelope} />
1356
431
  <AdminReportingTab envelope={tenantEnvelope} />
1357
432
  <AdminAIWorkflowsTab envelope={tenantEnvelope} />
433
+ <AdminBlogTab envelope={tenantEnvelope} />
1358
434
  <AdminPublishTab envelope={tenantEnvelope} />
1359
435
  <AdminSettingsTab envelope={tenantEnvelope} />
1360
436
 
@@ -1470,239 +546,9 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1470
546
  </span>
1471
547
  </button>
1472
548
 
1473
- <script is:inline nonce={cspNonce}>
1474
- (() => {
1475
- const tabIds = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "publish", "settings"];
1476
- const tabSet = new Set(tabIds);
1477
- const tabHashPattern = new RegExp(`^#(${tabIds.join("|")})$`);
1478
- const canViewInternalGuide = document.documentElement.dataset.internalAdminGuide === "true";
1479
- const defaultTab = canViewInternalGuide ? "orders" : "settings";
1480
- const askContexts = {
1481
- orders: "Orders",
1482
- products: "Products",
1483
- subscriptions: "Subscriptions",
1484
- customers: "Customers",
1485
- fulfillment: "Fulfillment",
1486
- reporting: "Reporting",
1487
- "ai-workflows": "AI Workflows",
1488
- publish: "Publish",
1489
- settings: "Settings",
1490
- };
1491
-
1492
- const panelIdFromHash = () => {
1493
- const hash = window.location.hash.slice(1);
1494
- if (!hash) return defaultTab;
1495
- if (tabSet.has(hash)) return hash;
1496
- if (!canViewInternalGuide) return defaultTab;
1497
-
1498
- const target = hash ? document.getElementById(hash) : null;
1499
- const panel = target?.closest?.("[data-admin-panel]");
1500
- if (panel?.id && tabSet.has(panel.id)) return panel.id;
1501
-
1502
- return null;
1503
- };
1504
-
1505
- const setActiveTab = (id) => {
1506
- if (!tabSet.has(id)) return;
1507
-
1508
- document.documentElement.dataset.adminMode = "work-area";
1509
- document.documentElement.dataset.adminTab = id;
1510
- setAskContext(id);
1511
-
1512
- document.querySelectorAll("[data-admin-panel]").forEach((panel) => {
1513
- const active = panel.id === id;
1514
- panel.hidden = !active;
1515
- panel.setAttribute("aria-hidden", active ? "false" : "true");
1516
- });
1517
-
1518
- document.querySelectorAll("[data-admin-tab-link]").forEach((link) => {
1519
- const active = link.hash === `#${id}`;
1520
- link.classList.toggle("active", active);
1521
-
1522
- if (link.getAttribute("role") === "tab") {
1523
- link.setAttribute("aria-selected", active ? "true" : "false");
1524
- link.tabIndex = active ? 0 : -1;
1525
- }
1526
- });
1527
- };
1528
-
1529
- const setGuideMode = () => {
1530
- if (!canViewInternalGuide) {
1531
- setActiveTab(defaultTab);
1532
- return;
1533
- }
1534
-
1535
- document.documentElement.dataset.adminMode = "guide";
1536
- setAskContext(defaultTab);
1537
-
1538
- document.querySelectorAll("[data-admin-panel]").forEach((panel) => {
1539
- panel.hidden = true;
1540
- panel.setAttribute("aria-hidden", "true");
1541
- });
1542
-
1543
- document.querySelectorAll("[data-admin-tab-link]").forEach((link) => {
1544
- link.classList.remove("active");
1545
-
1546
- if (link.getAttribute("role") === "tab") {
1547
- link.setAttribute("aria-selected", "false");
1548
- link.tabIndex = 0;
1549
- }
1550
- });
1551
- };
1552
-
1553
- // Ask context envelope (baseline §Horizontal AI Concepts / Ask,
1554
- // ai-evidence-substrate item a — mirrors lib/admin/ai/askEnvelope.ts's
1555
- // buildAskEnvelope; duplicated here in plain JS because is:inline
1556
- // scripts cannot import modules). No chat backend yet: this is
1557
- // dev-inspectable only (window.__toTAdminAsk + a CustomEvent), never
1558
- // sent over the network.
1559
- const buildAskEnvelopePlain = (id) => {
1560
- const root = document.documentElement.dataset;
1561
- const isStaff = root.adminIsStaff === "true";
1562
- const panel = document.getElementById(id);
1563
- const selectedRecordId = panel?.querySelector("[data-admin-selected-id]")?.dataset.adminSelectedId || null;
1564
-
1565
- return {
1566
- tenant: {
1567
- adminAppTenant: root.adminAppTenant ?? "",
1568
- targetTenant: root.adminTargetTenant ?? "",
1569
- operator: root.adminOperator || null,
1570
- },
1571
- activeTab: id,
1572
- selectedRecordId,
1573
- role: isStaff ? "internal_staff" : "merchant_operator",
1574
- permissions: isStaff ? ["internal_admin", "cross_tenant_read"] : ["merchant_admin"],
1575
- filters: {},
1576
- evidenceContext: [],
1577
- };
1578
- };
1579
-
1580
- const setAskContext = (id) => {
1581
- const label = askContexts[id] ?? "Storefront";
1582
- const askButton = document.querySelector("[data-admin-ask]");
1583
- const askContext = document.querySelector("[data-admin-ask-context]");
1584
-
1585
- if (askContext) askContext.textContent = label;
1586
- if (askButton) {
1587
- askButton.setAttribute("aria-label", `Ask about ${label}`);
1588
- askButton.setAttribute("title", `Ask about ${label}`);
1589
- askButton.dataset.askScope = id;
1590
- }
1591
-
1592
- window.__toTAdminAsk = buildAskEnvelopePlain(id);
1593
- };
1594
-
1595
- const scrollWorkspaceToTop = () => {
1596
- const scroller = document.querySelector(".main");
1597
- const reset = () => {
1598
- if (scroller) {
1599
- scroller.scrollTo({ top: 0, left: 0 });
1600
- return;
1601
- }
1602
-
1603
- window.scrollTo({ top: 0, left: 0 });
1604
- };
1605
-
1606
- reset();
1607
- requestAnimationFrame(reset);
1608
- setTimeout(reset, 0);
1609
- };
1610
-
1611
- const scrollToHashTarget = (panelId) => {
1612
- const hash = window.location.hash.slice(1);
1613
- const target = hash ? document.getElementById(hash) : null;
1614
-
1615
- if (!target || tabSet.has(hash) || target.id === panelId) {
1616
- scrollWorkspaceToTop();
1617
- return;
1618
- }
1619
-
1620
- target?.scrollIntoView({ block: "start" });
1621
- };
1622
-
1623
- const activateFromHash = ({ scroll = false } = {}) => {
1624
- const panelId = panelIdFromHash();
1625
-
1626
- if (panelId) {
1627
- setActiveTab(panelId);
1628
- const hash = window.location.hash.slice(1);
1629
- if (!canViewInternalGuide && (!hash || !tabSet.has(hash))) {
1630
- history.replaceState(null, "", "#settings");
1631
- }
1632
- if (scroll) requestAnimationFrame(() => scrollToHashTarget(panelId));
1633
- return;
1634
- }
1635
-
1636
- setGuideMode();
1637
- if (scroll) {
1638
- const hash = window.location.hash.slice(1);
1639
- requestAnimationFrame(() => document.getElementById(hash)?.scrollIntoView({ block: "start" }));
1640
- }
1641
- };
1642
-
1643
- document.querySelector("[data-admin-tenant-picker-select]")?.addEventListener("change", (event) => {
1644
- const params = new URLSearchParams(window.location.search);
1645
- params.set("asTenant", event.target.value);
1646
- window.location.assign(`${window.location.pathname}?${params}${window.location.hash}`);
1647
- });
1648
-
1649
- // Emit the Ask envelope on open. No chat backend yet — dev-inspectable
1650
- // only via window.__toTAdminAsk (already kept current by setAskContext)
1651
- // and this "tot:admin-ask" CustomEvent.
1652
- document.querySelector("[data-admin-ask]")?.addEventListener("click", () => {
1653
- const activeTab = document.documentElement.dataset.adminTab ?? defaultTab;
1654
- const envelope = buildAskEnvelopePlain(activeTab);
1655
- window.__toTAdminAsk = envelope;
1656
- document.dispatchEvent(new CustomEvent("tot:admin-ask", { detail: envelope }));
1657
- if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") {
1658
- console.debug("[admin:ask]", envelope);
1659
- }
1660
- });
1661
-
1662
- document.addEventListener("click", (event) => {
1663
- const target = event.target instanceof Element ? event.target : event.target?.parentElement;
1664
- const link = target?.closest("a[href^='#']");
1665
- if (!link || !tabHashPattern.test(link.hash)) return;
1666
-
1667
- event.preventDefault();
1668
- const id = link.hash.slice(1);
1669
- setActiveTab(id);
1670
- history.pushState(null, "", link.hash);
1671
- scrollWorkspaceToTop();
1672
- });
1673
-
1674
- document.addEventListener("keydown", (event) => {
1675
- const target = event.target instanceof Element ? event.target : null;
1676
- const currentTab = target?.closest("[role='tab'][data-admin-tab-link]");
1677
- if (!currentTab) return;
1678
-
1679
- // Filter to visible tabs only: the mobile tab bar and the internal
1680
- // Admin Tab Map can both be present in the DOM at once (one hidden
1681
- // via CSS depending on viewport/viewer), and this selector matches
1682
- // role="tab" elements in either — without this filter, arrow/home/
1683
- // end navigation could walk into a display:none tablist whose
1684
- // .focus() is a silent no-op.
1685
- const tabs = Array.from(document.querySelectorAll("[role='tab'][data-admin-tab-link]")).filter(
1686
- (el) => el.offsetParent !== null,
1687
- );
1688
- const currentIndex = tabs.indexOf(currentTab);
1689
- let nextIndex = currentIndex;
1690
-
1691
- if (event.key === "ArrowRight") nextIndex = (currentIndex + 1) % tabs.length;
1692
- if (event.key === "ArrowLeft") nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
1693
- if (event.key === "Home") nextIndex = 0;
1694
- if (event.key === "End") nextIndex = tabs.length - 1;
1695
-
1696
- if (nextIndex === currentIndex) return;
1697
-
1698
- event.preventDefault();
1699
- tabs[nextIndex].focus();
1700
- tabs[nextIndex].click();
1701
- });
1702
-
1703
- window.addEventListener("hashchange", () => activateFromHash({ scroll: true }));
1704
- activateFromHash({ scroll: Boolean(window.location.hash) });
1705
- })();
549
+ <script>
550
+ import { initAdminShell } from "@/lib/admin/adminShell";
551
+ initAdminShell();
1706
552
  </script>
1707
553
  </body>
1708
554
  </html>