@stacksjs/defaults 0.70.353 → 0.70.355

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 (125) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +17 -2
  2. package/ai/skills/stacks-browse/scripts/browse.ts +207 -6
  3. package/ai/skills/stacks-dashboard/scripts/audit.ts +0 -1
  4. package/ai/skills/stacks-email/SKILL.md +35 -15
  5. package/app/Actions/Dashboard/Auth/MeAction.ts +8 -7
  6. package/app/Actions/Dashboard/Ci/RepoRunJobsAction.ts +10 -15
  7. package/app/Actions/Dashboard/Ci/RepoRunsAction.ts +12 -14
  8. package/app/Actions/Dashboard/Ci/RunnerHistoryAction.ts +11 -10
  9. package/app/Actions/Dashboard/Ci/StatusAction.ts +2 -11
  10. package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +1 -0
  11. package/app/Actions/Dashboard/Commerce/CommercePrintLogsAction.ts +3 -4
  12. package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +1 -0
  13. package/app/Actions/Dashboard/Commerce/PaymentRefundAction.ts +1 -0
  14. package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +1 -0
  15. package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +1 -0
  16. package/app/Actions/Dashboard/Commerce/tax-rate-records.ts +9 -0
  17. package/app/Actions/Dashboard/Content/author-input.ts +1 -1
  18. package/app/Actions/Dashboard/Content/page-input.ts +1 -1
  19. package/app/Actions/Dashboard/DashboardStatsAction.ts +0 -1
  20. package/app/Actions/Dashboard/Deployments/CreateDeployment.ts +3 -2
  21. package/app/Actions/Dashboard/Deployments/GetDeployment.ts +1 -0
  22. package/app/Actions/Dashboard/Deployments/UpdateDeployScript.ts +3 -2
  23. package/app/Actions/Dashboard/Email/InboxActivityAction.ts +28 -16
  24. package/app/Actions/Dashboard/Email/InboxAttachmentDownloadAction.ts +45 -0
  25. package/app/Actions/Dashboard/Email/InboxDeleteAction.ts +11 -6
  26. package/app/Actions/Dashboard/Email/InboxIndexAction.ts +5 -7
  27. package/app/Actions/Dashboard/Email/InboxMarkReadAction.ts +11 -6
  28. package/app/Actions/Dashboard/Email/InboxMarkUnreadAction.ts +11 -6
  29. package/app/Actions/Dashboard/Email/InboxPreferenceShowAction.ts +10 -4
  30. package/app/Actions/Dashboard/Email/InboxPreferenceUpdateAction.ts +8 -9
  31. package/app/Actions/Dashboard/Email/InboxShowAction.ts +7 -7
  32. package/app/Actions/Dashboard/Email/InboxStatsAction.ts +5 -7
  33. package/app/Actions/Dashboard/Email/inbox-request.ts +33 -0
  34. package/app/Actions/Dashboard/Infrastructure/LogIndexAction.ts +3 -4
  35. package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +3 -6
  36. package/app/Actions/Dashboard/Jobs/JobIndexAction.ts +8 -15
  37. package/app/Actions/Dashboard/Jobs/JobRetryAction.ts +4 -3
  38. package/app/Actions/Dashboard/Jobs/JobShowAction.ts +3 -3
  39. package/app/Actions/Dashboard/Kanban/BoardDestroyAction.ts +5 -5
  40. package/app/Actions/Dashboard/Kanban/BoardShowAction.ts +9 -16
  41. package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +4 -3
  42. package/app/Actions/Dashboard/Kanban/BoardUpdateAction.ts +5 -5
  43. package/app/Actions/Dashboard/Kanban/BoardsIndexAction.ts +1 -2
  44. package/app/Actions/Dashboard/Kanban/BoardsReorderAction.ts +4 -3
  45. package/app/Actions/Dashboard/Kanban/CardAssigneesSyncAction.ts +11 -9
  46. package/app/Actions/Dashboard/Kanban/CardCommentDestroyAction.ts +10 -12
  47. package/app/Actions/Dashboard/Kanban/CardCommentStoreAction.ts +19 -43
  48. package/app/Actions/Dashboard/Kanban/CardCommentUpdateAction.ts +44 -0
  49. package/app/Actions/Dashboard/Kanban/CardDestroyAction.ts +5 -5
  50. package/app/Actions/Dashboard/Kanban/CardLabelsSyncAction.ts +6 -6
  51. package/app/Actions/Dashboard/Kanban/CardShowAction.ts +4 -4
  52. package/app/Actions/Dashboard/Kanban/CardStoreAction.ts +7 -6
  53. package/app/Actions/Dashboard/Kanban/CardUpdateAction.ts +5 -5
  54. package/app/Actions/Dashboard/Kanban/CardsReorderAction.ts +4 -3
  55. package/app/Actions/Dashboard/Kanban/ColumnDestroyAction.ts +5 -5
  56. package/app/Actions/Dashboard/Kanban/ColumnStoreAction.ts +4 -3
  57. package/app/Actions/Dashboard/Kanban/ColumnUpdateAction.ts +5 -5
  58. package/app/Actions/Dashboard/Kanban/ColumnsReorderAction.ts +4 -3
  59. package/app/Actions/Dashboard/Kanban/LabelDestroyAction.ts +4 -4
  60. package/app/Actions/Dashboard/Kanban/LabelStoreAction.ts +4 -3
  61. package/app/Actions/Dashboard/Kanban/LabelUpdateAction.ts +5 -5
  62. package/app/Actions/Dashboard/Kanban/UsersListAction.ts +1 -1
  63. package/app/Actions/Dashboard/Kanban/kanban-comment.ts +14 -0
  64. package/app/Actions/Dashboard/Kanban/kanban-model.test.ts +36 -0
  65. package/app/Actions/Dashboard/Kanban/kanban-model.ts +14 -7
  66. package/app/Actions/Dashboard/Models/ModelDestroyAction.ts +4 -3
  67. package/app/Actions/Dashboard/Models/ModelShowAction.ts +15 -18
  68. package/app/Actions/Dashboard/Models/ModelStoreAction.ts +8 -8
  69. package/app/Actions/Dashboard/Models/ModelUpdateAction.ts +9 -9
  70. package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +4 -3
  71. package/app/Actions/Dashboard/Models/model-api.ts +27 -0
  72. package/app/Actions/Dashboard/Models/model-write.ts +2 -10
  73. package/app/Actions/Dashboard/Notifications/NotificationDeliveryHistoryAction.ts +3 -4
  74. package/app/Actions/Dashboard/Notifications/NotificationDeliveryIndexAction.ts +6 -4
  75. package/app/Actions/Dashboard/Queries/QueryShowAction.ts +1 -0
  76. package/app/Actions/Dashboard/Rbac/PermissionDestroyAction.ts +4 -4
  77. package/app/Actions/Dashboard/Rbac/PermissionStoreAction.ts +3 -2
  78. package/app/Actions/Dashboard/Rbac/RoleDestroyAction.ts +4 -4
  79. package/app/Actions/Dashboard/Rbac/RolePermissionsShowAction.ts +4 -4
  80. package/app/Actions/Dashboard/Rbac/RolePermissionsSyncAction.ts +4 -3
  81. package/app/Actions/Dashboard/Rbac/RoleStoreAction.ts +3 -2
  82. package/app/Actions/Dashboard/Rbac/UserRolesShowAction.ts +4 -5
  83. package/app/Actions/Dashboard/Rbac/UserRolesSyncAction.ts +4 -4
  84. package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +1 -0
  85. package/app/Actions/Dashboard/Teams/TeamInviteAction.ts +3 -3
  86. package/app/Actions/Dashboard/Teams/TeamMemberUpdateAction.ts +2 -2
  87. package/app/Actions/Dashboard/dashboard-request.ts +13 -0
  88. package/app/Models/CardComment.ts +3 -4
  89. package/app/Models/commerce/TaxRate.ts +48 -0
  90. package/functions/dashboard-api.ts +51 -10
  91. package/functions/inbox.test.ts +2 -0
  92. package/functions/inbox.ts +27 -2
  93. package/functions/public-application-url.test.ts +13 -0
  94. package/functions/public-application-url.ts +27 -0
  95. package/ide/vscode/package.json +1 -1
  96. package/package.json +1 -1
  97. package/resources/components/Dashboard/Auth/AccessTokens.stx +15 -0
  98. package/resources/components/Dashboard/Ci/CiDashboard.stx +24 -6
  99. package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +23 -0
  100. package/resources/components/Dashboard/Commerce/CommerceTaxesDashboard.stx +4 -0
  101. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +9 -1
  102. package/resources/components/Dashboard/Commerce/ProductWaitlistDashboard.stx +8 -7
  103. package/resources/components/Dashboard/Commerce/RestaurantWaitlistDashboard.stx +8 -7
  104. package/resources/components/Dashboard/Content/BlogDashboard.stx +14 -2
  105. package/resources/components/Dashboard/Content/PostsDashboard.stx +7 -1
  106. package/resources/components/Dashboard/Deployments/DeploymentDetail.stx +16 -2
  107. package/resources/components/Dashboard/Email/EmailNavigation.stx +5 -5
  108. package/resources/components/Dashboard/Email/EmailSidebar.stx +1 -1
  109. package/resources/components/Dashboard/Email/InboxAttachmentList.stx +53 -0
  110. package/resources/components/Dashboard/Email/InboxDashboard.stx +5 -4
  111. package/resources/components/Dashboard/Email/InboxMessageDetail.stx +6 -4
  112. package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +63 -6
  113. package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +3 -6
  114. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +4 -4
  115. package/resources/components/Dashboard/NotFoundDashboard.stx +2 -5
  116. package/resources/components/Dashboard/Queries/QueryDashboard.stx +6 -2
  117. package/resources/components/Dashboard/Settings/AppearanceSettingsDashboard.stx +4 -4
  118. package/resources/components/Dashboard/UI/EmptyState.stx +2 -2
  119. package/resources/components/Dashboard/UI/PageLayout.stx +3 -3
  120. package/resources/functions/dashboard/sidebar.ts +4 -4
  121. package/routes/dashboard-api.ts +8 -20
  122. package/views/dashboard/.discovered-models.json +24 -0
  123. package/views/dashboard/layouts/default.stx +19 -16
  124. package/views/dashboard/stores/ci.ts +13 -7
  125. package/views/dashboard/stores/kanban.ts +48 -11
@@ -5,7 +5,7 @@
5
5
  // and the role/permission matrix against the live `user_roles` /
6
6
  // `role_permissions` / `user_permissions` pivots. All persistence
7
7
  // flows through the dashboard RBAC endpoints, which delegate
8
- // to `@stacksjs/auth`'s `Rbac` facade no direct DB writes from the
8
+ // to `@stacksjs/auth`'s `Rbac` facade - no direct DB writes from the
9
9
  // page.
10
10
  //
11
11
  // Follows #1838 conventions:
@@ -13,13 +13,13 @@
13
13
  // - All state in the rbacStore; the page is pure presentation.
14
14
  // - Mutations are optimistic with rollback on failure (store handles
15
15
  // the snapshot/rollback dance).
16
- import { useStore } from '@stacksjs/stx'
17
16
  import { useRole } from '~/storage/framework/defaults/views/dashboard/composables/useRole'
17
+ import { rbacStore } from '~/storage/framework/defaults/views/dashboard/stores/rbac'
18
18
 
19
- const rbac = useStore('rbac')
19
+ const rbac = rbacStore
20
20
  // Admin-only surface. Per `useRole.ts`, `isAdmin()` returns true for
21
21
  // any user with the `admin` role *or* an unauthenticated viewer on
22
- // the localhost dev dashboard same permissive-dev defaults the rest
22
+ // the localhost dev dashboard - same permissive-dev defaults the rest
23
23
  // of the management surface uses.
24
24
  const { isAdmin } = useRole()
25
25
  const canManage = derived(() => isAdmin())
@@ -11,19 +11,16 @@
11
11
  Sorry, we couldn't find the page you're looking for. The page might have been moved or deleted.
12
12
  </p>
13
13
  <div class="flex gap-x-6 items-center justify-center mt-10">
14
- <Button tag="a" href="/dashboard">
14
+ <Button tag="a" href="/">
15
15
  Return to dashboard
16
16
  </Button>
17
- <Button tag="a" href="/" variant="ghost">
18
- Go to home <span aria-hidden="true">&rarr;</span>
19
- </Button>
20
17
  </div>
21
18
  </div>
22
19
 
23
20
  <div>
24
21
  <h2 class="font-semibold leading-6 text-base text-center text-gray-900 dark:text-white">You might be looking for</h2>
25
22
  <div class="grid gap-5 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 mt-6">
26
- <StxLink to="/dashboard" class="overflow-hidden bg-white dark:bg-neutral-800 rounded-lg shadow hover:shadow-md duration-200 transition-shadow">
23
+ <StxLink to="/" class="overflow-hidden bg-white dark:bg-neutral-800 rounded-lg shadow hover:shadow-md duration-200 transition-shadow">
27
24
  <div class="p-5">
28
25
  <div class="flex items-center">
29
26
  <div class="flex-shrink-0 p-3 bg-blue-500 rounded-md">
@@ -97,6 +97,10 @@ function formatDate(value: string): string {
97
97
  return Number.isNaN(date.getTime()) ? value : date.toLocaleString()
98
98
  }
99
99
 
100
+ function queryDetailsPath(id: DashboardQueryLog['id']): string {
101
+ return `/queries/${encodeURIComponent(String(id))}`
102
+ }
103
+
100
104
  function typeClass(value: DashboardQueryLog['type']): string {
101
105
  const classes: Record<DashboardQueryLog['type'], string> = {
102
106
  SELECT: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40',
@@ -248,9 +252,9 @@ onMount(() => {
248
252
  </td>
249
253
  <td class="px-4 py-3.5 text-neutral-500 text-right text-xs whitespace-nowrap dark:text-neutral-400">{{ formatDate(query.executedAt) }}</td>
250
254
  <td class="pr-5 py-3.5 text-right">
251
- <StxLink :to="'/queries/' + query.id" :aria-label="'View query ' + query.id" class="inline-flex p-1.5 text-neutral-500 dark:text-neutral-400 hover:text-blue-600 dark:hover:text-blue-400 rounded-md">
255
+ <a :href="queryDetailsPath(query.id)" :aria-label="'View query ' + query.id" class="inline-flex p-1.5 text-neutral-500 dark:text-neutral-400 hover:text-blue-600 dark:hover:text-blue-400 rounded-md">
252
256
  <span class="h-4 w-4 i-hugeicons-arrow-right-01"></span>
253
- </StxLink>
257
+ </a>
254
258
  </td>
255
259
  </tr>
256
260
  </template>
@@ -1,6 +1,6 @@
1
1
  <script server>
2
2
  // The navigation the toggles below control. Read from the same builder the
3
- // layout uses, so the list can never drift from what the sidebar renders
3
+ // layout uses, so the list can never drift from what the sidebar renders -
4
4
  // including sections a project has switched off in config/dashboard.ts, which
5
5
  // simply are not here to toggle.
6
6
  import { buildWebSidebarSections } from '~/storage/framework/defaults/resources/functions/dashboard/sidebar'
@@ -29,7 +29,7 @@ const appearance = useAppearance()
29
29
  // see until you navigate away is the thing this is avoiding.
30
30
  const styleOptions = SIDEBAR_STYLE_OPTIONS
31
31
  const colorModes = COLOR_MODE_OPTIONS
32
- const sections = navSections
32
+ const sections = {{ navSections }}
33
33
 
34
34
  const sidebarStyle = appearance.sidebarStyle
35
35
  const colorMode = appearance.colorMode
@@ -58,9 +58,9 @@ function resetAll() {
58
58
  }
59
59
 
60
60
  // Selected state travels as a data attribute rather than as a swapped class
61
- // string. It began as a workaround Crosswind could not see class names that
61
+ // string. It began as a workaround - Crosswind could not see class names that
62
62
  // existed only inside a client-script expression, so those utilities were never
63
- // emitted and the control rendered unstyled and that gap is now closed
63
+ // emitted and the control rendered unstyled - and that gap is now closed
64
64
  // upstream. It stays because it is the better shape regardless: one selector
65
65
  // per state in the block below, rather than two class strings kept in sync by
66
66
  // hand inside a ternary.
@@ -19,9 +19,9 @@ const actionHref = useReactiveProp('actionHref', '')
19
19
  <p :if="description()" class="mb-6 max-w-sm text-neutral-500 text-sm dark:text-neutral-400" :text="description()"></p>
20
20
 
21
21
  <Button
22
- :if="actionLabel()"
22
+ :if="actionLabel() && actionHref()"
23
23
  tag="a"
24
- :href="actionHref() || '#'"
24
+ :href="actionHref()"
25
25
  >
26
26
  <span :if="actionIcon()" :class="actionIcon() + ' h-4 w-4'"></span>
27
27
  <span :text="actionLabel()"></span>
@@ -3,11 +3,11 @@ interface PageLayoutProps {
3
3
  /**
4
4
  * Visual variant.
5
5
  *
6
- * - `default` standard centered column with `mx-auto` + max-width
6
+ * - `default` - standard centered column with `mx-auto` + max-width
7
7
  * gutter. Use for almost every page.
8
- * - `centered` vertically centered hero block on a min-height
8
+ * - `centered` - vertically centered hero block on a min-height
9
9
  * container. Use for splash / 404 / empty-route pages.
10
- * - `bare` no container at all. Use when the page wants to own
10
+ * - `bare` - no container at all. Use when the page wants to own
11
11
  * its own gutter (e.g. a settings page with side-by-side panels
12
12
  * that already pad themselves).
13
13
  */
@@ -428,11 +428,11 @@ export function buildNavSections(
428
428
  // The layout's role map hides the row from client-role viewers.
429
429
  { to: '/kanban', icon: 'table', text: 'Kanban', roles: ['admin', 'dev'] },
430
430
  ]
431
- // CI tracking (stacksjs/stacks#1844) opt-in via `ci.enabled` in
432
- // config/dashboard.ts. The page additionally checks `useRole().isDev()`
433
- // (stub for #1843) before rendering.
431
+ // CI tracking (stacksjs/stacks#1844) - opt-in via `ci.enabled` in
432
+ // config/dashboard.ts. Keep the row gate aligned with the page's
433
+ // `useRole().isDev()` check so client-only viewers never get a dead link.
434
434
  if (toggles.ci)
435
- managementItems.push({ to: '/ci', icon: 'check-circle', text: 'CI' })
435
+ managementItems.push({ to: '/ci', icon: 'check-circle', text: 'CI', roles: ['dev'] })
436
436
  sections.push(['management', 'management', managementItems])
437
437
  }
438
438
 
@@ -264,28 +264,15 @@ route.group({ prefix: '/api/dashboard', apiResponse: true }, () => {
264
264
  // requests — see the action for the soft-fallback shape.
265
265
  route.get('/auth/me', 'Actions/Dashboard/Auth/MeAction')
266
266
 
267
- // Kanban surface (stacksjs/stacks#1846). Phase 1: read-only.
268
- //
269
- // Phase 2 lands write endpoints under the same prefix:
270
- // POST /kanban/boards — store
271
- // PATCH /kanban/boards/{id} — update
272
- // DELETE /kanban/boards/{id} — destroy
273
- // POST /kanban/boards/reorder — bulk position update
274
- // POST /kanban/columns — store
275
- // PATCH /kanban/columns/{id} — update
276
- // POST /kanban/columns/reorder — bulk position update
277
- // POST /kanban/cards — store
278
- // PATCH /kanban/cards/{id} — update (incl. column move)
279
- // POST /kanban/cards/reorder — bulk position update
280
- //
267
+ // Kanban reads and mutations share the guarded dashboard surface.
281
268
  // Kanban data is local-only without authentication. Outside local
282
269
  // environments every read and write uses the same auth + admin guard as
283
270
  // the other operational dashboard surfaces.
284
- // Reads (Phase 1)
271
+ // Reads
285
272
  guard(route.get('/kanban/boards', 'Actions/Dashboard/Kanban/BoardsIndexAction'))
286
273
  guard(route.get('/kanban/boards/{id}', 'Actions/Dashboard/Kanban/BoardShowAction'))
287
274
 
288
- // Writes (Phase 2). The reorder endpoints are POST not PATCH because
275
+ // Writes. The reorder endpoints are POST not PATCH because
289
276
  // their semantics — "here's the full new state of this slice of the
290
277
  // board" — match the resource-replacement intent better than PATCH's
291
278
  // "apply this delta" verb. They also accept a body shape that PATCH
@@ -305,7 +292,7 @@ route.group({ prefix: '/api/dashboard', apiResponse: true }, () => {
305
292
  guard(route.delete('/kanban/cards/{id}', 'Actions/Dashboard/Kanban/CardDestroyAction'))
306
293
  guard(route.post('/kanban/cards/reorder', 'Actions/Dashboard/Kanban/CardsReorderAction'))
307
294
 
308
- // Phase 3 — card detail + labels + assignees + comments.
295
+ // Card detail, labels, assignees, and comments.
309
296
  //
310
297
  // The card-show endpoint is the only "single card with everything"
311
298
  // read; the boards/{id} response already embeds labels + assignees
@@ -325,10 +312,10 @@ route.group({ prefix: '/api/dashboard', apiResponse: true }, () => {
325
312
  guard(route.post('/kanban/cards/{id}/labels', 'Actions/Dashboard/Kanban/CardLabelsSyncAction'))
326
313
  guard(route.post('/kanban/cards/{id}/assignees', 'Actions/Dashboard/Kanban/CardAssigneesSyncAction'))
327
314
 
328
- // Comments. Append-only thread: store + destroy, no edit yet — the
329
- // history-preservation argument outweighs the "fix a typo" argument
330
- // until someone explicitly asks for editing.
315
+ // Card-scoped comment thread. Writes stay under the dashboard guard while
316
+ // the CardComment model also exposes its conventional generated API.
331
317
  guard(route.post('/kanban/cards/{id}/comments', 'Actions/Dashboard/Kanban/CardCommentStoreAction'))
318
+ guard(route.patch('/kanban/comments/{id}', 'Actions/Dashboard/Kanban/CardCommentUpdateAction'))
332
319
  guard(route.delete('/kanban/comments/{id}', 'Actions/Dashboard/Kanban/CardCommentDestroyAction'))
333
320
 
334
321
  // Lightweight user list for the assignee picker. Distinct from the
@@ -466,6 +453,7 @@ route.group({ prefix: '/api/dashboard', apiResponse: true }, () => {
466
453
  // the per-mailbox inbox.json index. Both are guarded outside local/dev
467
454
  // because they touch real mailbox state and PII.
468
455
  guard(route.get('/email/inbox', 'Actions/Dashboard/Email/InboxIndexAction'))
456
+ guard(route.get('/email/inbox/{id}/attachments/{attachmentId}', 'Actions/Dashboard/Email/InboxAttachmentDownloadAction'))
469
457
  guard(route.get('/email/inbox/{id}', 'Actions/Dashboard/Email/InboxShowAction'))
470
458
  guard(route.get('/email/stats', 'Actions/Dashboard/Email/InboxStatsAction'))
471
459
  guard(route.get('/email/activity', 'Actions/Dashboard/Email/InboxActivityAction'))
@@ -138,6 +138,16 @@
138
138
  "highlight": true
139
139
  }
140
140
  },
141
+ {
142
+ "id": "delivery-stop",
143
+ "name": "DeliveryStop",
144
+ "icon": "tablecells.fill",
145
+ "hasDedicatedPage": false,
146
+ "category": "commerce",
147
+ "dashboard": {
148
+ "highlight": true
149
+ }
150
+ },
141
151
  {
142
152
  "id": "deployment",
143
153
  "name": "Deployment",
@@ -168,6 +178,13 @@
168
178
  "highlight": true
169
179
  }
170
180
  },
181
+ {
182
+ "id": "driver-ping",
183
+ "name": "DriverPing",
184
+ "icon": "tablecells.fill",
185
+ "hasDedicatedPage": false,
186
+ "category": "commerce"
187
+ },
171
188
  {
172
189
  "id": "email-list",
173
190
  "name": "EmailList",
@@ -456,6 +473,13 @@
456
473
  "highlight": true
457
474
  }
458
475
  },
476
+ {
477
+ "id": "social-account",
478
+ "name": "SocialAccount",
479
+ "icon": "tablecells.fill",
480
+ "hasDedicatedPage": false,
481
+ "category": "data"
482
+ },
459
483
  {
460
484
  "id": "social-post",
461
485
  "name": "SocialPost",
@@ -1,6 +1,6 @@
1
1
  <script server>
2
2
  // Dashboard chrome. The navigation pane is `@stacksjs/components`' <Sidebar>
3
- // in its `macos` theme the same component Craft apps use, so the dashboard
3
+ // in its `macos` theme - the same component Craft apps use, so the dashboard
4
4
  // and a native Craft build render the identical source list.
5
5
  //
6
6
  // This used to be a hand-rolled <aside> plus ~180 lines of plain CSS, because
@@ -89,7 +89,7 @@ canvas[id$="_chart"] {
89
89
  * solid body background would paint over it. So the body stays clear and the
90
90
  * content color lives on the scrolling pane, which lets the frosted material
91
91
  * read through the translucent sidebar. (Do not write angle-bracket tag names in
92
- * a layout style-block comment stx parses them as elements and breaks out.)
92
+ * a layout style-block comment - stx parses them as elements and breaks out.)
93
93
  */
94
94
  body {
95
95
  background: transparent;
@@ -104,6 +104,9 @@ body {
104
104
  [data-stx-content] {
105
105
  min-width: 0;
106
106
  min-height: 100vh;
107
+ /* Wide tables scroll inside their own overflow containers without widening
108
+ the document or moving the fixed dashboard shell. */
109
+ overflow-x: hidden;
107
110
  margin-left: var(--stx-sidebar-width, 250px);
108
111
  background: #f4f4f5;
109
112
  transition: margin-left 0.3s ease-out;
@@ -158,15 +161,15 @@ body {
158
161
  * Two halves, and only one of them is the sidebar.
159
162
  *
160
163
  * THE SHELL. Arc's defining move is that the content is not a pane butted up
161
- * against a sidebar it is a card floating ON the sidebar's background, inset
164
+ * against a sidebar - it is a card floating ON the sidebar's background, inset
162
165
  * and rounded on every side. So the panel color moves to the body, the sidebar
163
166
  * goes transparent on top of it, and the content gets a margin all the way
164
167
  * round. No sidebar theme can do this part; it belongs to the layout.
165
168
  *
166
169
  * THE ROWS. The installed @stacksjs/components predates its own `arc` theme,
167
170
  * so the component renders macOS rows and this restyles them. The values are
168
- * copied from that theme on purpose 30px rows, 8px radius, a white card with
169
- * a hairline and a one-pixel shadow instead of an accent fill so that when
171
+ * copied from that theme on purpose - 30px rows, 8px radius, a white card with
172
+ * a hairline and a one-pixel shadow instead of an accent fill - so that when
170
173
  * the package ships it, the component and this skin agree and these rules
171
174
  * become a no-op rather than a second, competing look.
172
175
  *
@@ -215,7 +218,7 @@ html[data-appearance="arc"] [data-stx-content] {
215
218
  properties wedges the interpolation in WebKit: switching appearance leaves
216
219
  it stuck at the old value until a reload. Inheriting the base declaration
217
220
  means the property never changes when the appearance does, so there is
218
- nothing to wedge and the collapse animation keeps working identically in
221
+ nothing to wedge - and the collapse animation keeps working identically in
219
222
  both styles.
220
223
 
221
224
  The panel strip down the card's left side is the sidebar's own trailing
@@ -224,20 +227,20 @@ html[data-appearance="arc"] [data-stx-content] {
224
227
  margin-right: var(--dash-arc-gap);
225
228
  margin-bottom: var(--dash-arc-gap);
226
229
  /* Beats the base rule's `min-height: 100vh`, which would otherwise hold the
227
- card a full viewport tall and push its bottom edge off-screen losing the
230
+ card a full viewport tall and push its bottom edge off-screen - losing the
228
231
  one gap that shows it is floating. */
229
232
  min-height: 0;
230
233
  /* The card is exactly the viewport minus its own gaps, and scrolls its own
231
234
  content. Letting the page scroll instead would grow the card past the
232
- bottom of the window, so the one edge that proves it is floating the
233
- panel showing beneath it would only ever appear at the end of a long
235
+ bottom of the window, so the one edge that proves it is floating - the
236
+ panel showing beneath it - would only ever appear at the end of a long
234
237
  page. This is also what Arc does: the sidebar is stationary and the card
235
238
  scrolls inside its frame. */
236
239
  height: calc(100vh - (2 * var(--dash-arc-gap)));
237
240
  overflow-y: auto;
238
241
  border-radius: var(--dash-arc-radius);
239
242
  background: #fff;
240
- /* Clip page content to the rounded corners full-bleed headers and tables
243
+ /* Clip page content to the rounded corners - full-bleed headers and tables
241
244
  otherwise square them off at the top. */
242
245
  overflow-x: hidden;
243
246
  box-shadow:
@@ -352,7 +355,7 @@ html[data-appearance="arc"].dark [data-sidebar-selection="accent"][data-window-k
352
355
  }
353
356
 
354
357
  /* The icon, count and chevron turn white on a macOS accent fill. On a white
355
- card they must not they keep their own tint, the same as an idle row. */
358
+ card they must not - they keep their own tint, the same as an idle row. */
356
359
  html[data-appearance="arc"] [data-sidebar-selection="accent"][data-window-key="true"] [data-sidebar-item][data-active="true"] [data-sidebar-icon],
357
360
  html[data-appearance="arc"] [data-sidebar-selection="accent"][data-window-key="true"] [data-sidebar-item][data-active="true"] [data-sidebar-chevron] {
358
361
  color: var(--stx-sidebar-icon, currentColor);
@@ -372,7 +375,7 @@ html[data-appearance="arc"] [data-sidebar-section] > div:first-child {
372
375
 
373
376
  /* A viewer can hide navigation sections they never use. Kept as CSS on an
374
377
  attribute rather than a DOM removal so the rows stay hydrated and a later
375
- change re-shows them without a re-render the same approach the role
378
+ change re-shows them without a re-render - the same approach the role
376
379
  filter above uses. */
377
380
  [data-sidebar-section][data-viewer-hidden="true"] {
378
381
  display: none;
@@ -397,7 +400,7 @@ registerStoresClient({ authStore, kanbanStore, rbacStore })
397
400
 
398
401
  // Helpers below return COMPLETE class strings (static + dynamic) and are
399
402
  // applied via `:class="fn(...)"` because stx's `{{ ... }}` placeholder is
400
- // not reactively bound inside a `class` attribute only inside text content.
403
+ // not reactively bound inside a `class` attribute - only inside text content.
401
404
  const toastIconClass = (type) => `flex-shrink-0 h-5 w-5 mt-0.5 ${({
402
405
  success: 'i-hugeicons-checkmark-circle-02 text-green-500 dark:text-green-400',
403
406
  error: 'i-hugeicons-alert-02 text-red-500 dark:text-red-400',
@@ -466,7 +469,7 @@ effect(() => {
466
469
  // The inline bootstrap at the top of this layout already stamped the root
467
470
  // attributes for the first paint. These effects keep them in step afterwards:
468
471
  // re-running `applyAppearance` whenever a preference signal changes is what
469
- // makes the settings page feel immediate the shell restyles under the user
472
+ // makes the settings page feel immediate - the shell restyles under the user
470
473
  // while they are still looking at the picker, with no reload and no navigation.
471
474
  // ---------------------------------------------------------------------------
472
475
 
@@ -487,7 +490,7 @@ effect(() => {
487
490
  function applySectionVisibility() {
488
491
  // The signal is read BEFORE the panes are looked up, and that order is the
489
492
  // whole point. Read after an early return, it never gets tracked on the first
490
- // pass the panes are not mounted when effects first run so the effect
493
+ // pass - the panes are not mounted when effects first run - so the effect
491
494
  // ends up with no dependency and never runs again. The toggle then appears to
492
495
  // do nothing until a reload.
493
496
  const hidden = appearance.hiddenSections()
@@ -501,7 +504,7 @@ function applySectionVisibility() {
501
504
 
502
505
  // The effect covers later toggles. It cannot cover the FIRST paint on its own:
503
506
  // effects run before the layout's refs are populated, so the very first pass
504
- // finds no panes, and on a fresh load the preference never changes afterwards
507
+ // finds no panes, and on a fresh load the preference never changes afterwards -
505
508
  // leaving hidden sections visible until something else happens to touch the
506
509
  // signal. `onMount` below runs it again once the panes exist.
507
510
  effect(applySectionVisibility)
@@ -115,6 +115,7 @@ export const ciStore = defineStore('ci', () => {
115
115
  // Lazy-loaded on first view of an org's tab.
116
116
  const runnerHistoryByOrg = state<Record<string, Array<{ queued: number, sampledAt: string }>>>({})
117
117
  const loadingRunnerHistory = state<Record<string, boolean>>({})
118
+ const runnerHistoryErrors = state<Record<string, string>>({})
118
119
 
119
120
  const reposForTab = derived(() => {
120
121
  const tab = activeTab()
@@ -227,33 +228,37 @@ export const ciStore = defineStore('ci', () => {
227
228
  drilldownError.set(null)
228
229
  }
229
230
 
230
- async function loadRunnerHistory(org: string): Promise<void> {
231
+ async function loadRunnerHistory(org: string, force = false): Promise<void> {
231
232
  // Already loaded? Don't re-fetch — runner samples have a 30s
232
233
  // refresh cadence anyway. If the user wants a fresh view they
233
234
  // can reload the page.
234
- if (runnerHistoryByOrg()[org] !== undefined) return
235
+ if (!force && runnerHistoryByOrg()[org] !== undefined) return
235
236
  if (loadingRunnerHistory()[org]) return
236
237
 
237
238
  loadingRunnerHistory.set({ ...loadingRunnerHistory(), [org]: true })
239
+ runnerHistoryErrors.set({ ...runnerHistoryErrors(), [org]: '' })
238
240
  try {
239
241
  const data = await dashboardApi<{
240
242
  samples?: Array<{ queued: number, sampledAt: string }>
241
243
  error?: string
242
244
  disabled?: boolean
243
245
  }>(`/api/dashboard/ci/runner-history?org=${encodeURIComponent(org)}`)
244
- if (data.disabled || data.error) {
246
+ if (data.disabled) {
245
247
  runnerHistoryByOrg.set({ ...runnerHistoryByOrg(), [org]: [] })
246
248
  return
247
249
  }
250
+ if (data.error)
251
+ throw new Error(data.error)
248
252
  runnerHistoryByOrg.set({
249
253
  ...runnerHistoryByOrg(),
250
254
  [org]: (data.samples ?? []).map(s => ({ queued: s.queued, sampledAt: s.sampledAt })),
251
255
  })
252
256
  }
253
- catch {
254
- // Soft-fail — sparkline renders nothing, no need to surface
255
- // the error in the main CI page.
256
- runnerHistoryByOrg.set({ ...runnerHistoryByOrg(), [org]: [] })
257
+ catch (e) {
258
+ runnerHistoryErrors.set({
259
+ ...runnerHistoryErrors(),
260
+ [org]: e instanceof Error ? e.message : 'Runner history could not be loaded.',
261
+ })
257
262
  }
258
263
  finally {
259
264
  loadingRunnerHistory.set({ ...loadingRunnerHistory(), [org]: false })
@@ -327,6 +332,7 @@ export const ciStore = defineStore('ci', () => {
327
332
  // Runner-pressure history (#1850)
328
333
  runnerHistoryByOrg,
329
334
  loadingRunnerHistory,
335
+ runnerHistoryErrors,
330
336
  loadRunnerHistory,
331
337
  }
332
338
  }, {
@@ -41,8 +41,8 @@ interface CardRecord {
41
41
  archived: boolean
42
42
  createdAt: string | null
43
43
  updatedAt: string | null
44
- // Optional pivot-derived fields. BoardShowAction populates them
45
- // (Phase 3); CardStoreAction's optimistic insert leaves them empty.
44
+ // Optional pivot-derived fields. BoardShowAction populates them;
45
+ // CardStoreAction's optimistic insert leaves them empty.
46
46
  labels?: CardLabel[]
47
47
  assignees?: CardAssignee[]
48
48
  }
@@ -89,10 +89,9 @@ interface BoardDetail {
89
89
  /**
90
90
  * Kanban dashboard store (stacksjs/stacks#1846).
91
91
  *
92
- * Phase 1 surface list of boards for the index page plus the
93
- * fetch-by-id helper that the board detail page (Phase 2) will use.
94
- * Drag-and-drop reorder, write mutations, and label/assignee ops land
95
- * in Phase 2 / Phase 3.
92
+ * Lists boards for the index page and fetches a board for its detail page.
93
+ * Drag-and-drop reorder, write mutations, labels, assignees, and comments
94
+ * share this store so optimistic updates and rollback have one owner.
96
95
  *
97
96
  * Persistence: only the last-viewed board id is cached in
98
97
  * sessionStorage so the user lands back where they left off when they
@@ -110,7 +109,7 @@ export const kanbanStore = defineStore('kanban', () => {
110
109
  const error = state<string | null>(null)
111
110
  const errorBoard = state<string | null>(null)
112
111
 
113
- // Phase 3 — card detail modal state. Holding card + comments here
112
+ // Card detail modal state. Holding card + comments here
114
113
  // (instead of one of column.cards) keeps the modal independent of
115
114
  // its source: opening from a card click on the board uses the
116
115
  // already-loaded preview; refresh-mid-modal hits /cards/:id and
@@ -174,14 +173,14 @@ export const kanbanStore = defineStore('kanban', () => {
174
173
  }
175
174
  }
176
175
 
177
- // ─── Mutations (Phase 2) ───────────────────────────────────────────
176
+ // Mutations
178
177
  //
179
178
  // Every mutation follows the same shape:
180
179
  // 1. Apply optimistically — update the local store immediately so
181
180
  // the UI feels snappy.
182
181
  // 2. Fire the network request.
183
182
  // 3. On failure, restore the pre-mutation snapshot + set an error
184
- // message. Phase 3 wires a toast helper into the rollback path
183
+ // message. The dashboard toast helper handles the rollback path
185
184
  // so the user actually sees the failure.
186
185
  //
187
186
  // The mutations return the new entity (or null on failure) so
@@ -360,7 +359,7 @@ export const kanbanStore = defineStore('kanban', () => {
360
359
  }
361
360
  }
362
361
 
363
- // ─── Phase 3 mutations (card detail) ─────────────────────────────
362
+ // Card detail mutations
364
363
 
365
364
  /**
366
365
  * Helper: project a CardRecord (board view) onto a partial
@@ -584,6 +583,43 @@ export const kanbanStore = defineStore('kanban', () => {
584
583
  }
585
584
  }
586
585
 
586
+ async function updateComment(commentId: number, body: string): Promise<boolean> {
587
+ const oc = openCard()
588
+ const trimmed = body.trim()
589
+ if (!oc || !trimmed) return false
590
+ const snapshot = oc.comments
591
+ const existing = snapshot.find(comment => comment.id === commentId)
592
+ if (!existing || existing.body === trimmed) return true
593
+
594
+ patchOpenCard({
595
+ comments: snapshot.map(comment => comment.id === commentId
596
+ ? { ...comment, body: trimmed }
597
+ : comment),
598
+ })
599
+ try {
600
+ const data = await dashboardApi<{ comment?: CardComment, error?: string }>(`/api/dashboard/kanban/comments/${commentId}`, {
601
+ method: 'PATCH',
602
+ body: { body: trimmed },
603
+ })
604
+ if (data.error || !data.comment)
605
+ throw new Error(data.error ?? 'Comment update failed')
606
+ const current = openCard()
607
+ if (current) {
608
+ patchOpenCard({
609
+ comments: current.comments.map(comment => comment.id === commentId
610
+ ? data.comment as CardComment
611
+ : comment),
612
+ })
613
+ }
614
+ return true
615
+ }
616
+ catch (error) {
617
+ patchOpenCard({ comments: snapshot })
618
+ errorCard.set(error instanceof Error ? error.message : String(error))
619
+ return false
620
+ }
621
+ }
622
+
587
623
  // ─── Label CRUD (board scope) ──────────────────────────────────────
588
624
 
589
625
  async function createLabel(input: { boardId: number, name: string, color?: string }): Promise<LabelRecord | null> {
@@ -652,7 +688,7 @@ export const kanbanStore = defineStore('kanban', () => {
652
688
  deleteCard,
653
689
  reorderCards,
654
690
  reorderColumns,
655
- // Phase 3
691
+ // Card detail
656
692
  openCard,
657
693
  loadingCard,
658
694
  errorCard,
@@ -665,6 +701,7 @@ export const kanbanStore = defineStore('kanban', () => {
665
701
  syncCardLabels,
666
702
  syncCardAssignees,
667
703
  addComment,
704
+ updateComment,
668
705
  deleteComment,
669
706
  createLabel,
670
707
  deleteLabel,