@stacksjs/defaults 0.75.263 → 0.75.267

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 (23) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +3 -0
  2. package/ai/skills/stacks-browse/scripts/browse.ts +73 -14
  3. package/ai/skills/stacks-chat/SKILL.md +32 -0
  4. package/ai/skills/stacks-native/CATALOG.md +8 -8
  5. package/ide/vscode/package.json +1 -1
  6. package/package.json +4 -4
  7. package/resources/components/Dashboard/App/RequestsOverview.stx +75 -58
  8. package/resources/components/Dashboard/App/SourceInventory.stx +68 -59
  9. package/resources/components/Dashboard/Auth/AccessTokens.stx +57 -37
  10. package/resources/components/Dashboard/Billing/BillingTransactionHistory.stx +27 -33
  11. package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +85 -49
  12. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +63 -66
  13. package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +67 -23
  14. package/resources/components/Dashboard/Content/BlogDashboard.stx +73 -67
  15. package/resources/components/Dashboard/Content/FileManagerDashboard.stx +39 -12
  16. package/resources/components/Dashboard/Data/DataRecords.stx +85 -88
  17. package/resources/components/Dashboard/Deployments/DeploymentTable.stx +49 -64
  18. package/resources/components/Dashboard/Infrastructure/DnsDashboard.stx +46 -28
  19. package/resources/components/Dashboard/Operations/RecoveryPointTable.stx +72 -61
  20. package/resources/components/Dashboard/Operations/SchedulerTaskTable.stx +74 -70
  21. package/resources/components/Dashboard/Queue/QueueDashboard.stx +27 -61
  22. package/resources/components/Dashboard/Releases/ReleaseDashboard.stx +35 -61
  23. package/resources/components/Dashboard/Teams/TeamPeopleDashboard.stx +127 -143
@@ -37,6 +37,8 @@ A candidate is validated with `--version` before use, so a dead wrapper (e.g. a
37
37
  shim pointing at an uninstalled `.app`) is skipped automatically. If none launch, it tells
38
38
  you to `brew install --cask chromium` or set `BROWSE_BROWSER`.
39
39
 
40
+ Startup waits up to 15 seconds for the running browser's valid DevTools port. An exited process or expired deadline fails launch; page readiness and scenario assertions have separate budgets.
41
+
40
42
  ## Default Stacks dev URLs
41
43
 
42
44
  - Frontend: `http://localhost:3000`
@@ -121,6 +123,7 @@ On a failed step, execution stops and the report retains its number and error,
121
123
  the completed steps, final page state and requested screenshot.
122
124
  Use `{"action":"assert","selector":"...","absent":true}` to verify that an element is missing or hidden after an interaction.
123
125
  Use `{"action":"assert","selector":"button","text":"Save","focused":true}` to verify keyboard focus and focus restoration.
126
+ For asynchronous results, use `{"action":"assert","selector":"button","text":"Sound on","timeoutMs":2000}`. This observes the actual visible result until it matches or the bounded budget expires. Ordinary assertions remain immediate. Only assertions accept `timeoutMs`, an integer from 0 to 30000; protocol and evaluation failures are not retried.
124
127
 
125
128
  ### Crawl (whole-site browser audit)
126
129
  ```bash
@@ -174,6 +174,28 @@ class Cdp {
174
174
 
175
175
  interface Session { proc: ReturnType<typeof spawn>, port: number, userDataDir: string, browser: string }
176
176
 
177
+ export async function waitForBrowserPort(proc: { exitCode: number | null }, portFile: string, timeoutMs: number): Promise<number | null> {
178
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 30_000)
179
+ throw new TypeError('Browser startup timeout must be an integer between 1 and 30000.')
180
+ const deadline = performance.now() + timeoutMs
181
+ while (performance.now() < deadline && proc.exitCode === null) {
182
+ try {
183
+ const line = readFileSync(portFile, 'utf8').split('\n')[0]?.trim()
184
+ const port = Number(line)
185
+ if (Number.isInteger(port) && port > 0 && port <= 65535) {
186
+ if (performance.now() >= deadline || proc.exitCode !== null) return null
187
+ return port
188
+ }
189
+ }
190
+ catch (error) {
191
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
192
+ }
193
+ const remaining = deadline - performance.now()
194
+ if (remaining > 0) await Bun.sleep(Math.min(50, remaining))
195
+ }
196
+ return null
197
+ }
198
+
177
199
  async function tryLaunch(browser: string): Promise<Session | null> {
178
200
  const userDataDir = join(tmpdir(), `stacks-browse-${process.pid}-${Math.floor(Number(process.hrtime.bigint() % 1000000n))}`)
179
201
  mkdirSync(userDataDir, { recursive: true })
@@ -196,18 +218,19 @@ async function tryLaunch(browser: string): Promise<Session | null> {
196
218
 
197
219
  // Chromium writes the chosen port to <user-data-dir>/DevToolsActivePort.
198
220
  const portFile = join(userDataDir, 'DevToolsActivePort')
199
- for (let i = 0; i < 80; i++) {
200
- if (existsSync(portFile)) {
201
- const line = readFileSync(portFile, 'utf8').split('\n')[0]?.trim()
202
- if (line)
203
- return { proc, port: Number(line), userDataDir, browser }
221
+ let port: number | null = null
222
+ try {
223
+ port = await waitForBrowserPort(proc, portFile, 15_000)
224
+ if (port !== null) return { proc, port, userDataDir, browser }
225
+ }
226
+ finally {
227
+ if (port === null) {
228
+ try { proc.kill() }
229
+ catch { /* ignore */ }
230
+ try { rmSync(userDataDir, { recursive: true, force: true }) }
231
+ catch { /* a failed launch must not mask the next browser candidate */ }
204
232
  }
205
- await Bun.sleep(50)
206
233
  }
207
- try { proc.kill() }
208
- catch { /* ignore */ }
209
- try { rmSync(userDataDir, { recursive: true, force: true }) }
210
- catch { /* a failed launch must not mask the next browser candidate */ }
211
234
  return null
212
235
  }
213
236
 
@@ -600,9 +623,12 @@ interface ScenarioStep {
600
623
  key?: string
601
624
  ms?: number
602
625
  settle?: number
626
+ timeoutMs?: number
603
627
  }
604
628
 
605
- function parseScenarioStep(value: string): ScenarioStep {
629
+ class ScenarioAssertionError extends Error {}
630
+
631
+ export function parseScenarioStep(value: string): ScenarioStep {
606
632
  let parsed: unknown
607
633
  try {
608
634
  parsed = JSON.parse(value)
@@ -633,11 +659,41 @@ function parseScenarioStep(value: string): ScenarioStep {
633
659
  throw new TypeError('Scenario action "press" requires a key.')
634
660
  if (action === 'wait' && (!Number.isFinite(step.ms) || Number(step.ms) < 0 || Number(step.ms) > 30_000))
635
661
  throw new TypeError('Scenario action "wait" requires ms between 0 and 30000.')
662
+ if (step.timeoutMs !== undefined && (action !== 'assert' || !Number.isSafeInteger(step.timeoutMs) || Number(step.timeoutMs) < 0 || Number(step.timeoutMs) > 30_000))
663
+ throw new TypeError('Scenario timeoutMs is supported only by assert actions and must be an integer between 0 and 30000.')
636
664
 
637
665
  return step as unknown as ScenarioStep
638
666
  }
639
667
 
640
- async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<string, unknown>> {
668
+ export async function runScenarioStep(cdp: Pick<Cdp, 'send'>, step: ScenarioStep): Promise<Record<string, unknown>> {
669
+ if (step.action === 'assert' && step.timeoutMs) {
670
+ const deadline = performance.now() + step.timeoutMs
671
+ let last: ScenarioAssertionError | undefined
672
+ const expired = () => new Error(`${last?.message || `Scenario assert failed (${step.selector || ''})`}; timed out after ${step.timeoutMs}ms`)
673
+ while (true) {
674
+ const budget = deadline - performance.now()
675
+ if (budget <= 0) throw expired()
676
+ let timeout: ReturnType<typeof setTimeout> | undefined
677
+ try {
678
+ const expiry = Promise.withResolvers<never>()
679
+ timeout = setTimeout(() => expiry.reject(expired()), budget)
680
+ const answer = await Promise.race([
681
+ runScenarioStep(cdp, { ...step, timeoutMs: 0 }),
682
+ expiry.promise,
683
+ ])
684
+ if (performance.now() > deadline) throw expired()
685
+ return answer
686
+ }
687
+ catch (error) {
688
+ if (!(error instanceof ScenarioAssertionError)) throw error
689
+ last = error
690
+ const remaining = deadline - performance.now()
691
+ if (remaining <= 0) throw error
692
+ await Bun.sleep(Math.min(50, remaining))
693
+ }
694
+ finally { if (timeout !== undefined) clearTimeout(timeout) }
695
+ }
696
+ }
641
697
  if (step.action === 'wait') {
642
698
  await Bun.sleep(Number(step.ms))
643
699
  return { action: step.action, ms: Number(step.ms), ok: true }
@@ -740,8 +796,11 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
740
796
  returnByValue: true,
741
797
  })
742
798
  const value = result.result?.value as Record<string, unknown> | undefined
743
- if (!value?.ok)
799
+ if (!value?.ok) {
800
+ if (step.action === 'assert' && value)
801
+ throw new ScenarioAssertionError(`Scenario assert failed: ${String(value.error || 'unknown error')} (${step.selector || ''})`)
744
802
  throw new Error(`Scenario ${step.action} failed: ${String(value?.error || 'unknown error')} (${step.selector || step.key || ''})`)
803
+ }
745
804
  if (step.action === 'click') {
746
805
  const point = value.clickPoint as { x: number, y: number }
747
806
  await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: point.x, y: point.y, button: 'left', clickCount: 1 })
@@ -1196,7 +1255,7 @@ async function main() {
1196
1255
  }
1197
1256
  }
1198
1257
 
1199
- main().catch((e) => {
1258
+ if (import.meta.main) main().catch((e) => {
1200
1259
  console.error('browse error:', e?.message || e)
1201
1260
  process.exit(1)
1202
1261
  })
@@ -508,6 +508,38 @@ History is chronological, with `has_more` and a `before` message-id cursor.
508
508
  Receipts acknowledge only messages through the id actually displayed.
509
509
  `MessagingError.status` provides safe HTTP statuses for application handlers.
510
510
 
511
+ ### Attachments
512
+
513
+ `send(id, body, retryKey, attachments?)` records files the application has
514
+ already stored. The trait keeps a reference, never bytes: `file` is the
515
+ application's own name for the upload (a disk path, a bucket key, an id), and
516
+ `type`, `size`, `width` and `height` are optional. Resolving and authorizing
517
+ the file stays with the application, so a disk, a bucket and a CDN look the
518
+ same from here.
519
+
520
+ ```ts
521
+ import type { DirectMessageAttachment } from '@stacksjs/chat'
522
+
523
+ const photo: DirectMessageAttachment = { file: upload.key, type: 'image/jpeg', size: upload.size, width: 1200, height: 900 }
524
+ await messenger.send(thread.id, '', retryKey, [photo])
525
+ ```
526
+
527
+ - A photo with no caption is a message: `body` may be empty when there is at
528
+ least one attachment, and only then.
529
+ - `message.attachments` is always an array, `[]` for text. Rows written before
530
+ the column existed read as `[]`.
531
+ - A retry key covers the attachments too. The same key with a different file
532
+ is a different message and returns 409, rather than silently keeping the
533
+ first photo.
534
+ - At most 10 attachments, 4000 characters once serialised. Keys the trait does
535
+ not know are dropped, so the column cannot become a second message body.
536
+ - The column is `chat_messages.attachments`, added by `buddy migrate` to a
537
+ database created before it. A database that has not migrated yet still sends
538
+ text.
539
+ - Notification previews are the application's to word: `onMessageSent`
540
+ receives the message with its attachments, so a photo with no caption can
541
+ read as "Sent a photo" rather than as an empty line.
542
+
511
543
  For the UI, STX ships `ChatInbox`, `ChatMessage`, and `ChatUnreadBadge` in
512
544
  `@stacksjs/components`. Browser stores import `createChat` from
513
545
  `@stacksjs/components/chat` and inject their authenticated HTTP transport.
@@ -18,10 +18,10 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
18
18
  | `@stacksjs/api` | [`stacks-api`](../stacks-api/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/api/src/index.ts) | 9 |
19
19
  | `@stacksjs/arrays` | [`stacks-arrays`](../stacks-arrays/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/arrays/src/index.ts) | 6 |
20
20
  | `@stacksjs/audio` | [`stacks-audio`](../stacks-audio/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/audio/src/index.ts) | 2 |
21
- | `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 119 |
21
+ | `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 120 |
22
22
  | `@stacksjs/browser` | [`stacks-browser`](../stacks-browser/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser/src/index.ts) | 8 |
23
23
  | `@stacksjs/browser-extension` | [`stacks-browser-extension`](../stacks-browser-extension/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser-extension/src/index.ts) | 7 |
24
- | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 161 |
24
+ | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 164 |
25
25
  | `@stacksjs/build` | [`stacks-build`](../stacks-build/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/build/src/index.ts) | 3 |
26
26
  | `@stacksjs/cache` | [`stacks-cache`](../stacks-cache/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/cache/src/index.ts) | 14 |
27
27
  | `@stacksjs/calendar-api` | [`stacks-calendar`](../stacks-calendar/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/calendar-api/src/index.ts) | 2 |
@@ -65,7 +65,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
65
65
  | `@stacksjs/newsletter` | [`stacks-newsletter`](../stacks-newsletter/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/newsletter/src/index.ts) | 7 |
66
66
  | `@stacksjs/notifications` | [`stacks-notifications`](../stacks-notifications/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/index.ts) | 16 |
67
67
  | `@stacksjs/objects` | [`stacks-objects`](../stacks-objects/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/objects/src/index.ts) | 4 |
68
- | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 71 |
68
+ | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 72 |
69
69
  | `@stacksjs/pagination` | [`stacks-pagination`](../stacks-pagination/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/pagination/src/index.ts) | 0 |
70
70
  | `@stacksjs/path` | [`stacks-path`](../stacks-path/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/src/index.ts) | 13 |
71
71
  | `@stacksjs/payments` | [`stacks-payments`](../stacks-payments/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/index.ts) | 28 |
@@ -79,7 +79,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
79
79
  | `@stacksjs/scheduler` | [`stacks-scheduler`](../stacks-scheduler/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/scheduler/src/index.ts) | 14 |
80
80
  | `@stacksjs/search-engine` | [`stacks-search-engine`](../stacks-search-engine/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/search-engine/src/index.ts) | 10 |
81
81
  | `@stacksjs/security` | [`stacks-security`](../stacks-security/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/security/src/index.ts) | 11 |
82
- | `@stacksjs/server` | [`stacks-server`](../stacks-server/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/index.ts) | 25 |
82
+ | `@stacksjs/server` | [`stacks-server`](../stacks-server/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/index.ts) | 26 |
83
83
  | `@stacksjs/shell` | [`stacks-shell`](../stacks-shell/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/shell/src/index.ts) | 1 |
84
84
  | `@stacksjs/sites` | [`stacks-sites`](../stacks-sites/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/sites/src/index.ts) | 3 |
85
85
  | `@stacksjs/skills` | [`stacks-skills`](../stacks-skills/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/skills/src/index.ts) | 1 |
@@ -185,7 +185,7 @@ Declared runtime exports: `DEFAULT_ROLE_PACKS`, `createBqbRbacStore`, `generateT
185
185
 
186
186
  Local source modules: [src/authentication.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authentication.ts), [src/authenticator.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authenticator.ts), [src/authorizable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authorizable.ts), [src/browser-session.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/browser-session.ts), [src/client.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/client.ts), [src/cookie-auth.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/cookie-auth.ts), [src/credential-user.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/credential-user.ts), [src/email-verification.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/email-verification.ts), [src/gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/gate.ts), [src/magic-link.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/magic-link.ts), [src/middleware.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/middleware.ts), [src/oauth-authorization-codes.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-codes.ts), [src/oauth-authorization-endpoint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-endpoint.ts), [src/oauth-authorization-requests.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-requests.ts), [src/oauth-authorization-response.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-response.ts), [src/oauth-authorization.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization.ts), [src/oauth-browser-session.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-browser-session.ts), [src/oauth-client-registration.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-client-registration.ts), [src/oauth-consent.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-consent.ts), [src/oauth-delegated-access.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-delegated-access.ts), [src/oauth-dynamic-registration.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-dynamic-registration.ts), [src/oauth-grants.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-grants.ts), [src/oauth-introspection.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-introspection.ts), [src/oauth-metadata.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-metadata.ts), [src/oauth-pkce.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-pkce.ts), [src/oauth-protected-resource.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-protected-resource.ts), [src/oauth-provider.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-provider.ts), [src/oauth-pruning.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-pruning.ts), [src/oauth-revocation.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-revocation.ts), [src/oauth-server.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-server.ts), [src/oauth-token-endpoint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-endpoint.ts), [src/oauth-token-exchange.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-exchange.ts), [src/oauth-token-request.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-request.ts), [src/oauth-token-response.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-response.ts), [src/page-gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/page-gate.ts), [src/passkey.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/passkey.ts), [src/passkeys.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/passkeys.ts), [src/password/reset.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/password/reset.ts), [src/permissions.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/permissions.ts), [src/policy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/policy.ts), [src/rate-limiter.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rate-limiter.ts), [src/rbac-seed.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac-seed.ts), [src/rbac-store-bqb.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac-store-bqb.ts), [src/rbac.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac.ts), [src/referrals.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/referrals.ts), [src/register.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/register.ts), [src/request-token.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/request-token.ts), [src/session-auth.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/session-auth.ts), [src/session-handoff.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/session-handoff.ts), [src/socials.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/socials.ts), [src/team.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/team.ts), [src/tokens.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/tokens.ts), [src/two-factor.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/two-factor.ts), [src/user.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/user.ts).
187
187
 
188
- Evidence directory: [119 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
188
+ Evidence directory: [120 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
189
189
 
190
190
  ### @stacksjs/browser
191
191
 
@@ -215,7 +215,7 @@ Read the entrypoint and package export map for re-exported or type-only surfaces
215
215
 
216
216
  Local source modules: [src/commands/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/commands/index.ts).
217
217
 
218
- Evidence directory: [161 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
218
+ Evidence directory: [164 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
219
219
 
220
220
  ### @stacksjs/build
221
221
 
@@ -633,7 +633,7 @@ Declared runtime exports: `Activity`, `AnalyticsEvent`, `Auction`, `AuctionItem`
633
633
 
634
634
  Local source modules: [src/auto-crud.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/auto-crud.ts), [src/batch-loader.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/batch-loader.ts), [src/db.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/db.ts), [src/define-model.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/define-model.ts), [src/extend-model.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/extend-model.ts), [src/gdpr/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/gdpr/index.ts), [src/generate-database-schema.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/generate-database-schema.ts), [src/locked-row.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/locked-row.ts), [src/messaging.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/messaging.ts), [src/model-types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/model-types.ts), [src/ownership.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/ownership.ts), [src/paginator-request.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/paginator-request.ts), [src/row-token.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/row-token.ts), [src/subquery.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/subquery.ts), [src/traits/audit.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/audit.ts), [src/traits/billable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/billable.ts), [src/traits/categorizable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/categorizable.ts), [src/traits/commentable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/commentable.ts), [src/traits/likeable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/likeable.ts), [src/traits/taggable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/taggable.ts), [src/transaction.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/transaction.ts), [src/types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/types.ts), [src/utils/encrypted.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/utils/encrypted.ts), [src/utils/prunable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/utils/prunable.ts).
635
635
 
636
- Evidence directory: [71 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
636
+ Evidence directory: [72 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
637
637
 
638
638
  ### @stacksjs/pagination
639
639
 
@@ -763,7 +763,7 @@ Declared runtime exports: `server`.
763
763
 
764
764
  Local source modules: [src/config.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/config.ts), [src/controllers/base.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/controllers/base.ts), [src/imports.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/imports.ts), [src/maintenance.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/maintenance.ts), [src/proxy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/proxy.ts), [src/redirects.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/redirects.ts), [src/rewrites.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/rewrites.ts), [src/security-headers.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/security-headers.ts), [src/shadowed-defaults.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/shadowed-defaults.ts).
765
765
 
766
- Evidence directory: [25 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
766
+ Evidence directory: [26 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
767
767
 
768
768
  ### @stacksjs/shell
769
769
 
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.263",
5
+ "version": "0.75.267",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.263",
5
+ "version": "0.75.267",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -45,9 +45,9 @@
45
45
  "dependencies": {
46
46
  "@iconify-json/f7": "^1.2.2",
47
47
  "@iconify-json/hugeicons": "^1.2.35",
48
- "@stacksjs/mobile": "0.75.263",
49
- "@stacksjs/sanitizer": "0.2.661",
50
- "@stacksjs/stx": "0.2.661",
48
+ "@stacksjs/mobile": "0.75.267",
49
+ "@stacksjs/sanitizer": "0.2.664",
50
+ "@stacksjs/stx": "0.2.664",
51
51
  "@xterm/addon-fit": "^0.11.0",
52
52
  "@xterm/xterm": "^6.0.0",
53
53
  "ts-qr-codes": "^0.1.8"
@@ -1,3 +1,5 @@
1
+ @import('../UI/Table')
2
+
1
3
  <script client>
2
4
  import { dashboardApi } from '../../../../functions/dashboard-api'
3
5
 
@@ -67,29 +69,75 @@ const filteredRequests = derived(() => {
67
69
  })
68
70
  })
69
71
 
70
- function formatDate(value: string): string {
71
- if (!value)
72
- return 'Unknown'
73
- const date = new Date(value)
74
- return Number.isNaN(date.getTime()) ? 'Unknown' : date.toLocaleString()
75
- }
76
-
77
- function statusClass(status: number): string {
72
+ /**
73
+ * Pill colours by tone rather than by value, so every status code and every
74
+ * method the table has not named still lands on a colour, as the hand-rolled
75
+ * pills did.
76
+ */
77
+ function statusTone(status: number): string {
78
78
  if (status >= 500)
79
- return 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/40'
79
+ return 'server'
80
80
  if (status >= 400)
81
- return 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40'
81
+ return 'client'
82
82
  if (status >= 300)
83
- return 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40'
84
- return 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/40'
83
+ return 'redirect'
84
+ return 'ok'
85
85
  }
86
86
 
87
- function methodClass(method: string): string {
87
+ function methodTone(method: string): string {
88
88
  if (method === 'GET')
89
- return 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40'
89
+ return 'read'
90
90
  if (method === 'DELETE')
91
- return 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/40'
92
- return 'text-violet-700 dark:text-violet-300 bg-violet-50 dark:bg-violet-950/40'
91
+ return 'delete'
92
+ return 'write'
93
+ }
94
+
95
+ const COLUMNS = [
96
+ {
97
+ key: 'methodTone',
98
+ label: 'Method',
99
+ type: 'badge' as const,
100
+ sortable: true,
101
+ badgeLabelKey: 'method',
102
+ badgeClasses: {
103
+ read: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40',
104
+ delete: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/40',
105
+ write: 'text-violet-700 dark:text-violet-300 bg-violet-50 dark:bg-violet-950/40',
106
+ },
107
+ },
108
+ { key: 'path', label: 'Path', sortable: true, class: 'max-w-md font-mono text-neutral-800 text-xs truncate dark:text-neutral-200' },
109
+ {
110
+ key: 'statusTone',
111
+ label: 'Status',
112
+ type: 'badge' as const,
113
+ sortable: true,
114
+ badgeLabelKey: 'status',
115
+ badgeClasses: {
116
+ server: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/40',
117
+ client: 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40',
118
+ redirect: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40',
119
+ ok: 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/40',
120
+ },
121
+ },
122
+ { key: 'durationMs', label: 'Duration (ms)', type: 'number' as const, sortable: true, align: 'right' as const, class: 'font-mono whitespace-nowrap' },
123
+ { key: 'ipAddress', label: 'Client', sortable: true, emptyText: 'Not recorded' },
124
+ { key: 'createdAt', label: 'Recorded', type: 'datetime' as const, sortable: true, align: 'right' as const, emptyText: 'Unknown', class: 'whitespace-nowrap' },
125
+ ]
126
+
127
+ const ROW_ACTIONS = [
128
+ { action: 'inspect', label: 'Inspect request', icon: 'i-hugeicons-view' },
129
+ ]
130
+
131
+ const requestRows = derived(() => filteredRequests().map(request => ({
132
+ ...request,
133
+ methodTone: methodTone(request.method),
134
+ statusTone: statusTone(request.status),
135
+ durationMs: Math.round(request.durationMs * 10) / 10,
136
+ })))
137
+
138
+ function handleRowAction(payload: { action: string, row: RequestRow }): void {
139
+ if (payload.action === 'inspect')
140
+ inspectRequest(payload.row)
93
141
  }
94
142
 
95
143
  function inspectRequest(request: RequestRow): void {
@@ -184,49 +232,18 @@ onMount(() => {
184
232
  </div>
185
233
  </div>
186
234
 
187
- <div :if="loading()" class="flex items-center justify-center py-16 text-neutral-500 text-sm dark:text-neutral-400">
188
- <span class="mr-2 h-5 w-5 animate-spin i-hugeicons-loading-03"></span>
189
- Loading request records...
190
- </div>
191
-
192
- <EmptyState
193
- :if="!loading() && !loadError() && filteredRequests().length === 0"
194
- icon="i-hugeicons-global"
195
- :title="search() || methodFilter() !== 'all' || statusFilter() !== 'all' ? 'No matching requests' : 'No requests recorded'"
196
- description="Request records appear after the application writes to the Request model."
235
+ <Table
236
+ :columns="COLUMNS"
237
+ :data="requestRows()"
238
+ :loading="loading()"
239
+ :row-actions="ROW_ACTIONS"
240
+ action-label-key="path"
241
+ paginated
242
+ :page-size="25"
243
+ :empty-title="search() || methodFilter() !== 'all' || statusFilter() !== 'all' ? 'No matching requests' : 'No requests recorded'"
244
+ empty-description="Request records appear after the application writes to the Request model."
245
+ @row-action="handleRowAction"
197
246
  />
198
-
199
- <div :if="!loading() && filteredRequests().length > 0" class="overflow-x-auto">
200
- <table class="w-full">
201
- <thead class="bg-neutral-50 dark:bg-neutral-800/50">
202
- <tr>
203
- <th class="pl-5 pr-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Request</th>
204
- <th class="px-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Status</th>
205
- <th class="px-4 py-3 font-semibold text-neutral-500 text-right text-xs tracking-wider uppercase dark:text-neutral-400">Duration</th>
206
- <th class="px-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Client</th>
207
- <th class="px-4 py-3 font-semibold text-neutral-500 text-right text-xs tracking-wider uppercase dark:text-neutral-400">Recorded</th>
208
- <th class="pr-5 py-3 font-semibold text-neutral-500 text-right text-xs tracking-wider uppercase dark:text-neutral-400">Details</th>
209
- </tr>
210
- </thead>
211
- <tbody class="divide-neutral-100 divide-y dark:divide-neutral-700">
212
- <tr :for="request in filteredRequests()">
213
- <td class="pl-5 pr-4 py-3.5">
214
- <div class="flex gap-2 items-center">
215
- <span :class="'inline-flex px-2 py-0.5 font-medium text-xs rounded ' + methodClass(request.method)">{{ request.method }}</span>
216
- <code class="max-w-md text-neutral-800 text-xs truncate dark:text-neutral-200">{{ request.path }}</code>
217
- </div>
218
- </td>
219
- <td class="px-4 py-3.5"><span :class="'inline-flex px-2 py-0.5 font-medium text-xs rounded ' + statusClass(request.status)">{{ request.status }}</span></td>
220
- <td class="px-4 py-3.5 font-mono text-neutral-600 text-right text-xs whitespace-nowrap dark:text-neutral-300">{{ request.durationMs.toFixed(1) }} ms</td>
221
- <td class="px-4 py-3.5 text-neutral-500 text-xs dark:text-neutral-400">{{ request.ipAddress || 'Not recorded' }}</td>
222
- <td class="px-4 py-3.5 text-neutral-500 text-right text-xs whitespace-nowrap dark:text-neutral-400">{{ formatDate(request.createdAt) }}</td>
223
- <td class="pr-5 py-3.5 text-right">
224
- <Button variant="ghost" size="sm" @click="inspectRequest(request)">Inspect</Button>
225
- </td>
226
- </tr>
227
- </tbody>
228
- </table>
229
- </div>
230
247
  </Card>
231
248
 
232
249
  <RequestDetailsDialog :request="selectedRequest()" @close="closeInspector" />