@stacksjs/defaults 0.70.357 → 0.70.362

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 (55) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +10 -5
  2. package/ai/skills/stacks-browse/scripts/browse.ts +109 -13
  3. package/app/Actions/AI/AskAction.ts +12 -17
  4. package/app/Actions/AI/SummaryAction.ts +12 -17
  5. package/app/Actions/Dashboard/Actions/GetActions.ts +15 -9
  6. package/app/Actions/Dashboard/Analytics/EventStoreAction.ts +21 -9
  7. package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +12 -3
  8. package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +12 -3
  9. package/app/Actions/Dashboard/DashboardHealthAction.ts +34 -14
  10. package/app/Actions/Dashboard/DashboardHomeAction.test.ts +1 -1
  11. package/app/Actions/Dashboard/DashboardHomeAction.ts +31 -4
  12. package/app/Actions/Dashboard/DashboardStatsAction.ts +6 -1
  13. package/app/Actions/Dashboard/Email/InboxSendAction.ts +14 -7
  14. package/app/Actions/Dashboard/Infrastructure/CommandIndexAction.ts +33 -27
  15. package/app/Actions/Dashboard/Infrastructure/EnvironmentIndexAction.ts +10 -4
  16. package/app/Actions/Dashboard/Infrastructure/EnvironmentUpdateAction.ts +8 -1
  17. package/app/Actions/Dashboard/Infrastructure/RequestIndexAction.ts +24 -18
  18. package/app/Actions/Dashboard/Infrastructure/ServerIndexAction.ts +15 -9
  19. package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +9 -2
  20. package/app/Actions/Dashboard/Marketing/CampaignCancelAction.ts +23 -5
  21. package/app/Actions/Dashboard/Marketing/CampaignDestroyAction.ts +26 -17
  22. package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +49 -43
  23. package/app/Actions/Dashboard/Marketing/CampaignScheduleAction.ts +31 -25
  24. package/app/Actions/Dashboard/Marketing/CampaignSendAction.ts +28 -22
  25. package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +15 -10
  26. package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +19 -12
  27. package/app/Actions/Dashboard/Marketing/ListDestroyAction.ts +34 -25
  28. package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +32 -26
  29. package/app/Actions/Dashboard/Marketing/ListStoreAction.ts +31 -18
  30. package/app/Actions/Dashboard/Marketing/ListUpdateAction.ts +35 -19
  31. package/app/Actions/Dashboard/Marketing/SocialPostDestroyAction.ts +15 -5
  32. package/app/Actions/Dashboard/Marketing/SocialPostIndexAction.ts +11 -5
  33. package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +19 -13
  34. package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +22 -12
  35. package/app/Actions/Dashboard/Marketing/campaign-delivery.ts +34 -0
  36. package/app/Actions/Dashboard/Marketing/marketing-list-records.test.ts +10 -0
  37. package/app/Actions/Dashboard/Marketing/marketing-list-records.ts +10 -0
  38. package/app/Actions/Dashboard/Marketing/marketing-response.ts +28 -0
  39. package/app/Actions/Dashboard/Marketing/social-post-records.test.ts +13 -0
  40. package/app/Actions/Dashboard/Marketing/social-post-records.ts +22 -4
  41. package/app/Actions/Dashboard/Models/GetModels.ts +14 -7
  42. package/app/Actions/Dashboard/Models/GetSubscriberCount.ts +8 -1
  43. package/app/Actions/Dashboard/Models/GetUserCount.ts +8 -1
  44. package/app/Actions/Dashboard/Releases/ReleaseIndexAction.ts +11 -5
  45. package/app/Actions/Dashboard/Settings/MailSettingsGetAction.ts +10 -4
  46. package/app/Actions/Dashboard/Settings/MailSettingsUpdateAction.ts +8 -1
  47. package/app/Actions/LogAction.ts +11 -11
  48. package/ide/vscode/package.json +1 -1
  49. package/package.json +1 -1
  50. package/resources/components/Dashboard/Marketing/CampaignDeleteDialog.stx +11 -12
  51. package/resources/components/Dashboard/Marketing/MarketingListDeleteDialog.stx +15 -15
  52. package/resources/components/Dashboard/Marketing/SocialPostDeleteDialog.stx +11 -12
  53. package/resources/components/Dashboard/UI/ConfirmDialog.stx +51 -53
  54. package/resources/components/Dashboard/UI/Modal.stx +83 -29
  55. package/views/dashboard/stores/auth.ts +18 -2
@@ -100,20 +100,24 @@ Extracts headings, links (`text -> href`), buttons, forms (action + field count)
100
100
  ```bash
101
101
  bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts scenario <url> \
102
102
  --step '{"action":"click","selector":"button[data-open]"}' \
103
+ --step '{"action":"focus","selector":"input[name=name]"}' \
103
104
  --step '{"action":"fill","selector":"input[name=name]","value":"Example"}' \
104
105
  --step '{"action":"click","selector":"button[type=submit]"}' \
105
106
  --step '{"action":"assert","selector":"main","text":"Saved"}' \
107
+ --step '{"action":"assert","selector":"[role=dialog]","absent":true}' \
106
108
  --out storage/framework/runtime/shots/scenario.png
107
109
  ```
108
110
 
109
- Steps run in order within one isolated browser page, preserving reactive STX state and SPA navigation. Supported actions are `click`, `fill`, `press`, `wait`, and `assert`. Each step is a JSON object passed through a repeatable `--step` flag. A click step may include `text` to select the matching control from its CSS selector. The command reports every completed step, the final URL and page text, console exceptions, failed requests, and an optional screenshot. It exits nonzero when the scenario assertion, browser console, or network fails.
111
+ Steps run in order within one isolated browser page, preserving reactive STX state and SPA navigation. Supported actions are `click`, `fill`, `focus`, `press`, `wait`, and `assert`. Each step is a JSON object passed through a repeatable `--step` flag. Click and focus steps may include `text` to select the matching control from their CSS selector. The command reports every completed step, the final URL and page text, console messages, console exceptions, failed requests, and an optional screenshot. It exits nonzero when the scenario assertion, browser console, or network fails.
112
+ Use `{"action":"assert","selector":"...","absent":true}` to verify that an element is missing or hidden after an interaction.
113
+ Use `{"action":"assert","selector":"button","text":"Save","focused":true}` to verify keyboard focus and focus restoration.
110
114
 
111
115
  ### Crawl (whole-site browser audit)
112
116
  ```bash
113
- bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts crawl <url> [--viewport 1280x900] [--max 500] [--path /extra-route] [--settle 350] [--progress]
117
+ bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts crawl <url> [--viewport 1280x900] [--max 500] [--path /extra-route] [--settle 350] [--progress] [--summary]
114
118
  ```
115
- Uses a fresh isolated page target per route, relaunching Chromium if its DevTools
116
- session exits during a long audit, while
119
+ Uses a fresh isolated page target per route, waits for each target to fully close,
120
+ and relaunches Chromium if its DevTools session or target lifecycle fails during a long audit, while
117
121
  following every same-origin link it discovers.
118
122
  The viewport is deterministic and defaults to `1280x900`; pass `--viewport 768x1024`
119
123
  to repeat the same route audit at a tablet breakpoint.
@@ -121,7 +125,8 @@ Each page is checked for a non-200 document, console exceptions, failed
121
125
  requests, and horizontal overflow. Repeat `--path` to seed routes that are not
122
126
  linked from the starting page. The command prints every crawled path and exits
123
127
  nonzero when any page fails. Add `--progress` to stream each completed page
124
- during a long audit.
128
+ during a long audit. Add `--summary` to report totals and compact failure
129
+ diagnostics without printing every visited path or overflowing element.
125
130
 
126
131
  ## Stacks-Specific QA
127
132
 
@@ -264,12 +264,38 @@ async function createPage(port: number): Promise<BrowserPage> {
264
264
  }
265
265
  }
266
266
 
267
- async function closePage(port: number, page: BrowserPage): Promise<void> {
268
- page.cdp.close()
267
+ async function closePage(port: number, page: BrowserPage): Promise<boolean> {
269
268
  try {
270
- await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(page.targetId)}`)
269
+ const response = await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(page.targetId)}`)
270
+ if (!response.ok) {
271
+ page.cdp.close()
272
+ return false
273
+ }
274
+ }
275
+ catch {
276
+ page.cdp.close()
277
+ return false
278
+ }
279
+ page.cdp.close()
280
+
281
+ // Chromium acknowledges target closure before every renderer/helper has
282
+ // actually left the target list. Creating the next target immediately can
283
+ // accumulate closing renderers across a large crawl, eventually dropping
284
+ // Network and Runtime events. Wait for the target to disappear so each route
285
+ // starts with the isolation the command promises.
286
+ for (let attempt = 0; attempt < 40; attempt++) {
287
+ try {
288
+ const targets = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json() as any[]
289
+ if (!targets.some(target => target.id === page.targetId))
290
+ return true
291
+ }
292
+ catch {
293
+ return false
294
+ }
295
+ await Bun.sleep(25)
271
296
  }
272
- catch { /* the target or browser may already have closed */ }
297
+
298
+ return false
273
299
  }
274
300
 
275
301
  function kill(s: Session): void {
@@ -497,12 +523,14 @@ function parseViewport(value: string | undefined): { w: number, h: number } {
497
523
  return { w, h }
498
524
  }
499
525
 
500
- type ScenarioAction = 'assert' | 'click' | 'fill' | 'press' | 'wait'
526
+ type ScenarioAction = 'assert' | 'click' | 'fill' | 'focus' | 'press' | 'wait'
501
527
 
502
528
  interface ScenarioStep {
503
529
  action: ScenarioAction
504
530
  selector?: string
505
531
  text?: string
532
+ absent?: boolean
533
+ focused?: boolean
506
534
  value?: string
507
535
  key?: string
508
536
  ms?: number
@@ -522,14 +550,18 @@ function parseScenarioStep(value: string): ScenarioStep {
522
550
  throw new TypeError('Each scenario step must be a JSON object.')
523
551
 
524
552
  const step = parsed as Record<string, unknown>
525
- if (!['assert', 'click', 'fill', 'press', 'wait'].includes(String(step.action)))
553
+ if (!['assert', 'click', 'fill', 'focus', 'press', 'wait'].includes(String(step.action)))
526
554
  throw new TypeError(`Unsupported scenario action: ${String(step.action)}`)
527
555
 
528
556
  const action = step.action as ScenarioAction
529
- if (['assert', 'click', 'fill'].includes(action) && typeof step.selector !== 'string')
557
+ if (['assert', 'click', 'fill', 'focus'].includes(action) && typeof step.selector !== 'string')
530
558
  throw new TypeError(`Scenario action "${action}" requires a selector.`)
531
559
  if (action === 'fill' && typeof step.value !== 'string')
532
560
  throw new TypeError('Scenario action "fill" requires a string value.')
561
+ if (step.absent !== undefined && (action !== 'assert' || typeof step.absent !== 'boolean'))
562
+ throw new TypeError('Scenario "absent" is a boolean supported only by assert actions.')
563
+ if (step.focused !== undefined && (action !== 'assert' || typeof step.focused !== 'boolean'))
564
+ throw new TypeError('Scenario "focused" is a boolean supported only by assert actions.')
533
565
  if (action === 'press' && typeof step.key !== 'string')
534
566
  throw new TypeError('Scenario action "press" requires a key.')
535
567
  if (action === 'wait' && (!Number.isFinite(step.ms) || Number(step.ms) < 0 || Number(step.ms) > 30_000))
@@ -557,23 +589,36 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
557
589
  const step = ${JSON.stringify(step)}
558
590
  const candidates = Array.from(document.querySelectorAll(step.selector))
559
591
  const normalizedText = (element) => (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim()
560
- const element = step.action === 'click' && step.text !== undefined
592
+ const element = (step.action === 'click' || step.action === 'focus' || step.action === 'assert') && step.text !== undefined
561
593
  ? candidates.find(candidate => normalizedText(candidate) === step.text)
562
594
  || candidates.find(candidate => normalizedText(candidate).includes(step.text))
563
595
  : candidates[0]
596
+ if (!element && step.action === 'assert' && step.absent)
597
+ return { ok: true, action: step.action, selector: step.selector, absent: true }
564
598
  if (!element)
565
599
  return { ok: false, error: 'Element not found', selector: step.selector }
566
600
 
567
601
  const rect = element.getBoundingClientRect()
568
602
  const style = getComputedStyle(element)
569
603
  const visible = rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none'
604
+ if (step.action === 'assert' && step.absent) {
605
+ if (visible)
606
+ return { ok: false, error: 'Expected element to be absent', selector: step.selector }
607
+ return { ok: true, action: step.action, selector: step.selector, absent: true }
608
+ }
570
609
  if (!visible)
571
610
  return { ok: false, error: 'Element is not visible', selector: step.selector }
572
611
 
573
612
  if (step.action === 'click') {
574
613
  if (element.disabled || element.getAttribute('aria-disabled') === 'true')
575
614
  return { ok: false, error: 'Element is disabled', selector: step.selector }
576
- element.click()
615
+ }
616
+ else if (step.action === 'focus') {
617
+ if (typeof element.focus !== 'function')
618
+ return { ok: false, error: 'Element cannot receive focus', selector: step.selector }
619
+ element.focus()
620
+ if (document.activeElement !== element)
621
+ return { ok: false, error: 'Element did not receive focus', selector: step.selector }
577
622
  }
578
623
  else if (step.action === 'fill') {
579
624
  if (!('value' in element))
@@ -593,12 +638,19 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
593
638
  return { ok: false, error: 'Expected text was not found', selector: step.selector, expected: step.text, actual: content.slice(0, 240) }
594
639
  }
595
640
 
641
+ if (step.action === 'assert' && step.focused !== undefined) {
642
+ const focused = document.activeElement === element
643
+ if (focused !== step.focused)
644
+ return { ok: false, error: step.focused ? 'Expected element to be focused' : 'Expected element not to be focused', selector: step.selector }
645
+ }
646
+
596
647
  return {
597
648
  ok: true,
598
649
  action: step.action,
599
650
  selector: step.selector,
600
651
  tag: element.tagName.toLowerCase(),
601
652
  text: normalizedText(element).slice(0, 160),
653
+ clickPoint: step.action === 'click' ? { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 } : undefined,
602
654
  }
603
655
  })()`,
604
656
  returnByValue: true,
@@ -606,6 +658,12 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
606
658
  const value = result.result?.value as Record<string, unknown> | undefined
607
659
  if (!value?.ok)
608
660
  throw new Error(`Scenario ${step.action} failed: ${String(value?.error || 'unknown error')} (${step.selector || step.key || ''})`)
661
+ if (step.action === 'click') {
662
+ const point = value.clickPoint as { x: number, y: number }
663
+ await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: point.x, y: point.y, button: 'left', clickCount: 1 })
664
+ await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: point.x, y: point.y, button: 'left', clickCount: 1 })
665
+ delete value.clickPoint
666
+ }
609
667
  return value
610
668
  }
611
669
 
@@ -741,6 +799,20 @@ async function main() {
741
799
  const viewport = parseViewport(typeof flags.viewport === 'string' ? flags.viewport : undefined)
742
800
  const state = await gotoAndInstrument(cdp, url, { viewport, cookies, settleMs, scheme })
743
801
  const results: Record<string, unknown>[] = []
802
+ await cdp.send('Page.bringToFront')
803
+ await cdp.send('Runtime.evaluate', {
804
+ expression: `(() => {
805
+ window.__browseFocusHistory = []
806
+ const describe = (element) => element ? {
807
+ tag: element.tagName?.toLowerCase() || null,
808
+ role: element.getAttribute?.('role') || null,
809
+ ariaLabel: element.getAttribute?.('aria-label') || null,
810
+ text: (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 80),
811
+ } : null
812
+ document.addEventListener('focusin', event => window.__browseFocusHistory.push({ event: 'focusin', target: describe(event.target) }), true)
813
+ document.addEventListener('focusout', event => window.__browseFocusHistory.push({ event: 'focusout', target: describe(event.target), related: describe(event.relatedTarget) }), true)
814
+ })()`,
815
+ })
744
816
 
745
817
  for (const step of steps) {
746
818
  results.push(await runScenarioStep(cdp, step))
@@ -752,7 +824,15 @@ async function main() {
752
824
  expression: `({
753
825
  url: location.href,
754
826
  title: document.title,
755
- activeElement: document.activeElement?.tagName?.toLowerCase() || null,
827
+ focusHistory: window.__browseFocusHistory || [],
828
+ activeElement: document.activeElement ? {
829
+ tag: document.activeElement.tagName?.toLowerCase() || null,
830
+ id: document.activeElement.id || null,
831
+ role: document.activeElement.getAttribute?.('role') || null,
832
+ ariaLabel: document.activeElement.getAttribute?.('aria-label') || null,
833
+ ref: document.activeElement.getAttribute?.('data-stx-ref') || null,
834
+ text: (document.activeElement.innerText || document.activeElement.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 120),
835
+ } : null,
756
836
  bodyText: (document.body.innerText || '').replace(/\\s+/g, ' ').trim().slice(0, 500),
757
837
  })`,
758
838
  returnByValue: true,
@@ -770,6 +850,7 @@ async function main() {
770
850
  steps: results,
771
851
  final: finalState.result?.value,
772
852
  screenshot,
853
+ consoleMessages: state.console,
773
854
  consoleErrors: state.consoleErrors,
774
855
  failedRequests,
775
856
  }, null, 2))
@@ -809,7 +890,11 @@ async function main() {
809
890
  }
810
891
  }
811
892
  const replacePage = async (): Promise<void> => {
812
- await closePage(session.port, browserPage)
893
+ const closed = await closePage(session.port, browserPage)
894
+ if (!closed) {
895
+ kill(session)
896
+ session = await launch()
897
+ }
813
898
  browserPage = await createReplacementPage()
814
899
  cdp = browserPage.cdp
815
900
  }
@@ -964,6 +1049,16 @@ async function main() {
964
1049
  || page.consoleErrors.length > 0
965
1050
  || page.failedRequests.length > 0
966
1051
  || page.horizontalOverflowPx > 0)
1052
+ const summaryOnly = Boolean(flags.summary)
1053
+ const reportedFailures = summaryOnly
1054
+ ? failures.map(page => ({
1055
+ path: page.path,
1056
+ status: page.status,
1057
+ consoleErrorCount: page.consoleErrors.length,
1058
+ failedRequestCount: page.failedRequests.length,
1059
+ horizontalOverflowPx: page.horizontalOverflowPx,
1060
+ }))
1061
+ : failures
967
1062
  console.log(JSON.stringify({
968
1063
  start: start.href,
969
1064
  browser: session.browser,
@@ -971,8 +1066,9 @@ async function main() {
971
1066
  crawled: pages.length,
972
1067
  remaining: queue.length,
973
1068
  max,
974
- paths: pages.map(page => page.path),
975
- failures,
1069
+ failureCount: failures.length,
1070
+ ...(!summaryOnly ? { paths: pages.map(page => page.path) } : {}),
1071
+ failures: reportedFailures,
976
1072
  }, null, 2))
977
1073
  if (failures.length > 0)
978
1074
  process.exitCode = 1
@@ -1,16 +1,15 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { ask } from '@stacksjs/ai'
4
+ import { log } from '@stacksjs/logging'
5
+ import { response } from '@stacksjs/router'
3
6
  import { schema } from '@stacksjs/validation'
4
7
 
5
- // TODO: this should have been auto-generated
6
- interface Request {
7
- question: string
8
- }
9
-
10
8
  export default new Action({
11
9
  name: 'AiAskAction',
12
10
  description: 'Ask AI',
13
11
  method: 'POST',
12
+ apiResponse: true,
14
13
 
15
14
  validations: {
16
15
  question: {
@@ -19,22 +18,18 @@ export default new Action({
19
18
  },
20
19
  },
21
20
 
22
- async handle(request: Request) {
23
- try {
24
- const question = request.question
25
-
26
- console.log(`Question received: ${question}`)
21
+ async handle(request: RequestInstance) {
22
+ await request.validate()
23
+ const question = String(request.get('question') || '').trim()
27
24
 
28
- return {
25
+ try {
26
+ return response.json({
29
27
  data: await ask(question),
30
- }
28
+ })
31
29
  }
32
30
  catch (error) {
33
- console.error('Error:', error)
34
-
35
- return {
36
- error: 'An error occurred while processing your request.',
37
- }
31
+ log.error('[ai] Ask request failed', { error })
32
+ return response.json({ message: 'The AI provider could not answer the question.' }, 502)
38
33
  }
39
34
  },
40
35
  })
@@ -1,16 +1,15 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { summarize } from '@stacksjs/ai'
4
+ import { log } from '@stacksjs/logging'
5
+ import { response } from '@stacksjs/router'
3
6
  import { schema } from '@stacksjs/validation'
4
7
 
5
- // TODO: this should have been auto-generated
6
- interface Request {
7
- text: string
8
- }
9
-
10
8
  export default new Action({
11
9
  name: 'AiSummaryAction',
12
10
  description: 'Summary AI',
13
11
  method: 'POST',
12
+ apiResponse: true,
14
13
 
15
14
  validations: {
16
15
  text: {
@@ -19,22 +18,18 @@ export default new Action({
19
18
  },
20
19
  },
21
20
 
22
- async handle(request: Request) {
23
- try {
24
- const text = request.text
25
-
26
- console.log(`Text received: ${text}`)
21
+ async handle(request: RequestInstance) {
22
+ await request.validate()
23
+ const text = String(request.get('text') || '').trim()
27
24
 
28
- return {
25
+ try {
26
+ return response.json({
29
27
  data: await summarize(text),
30
- }
28
+ })
31
29
  }
32
30
  catch (error) {
33
- console.error('Error:', error)
34
-
35
- return {
36
- error: 'An error occurred while processing your request.',
37
- }
31
+ log.error('[ai] Summary request failed', { error })
32
+ return response.json({ message: 'The AI provider could not summarize the text.' }, 502)
38
33
  }
39
34
  },
40
35
  })
@@ -1,22 +1,28 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import process from 'node:process'
3
3
  import { discoverActionSources } from '../Source/source-inventory'
4
+ import { dashboardOperationalError } from '../dashboard-response'
4
5
 
5
6
  export default new Action({
6
7
  name: 'GetActions',
7
8
  description: 'Lists application and framework Actions from their native source files.',
8
9
  method: 'GET',
9
10
  async handle() {
10
- const items = await discoverActionSources(process.cwd())
11
+ try {
12
+ const items = await discoverActionSources(process.cwd())
11
13
 
12
- return {
13
- items,
14
- stats: {
15
- total: items.length,
16
- application: items.filter(item => item.origin === 'Application').length,
17
- framework: items.filter(item => item.origin === 'Framework').length,
18
- writes: items.filter(item => !['ANY', 'GET', 'HEAD', 'OPTIONS'].includes(item.method || 'ANY')).length,
19
- },
14
+ return {
15
+ items,
16
+ stats: {
17
+ total: items.length,
18
+ application: items.filter(item => item.origin === 'Application').length,
19
+ framework: items.filter(item => item.origin === 'Framework').length,
20
+ writes: items.filter(item => !['ANY', 'GET', 'HEAD', 'OPTIONS'].includes(item.method || 'ANY')).length,
21
+ },
22
+ }
23
+ }
24
+ catch (error) {
25
+ return dashboardOperationalError(error, 'Action sources could not be loaded.', 'GetActions')
20
26
  }
21
27
  },
22
28
  })
@@ -1,7 +1,8 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { AnalyticsEvent } from '@stacksjs/orm'
3
+ import { AnalyticsEvent, ModelValidationError } from '@stacksjs/orm'
4
4
  import { response } from '@stacksjs/router'
5
+ import { dashboardOperationalError } from '../dashboard-response'
5
6
 
6
7
  function token(value: unknown, fallback: string): string {
7
8
  const normalized = String(value || '')
@@ -46,14 +47,25 @@ export default new Action({
46
47
  if (properties.length > 10_000)
47
48
  return response.json({ message: 'Properties must be 10,000 characters or fewer.' }, 422)
48
49
 
49
- await AnalyticsEvent.create({
50
- name,
51
- category,
52
- path,
53
- value,
54
- currency,
55
- properties,
56
- })
50
+ try {
51
+ await AnalyticsEvent.create({
52
+ name,
53
+ category,
54
+ path,
55
+ value,
56
+ currency,
57
+ properties,
58
+ })
59
+ }
60
+ catch (error) {
61
+ if (error instanceof ModelValidationError) {
62
+ return response.json({
63
+ message: 'Validation failed.',
64
+ errors: error.errors,
65
+ }, 422)
66
+ }
67
+ return dashboardOperationalError(error, 'Analytics event could not be recorded.', 'EventStoreAction', 500)
68
+ }
57
69
 
58
70
  return response.json({ success: true }, 201)
59
71
  },
@@ -2,6 +2,7 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { products } from '@stacksjs/commerce'
4
4
  import { response } from '@stacksjs/router'
5
+ import { dashboardOperationalError } from '../dashboard-response'
5
6
 
6
7
  export default new Action({
7
8
  name: 'ProductUnitDefaultAction',
@@ -11,11 +12,19 @@ export default new Action({
11
12
 
12
13
  async handle(request: RequestInstance) {
13
14
  const id = Number(request.getParam('id'))
15
+ if (!Number.isSafeInteger(id) || id <= 0)
16
+ return response.json({ error: 'A valid product unit id is required.' }, 400)
17
+ if (typeof request.get('isDefault') !== 'boolean')
18
+ return response.json({ error: 'isDefault must be a boolean.' }, 422)
14
19
  const isDefault = request.boolean('isDefault')
15
- if (!Number.isFinite(id) || id <= 0)
16
- return response.notFound({ error: 'Product unit not found' })
17
20
 
18
- const updated = await products.units.updateDefaultStatus(id, isDefault)
21
+ let updated
22
+ try {
23
+ updated = await products.units.updateDefaultStatus(id, isDefault)
24
+ }
25
+ catch (error) {
26
+ return dashboardOperationalError(error, 'Product unit default could not be updated.', 'ProductUnitDefaultAction', 500)
27
+ }
19
28
  if (!updated)
20
29
  return response.notFound({ error: 'Product unit not found' })
21
30
 
@@ -2,6 +2,7 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { tax } from '@stacksjs/commerce'
4
4
  import { response } from '@stacksjs/router'
5
+ import { dashboardOperationalError } from '../dashboard-response'
5
6
 
6
7
  export default new Action({
7
8
  name: 'TaxRateDefaultAction',
@@ -11,11 +12,19 @@ export default new Action({
11
12
 
12
13
  async handle(request: RequestInstance) {
13
14
  const id = Number(request.getParam('id'))
15
+ if (!Number.isSafeInteger(id) || id <= 0)
16
+ return response.json({ error: 'A valid tax rate id is required.' }, 400)
17
+ if (typeof request.get('isDefault') !== 'boolean')
18
+ return response.json({ error: 'isDefault must be a boolean.' }, 422)
14
19
  const isDefault = request.boolean('isDefault')
15
- if (!Number.isFinite(id) || id <= 0)
16
- return response.notFound({ error: 'Tax rate not found' })
17
20
 
18
- const updated = await tax.updateDefaultStatus(id, isDefault)
21
+ let updated
22
+ try {
23
+ updated = await tax.updateDefaultStatus(id, isDefault)
24
+ }
25
+ catch (error) {
26
+ return dashboardOperationalError(error, 'Tax rate default could not be updated.', 'TaxRateDefaultAction', 500)
27
+ }
19
28
  if (!updated)
20
29
  return response.notFound({ error: 'Tax rate not found' })
21
30
 
@@ -1,6 +1,7 @@
1
1
  import process from 'node:process'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { checkApplicationHealth } from '@stacksjs/router'
4
+ import { dashboardOperationalError, dashboardOperationalIssue } from './dashboard-response'
4
5
 
5
6
  export default new Action({
6
7
  name: 'DashboardHealthAction',
@@ -10,21 +11,40 @@ export default new Action({
10
11
 
11
12
  async handle() {
12
13
  const startedAt = performance.now()
13
- const health = await checkApplicationHealth()
14
- const memory = process.memoryUsage()
14
+ try {
15
+ const health = await checkApplicationHealth()
16
+ const memory = process.memoryUsage()
17
+ const checks = Object.fromEntries(Object.entries(health.checks).map(([name, check]) => {
18
+ if (!check.ok) {
19
+ dashboardOperationalIssue(
20
+ check.message,
21
+ 'Dependency probe failed.',
22
+ `DashboardHealthAction.${name}`,
23
+ )
24
+ }
25
+ return [name, {
26
+ ...check,
27
+ message: check.ok ? undefined : 'Dependency probe failed.',
28
+ }]
29
+ }))
15
30
 
16
- return {
17
- ...health,
18
- durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
19
- runtime: {
20
- bunVersion: process.versions.bun || '',
21
- nodeVersion: process.versions.node || '',
22
- platform: process.platform,
23
- architecture: process.arch,
24
- uptimeSeconds: Math.max(0, Math.floor(process.uptime())),
25
- residentMemoryBytes: memory.rss,
26
- heapUsedBytes: memory.heapUsed,
27
- },
31
+ return {
32
+ ...health,
33
+ checks,
34
+ durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
35
+ runtime: {
36
+ bunVersion: process.versions.bun || '',
37
+ nodeVersion: process.versions.node || '',
38
+ platform: process.platform,
39
+ architecture: process.arch,
40
+ uptimeSeconds: Math.max(0, Math.floor(process.uptime())),
41
+ residentMemoryBytes: memory.rss,
42
+ heapUsedBytes: memory.heapUsed,
43
+ },
44
+ }
45
+ }
46
+ catch (error) {
47
+ return dashboardOperationalError(error, 'System health could not be loaded.', 'DashboardHealthAction')
28
48
  }
29
49
  },
30
50
  })
@@ -28,7 +28,7 @@ describe('dashboard HTTP metrics', () => {
28
28
  name: 'Cache',
29
29
  status: 'critical',
30
30
  latency: '1500ms',
31
- detail: 'timeout',
31
+ detail: 'Dependency probe failed.',
32
32
  })
33
33
  })
34
34