@stacksjs/defaults 0.70.357 → 0.70.358

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 (48) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +5 -1
  2. package/ai/skills/stacks-browse/scripts/browse.ts +61 -6
  3. package/app/Actions/Dashboard/Actions/GetActions.ts +15 -9
  4. package/app/Actions/Dashboard/Analytics/EventStoreAction.ts +21 -9
  5. package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +12 -3
  6. package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +12 -3
  7. package/app/Actions/Dashboard/DashboardHealthAction.ts +34 -14
  8. package/app/Actions/Dashboard/DashboardHomeAction.test.ts +1 -1
  9. package/app/Actions/Dashboard/DashboardHomeAction.ts +31 -4
  10. package/app/Actions/Dashboard/DashboardStatsAction.ts +6 -1
  11. package/app/Actions/Dashboard/Email/InboxSendAction.ts +14 -7
  12. package/app/Actions/Dashboard/Infrastructure/CommandIndexAction.ts +33 -27
  13. package/app/Actions/Dashboard/Infrastructure/EnvironmentIndexAction.ts +10 -4
  14. package/app/Actions/Dashboard/Infrastructure/EnvironmentUpdateAction.ts +8 -1
  15. package/app/Actions/Dashboard/Infrastructure/RequestIndexAction.ts +24 -18
  16. package/app/Actions/Dashboard/Infrastructure/ServerIndexAction.ts +15 -9
  17. package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +9 -2
  18. package/app/Actions/Dashboard/Marketing/CampaignCancelAction.ts +23 -5
  19. package/app/Actions/Dashboard/Marketing/CampaignDestroyAction.ts +26 -17
  20. package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +49 -43
  21. package/app/Actions/Dashboard/Marketing/CampaignScheduleAction.ts +31 -25
  22. package/app/Actions/Dashboard/Marketing/CampaignSendAction.ts +28 -22
  23. package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +15 -10
  24. package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +19 -12
  25. package/app/Actions/Dashboard/Marketing/ListDestroyAction.ts +34 -25
  26. package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +32 -26
  27. package/app/Actions/Dashboard/Marketing/ListStoreAction.ts +31 -18
  28. package/app/Actions/Dashboard/Marketing/ListUpdateAction.ts +35 -19
  29. package/app/Actions/Dashboard/Marketing/SocialPostDestroyAction.ts +15 -5
  30. package/app/Actions/Dashboard/Marketing/SocialPostIndexAction.ts +11 -5
  31. package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +19 -13
  32. package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +22 -12
  33. package/app/Actions/Dashboard/Marketing/campaign-delivery.ts +34 -0
  34. package/app/Actions/Dashboard/Marketing/marketing-list-records.test.ts +10 -0
  35. package/app/Actions/Dashboard/Marketing/marketing-list-records.ts +10 -0
  36. package/app/Actions/Dashboard/Marketing/marketing-response.ts +28 -0
  37. package/app/Actions/Dashboard/Marketing/social-post-records.test.ts +13 -0
  38. package/app/Actions/Dashboard/Marketing/social-post-records.ts +22 -4
  39. package/app/Actions/Dashboard/Models/GetModels.ts +14 -7
  40. package/app/Actions/Dashboard/Models/GetSubscriberCount.ts +8 -1
  41. package/app/Actions/Dashboard/Models/GetUserCount.ts +8 -1
  42. package/app/Actions/Dashboard/Releases/ReleaseIndexAction.ts +11 -5
  43. package/app/Actions/Dashboard/Settings/MailSettingsGetAction.ts +10 -4
  44. package/app/Actions/Dashboard/Settings/MailSettingsUpdateAction.ts +8 -1
  45. package/ide/vscode/package.json +1 -1
  46. package/package.json +1 -1
  47. package/resources/components/Dashboard/UI/Modal.stx +60 -10
  48. package/views/dashboard/stores/auth.ts +18 -2
@@ -100,13 +100,17 @@ 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
@@ -497,12 +497,14 @@ function parseViewport(value: string | undefined): { w: number, h: number } {
497
497
  return { w, h }
498
498
  }
499
499
 
500
- type ScenarioAction = 'assert' | 'click' | 'fill' | 'press' | 'wait'
500
+ type ScenarioAction = 'assert' | 'click' | 'fill' | 'focus' | 'press' | 'wait'
501
501
 
502
502
  interface ScenarioStep {
503
503
  action: ScenarioAction
504
504
  selector?: string
505
505
  text?: string
506
+ absent?: boolean
507
+ focused?: boolean
506
508
  value?: string
507
509
  key?: string
508
510
  ms?: number
@@ -522,14 +524,18 @@ function parseScenarioStep(value: string): ScenarioStep {
522
524
  throw new TypeError('Each scenario step must be a JSON object.')
523
525
 
524
526
  const step = parsed as Record<string, unknown>
525
- if (!['assert', 'click', 'fill', 'press', 'wait'].includes(String(step.action)))
527
+ if (!['assert', 'click', 'fill', 'focus', 'press', 'wait'].includes(String(step.action)))
526
528
  throw new TypeError(`Unsupported scenario action: ${String(step.action)}`)
527
529
 
528
530
  const action = step.action as ScenarioAction
529
- if (['assert', 'click', 'fill'].includes(action) && typeof step.selector !== 'string')
531
+ if (['assert', 'click', 'fill', 'focus'].includes(action) && typeof step.selector !== 'string')
530
532
  throw new TypeError(`Scenario action "${action}" requires a selector.`)
531
533
  if (action === 'fill' && typeof step.value !== 'string')
532
534
  throw new TypeError('Scenario action "fill" requires a string value.')
535
+ if (step.absent !== undefined && (action !== 'assert' || typeof step.absent !== 'boolean'))
536
+ throw new TypeError('Scenario "absent" is a boolean supported only by assert actions.')
537
+ if (step.focused !== undefined && (action !== 'assert' || typeof step.focused !== 'boolean'))
538
+ throw new TypeError('Scenario "focused" is a boolean supported only by assert actions.')
533
539
  if (action === 'press' && typeof step.key !== 'string')
534
540
  throw new TypeError('Scenario action "press" requires a key.')
535
541
  if (action === 'wait' && (!Number.isFinite(step.ms) || Number(step.ms) < 0 || Number(step.ms) > 30_000))
@@ -557,23 +563,36 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
557
563
  const step = ${JSON.stringify(step)}
558
564
  const candidates = Array.from(document.querySelectorAll(step.selector))
559
565
  const normalizedText = (element) => (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim()
560
- const element = step.action === 'click' && step.text !== undefined
566
+ const element = (step.action === 'click' || step.action === 'focus' || step.action === 'assert') && step.text !== undefined
561
567
  ? candidates.find(candidate => normalizedText(candidate) === step.text)
562
568
  || candidates.find(candidate => normalizedText(candidate).includes(step.text))
563
569
  : candidates[0]
570
+ if (!element && step.action === 'assert' && step.absent)
571
+ return { ok: true, action: step.action, selector: step.selector, absent: true }
564
572
  if (!element)
565
573
  return { ok: false, error: 'Element not found', selector: step.selector }
566
574
 
567
575
  const rect = element.getBoundingClientRect()
568
576
  const style = getComputedStyle(element)
569
577
  const visible = rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none'
578
+ if (step.action === 'assert' && step.absent) {
579
+ if (visible)
580
+ return { ok: false, error: 'Expected element to be absent', selector: step.selector }
581
+ return { ok: true, action: step.action, selector: step.selector, absent: true }
582
+ }
570
583
  if (!visible)
571
584
  return { ok: false, error: 'Element is not visible', selector: step.selector }
572
585
 
573
586
  if (step.action === 'click') {
574
587
  if (element.disabled || element.getAttribute('aria-disabled') === 'true')
575
588
  return { ok: false, error: 'Element is disabled', selector: step.selector }
576
- element.click()
589
+ }
590
+ else if (step.action === 'focus') {
591
+ if (typeof element.focus !== 'function')
592
+ return { ok: false, error: 'Element cannot receive focus', selector: step.selector }
593
+ element.focus()
594
+ if (document.activeElement !== element)
595
+ return { ok: false, error: 'Element did not receive focus', selector: step.selector }
577
596
  }
578
597
  else if (step.action === 'fill') {
579
598
  if (!('value' in element))
@@ -593,12 +612,19 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
593
612
  return { ok: false, error: 'Expected text was not found', selector: step.selector, expected: step.text, actual: content.slice(0, 240) }
594
613
  }
595
614
 
615
+ if (step.action === 'assert' && step.focused !== undefined) {
616
+ const focused = document.activeElement === element
617
+ if (focused !== step.focused)
618
+ return { ok: false, error: step.focused ? 'Expected element to be focused' : 'Expected element not to be focused', selector: step.selector }
619
+ }
620
+
596
621
  return {
597
622
  ok: true,
598
623
  action: step.action,
599
624
  selector: step.selector,
600
625
  tag: element.tagName.toLowerCase(),
601
626
  text: normalizedText(element).slice(0, 160),
627
+ clickPoint: step.action === 'click' ? { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 } : undefined,
602
628
  }
603
629
  })()`,
604
630
  returnByValue: true,
@@ -606,6 +632,12 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
606
632
  const value = result.result?.value as Record<string, unknown> | undefined
607
633
  if (!value?.ok)
608
634
  throw new Error(`Scenario ${step.action} failed: ${String(value?.error || 'unknown error')} (${step.selector || step.key || ''})`)
635
+ if (step.action === 'click') {
636
+ const point = value.clickPoint as { x: number, y: number }
637
+ await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: point.x, y: point.y, button: 'left', clickCount: 1 })
638
+ await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: point.x, y: point.y, button: 'left', clickCount: 1 })
639
+ delete value.clickPoint
640
+ }
609
641
  return value
610
642
  }
611
643
 
@@ -741,6 +773,20 @@ async function main() {
741
773
  const viewport = parseViewport(typeof flags.viewport === 'string' ? flags.viewport : undefined)
742
774
  const state = await gotoAndInstrument(cdp, url, { viewport, cookies, settleMs, scheme })
743
775
  const results: Record<string, unknown>[] = []
776
+ await cdp.send('Page.bringToFront')
777
+ await cdp.send('Runtime.evaluate', {
778
+ expression: `(() => {
779
+ window.__browseFocusHistory = []
780
+ const describe = (element) => element ? {
781
+ tag: element.tagName?.toLowerCase() || null,
782
+ role: element.getAttribute?.('role') || null,
783
+ ariaLabel: element.getAttribute?.('aria-label') || null,
784
+ text: (element.innerText || element.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 80),
785
+ } : null
786
+ document.addEventListener('focusin', event => window.__browseFocusHistory.push({ event: 'focusin', target: describe(event.target) }), true)
787
+ document.addEventListener('focusout', event => window.__browseFocusHistory.push({ event: 'focusout', target: describe(event.target), related: describe(event.relatedTarget) }), true)
788
+ })()`,
789
+ })
744
790
 
745
791
  for (const step of steps) {
746
792
  results.push(await runScenarioStep(cdp, step))
@@ -752,7 +798,15 @@ async function main() {
752
798
  expression: `({
753
799
  url: location.href,
754
800
  title: document.title,
755
- activeElement: document.activeElement?.tagName?.toLowerCase() || null,
801
+ focusHistory: window.__browseFocusHistory || [],
802
+ activeElement: document.activeElement ? {
803
+ tag: document.activeElement.tagName?.toLowerCase() || null,
804
+ id: document.activeElement.id || null,
805
+ role: document.activeElement.getAttribute?.('role') || null,
806
+ ariaLabel: document.activeElement.getAttribute?.('aria-label') || null,
807
+ ref: document.activeElement.getAttribute?.('data-stx-ref') || null,
808
+ text: (document.activeElement.innerText || document.activeElement.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 120),
809
+ } : null,
756
810
  bodyText: (document.body.innerText || '').replace(/\\s+/g, ' ').trim().slice(0, 500),
757
811
  })`,
758
812
  returnByValue: true,
@@ -770,6 +824,7 @@ async function main() {
770
824
  steps: results,
771
825
  final: finalState.result?.value,
772
826
  screenshot,
827
+ consoleMessages: state.console,
773
828
  consoleErrors: state.consoleErrors,
774
829
  failedRequests,
775
830
  }, null, 2))
@@ -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
 
@@ -2,6 +2,7 @@ import { Action } from '@stacksjs/actions'
2
2
  import { Order, Post, Product, Request, User } from '@stacksjs/orm'
3
3
  import { checkApplicationHealth, type ApplicationHealthCheck } from '@stacksjs/router'
4
4
  import { formatRelative, safeGet } from '../../../resources/functions/dashboard/data'
5
+ import { dashboardOperationalIssue } from './dashboard-response'
5
6
 
6
7
  interface HttpRequestSample {
7
8
  duration: number
@@ -28,7 +29,7 @@ export function serializeHealthCheck(name: string, check: ApplicationHealthCheck
28
29
  name: name.charAt(0).toUpperCase() + name.slice(1),
29
30
  status: check.ok ? 'healthy' : 'critical',
30
31
  latency: `${check.ms}ms`,
31
- detail: check.message || '',
32
+ detail: check.ok ? '' : 'Dependency probe failed.',
32
33
  }
33
34
  }
34
35
 
@@ -43,7 +44,11 @@ function issue(source: string, result: PromiseSettledResult<unknown>) {
43
44
  return result.status === 'rejected'
44
45
  ? {
45
46
  source,
46
- message: result.reason instanceof Error ? result.reason.message : 'Query failed.',
47
+ message: dashboardOperationalIssue(
48
+ result.reason,
49
+ `${source} data could not be loaded.`,
50
+ `DashboardHomeAction.${source.toLowerCase().replaceAll(' ', '-')}`,
51
+ ),
47
52
  }
48
53
  : null
49
54
  }
@@ -65,7 +70,7 @@ export default new Action({
65
70
  Request.count(),
66
71
  Request.orderBy('created_at', 'desc').limit(1000).get(),
67
72
  ])
68
- const health = await checkApplicationHealth()
73
+ const healthResult = await Promise.allSettled([checkApplicationHealth()])
69
74
 
70
75
  const [
71
76
  userCount,
@@ -93,7 +98,19 @@ export default new Action({
93
98
  })))
94
99
  : summarizeHttpRequests(0, [])
95
100
 
96
- const services = Object.entries(health.checks).map(([name, check]) => serializeHealthCheck(name, check))
101
+ const health = healthResult[0]
102
+ const services = health.status === 'fulfilled'
103
+ ? Object.entries(health.value.checks).map(([name, check]) => {
104
+ if (!check.ok) {
105
+ dashboardOperationalIssue(
106
+ check.message,
107
+ 'Dependency probe failed.',
108
+ `DashboardHomeAction.health.${name}`,
109
+ )
110
+ }
111
+ return serializeHealthCheck(name, check)
112
+ })
113
+ : []
97
114
 
98
115
  const activities = [
99
116
  ...(recentOrders.status === 'fulfilled' ? recentOrders.value : []).map((order: any) => ({
@@ -127,6 +144,16 @@ export default new Action({
127
144
  const issues = modelResults
128
145
  .map((result, index) => issue(sources[index], result))
129
146
  .filter((entry): entry is { source: string, message: string } => entry !== null)
147
+ if (health.status === 'rejected') {
148
+ issues.push({
149
+ source: 'System health',
150
+ message: dashboardOperationalIssue(
151
+ health.reason,
152
+ 'System health could not be loaded.',
153
+ 'DashboardHomeAction.health',
154
+ ),
155
+ })
156
+ }
130
157
 
131
158
  return { stats, httpMetrics, services, activities, issues }
132
159
  },
@@ -1,5 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { Customer, Order, Post, User } from '@stacksjs/orm'
3
+ import { dashboardOperationalIssue } from './dashboard-response'
3
4
 
4
5
  interface DashboardStatDefinition {
5
6
  title: string
@@ -61,7 +62,11 @@ export default new Action({
61
62
  const issues = results.flatMap((result, index) => result.status === 'rejected'
62
63
  ? [{
63
64
  source: definitions[index].title,
64
- message: result.reason instanceof Error ? result.reason.message : 'Model query failed.',
65
+ message: dashboardOperationalIssue(
66
+ result.reason,
67
+ `${definitions[index].title} data could not be loaded.`,
68
+ `DashboardStatsAction.${definitions[index].title.toLowerCase().replaceAll(' ', '-')}`,
69
+ ),
65
70
  }]
66
71
  : [])
67
72
 
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
3
3
  import { notify } from '@stacksjs/notifications'
4
4
  import { response } from '@stacksjs/router'
5
5
  import { schema } from '@stacksjs/validation'
6
+ import { dashboardOperationalError } from '../dashboard-response'
6
7
 
7
8
  export default new Action({
8
9
  name: 'InboxSendAction',
@@ -29,15 +30,21 @@ export default new Action({
29
30
  const subject = String(request.get('subject') || '').trim()
30
31
  const body = String(request.get('body') || '').trim()
31
32
 
32
- const [result] = await notify(
33
- { email: to },
34
- { subject, body, data: { source: 'dashboard-inbox' } },
35
- ['email'],
36
- { ignorePreferences: true },
37
- )
33
+ let result
34
+ try {
35
+ [result] = await notify(
36
+ { email: to },
37
+ { subject, body, data: { source: 'dashboard-inbox' } },
38
+ ['email'],
39
+ { ignorePreferences: true },
40
+ )
41
+ }
42
+ catch (error) {
43
+ return dashboardOperationalError(error, 'The email could not be sent.', 'InboxSendAction.provider', 502)
44
+ }
38
45
 
39
46
  if (!result?.success)
40
- return response.json({ message: result?.error?.message || 'The email could not be sent.' }, 502)
47
+ return dashboardOperationalError(result?.error, 'The email could not be sent.', 'InboxSendAction.result', 502)
41
48
 
42
49
  return response.json({ success: true })
43
50
  },
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, statSync } from 'node:fs'
3
3
  import { join, relative } from 'node:path'
4
4
  import process from 'node:process'
5
5
  import { parseCommandSource } from '../Source/source-inventory'
6
+ import { dashboardOperationalError } from '../dashboard-response'
6
7
 
7
8
  interface CommandConfig {
8
9
  file: string
@@ -15,35 +16,40 @@ export default new Action({
15
16
  description: 'Lists commands registered by the application.',
16
17
  method: 'GET',
17
18
  async handle() {
18
- const projectRoot = process.cwd()
19
- const registryPath = join(projectRoot, 'app/Commands.ts')
20
- const registryModule = await import(registryPath)
21
- const registry = (registryModule.default || {}) as Record<string, string | CommandConfig>
22
- const items = Object.entries(registry).flatMap(([signature, value]) => {
23
- const config = typeof value === 'string'
24
- ? { file: value, enabled: true, aliases: [] }
25
- : { enabled: true, aliases: [], ...value }
26
- const file = join(projectRoot, 'app/Commands', `${config.file}.ts`)
27
- if (!existsSync(file))
28
- return []
19
+ try {
20
+ const projectRoot = process.cwd()
21
+ const registryPath = join(projectRoot, 'app/Commands.ts')
22
+ const registryModule = await import(registryPath)
23
+ const registry = (registryModule.default || {}) as Record<string, string | CommandConfig>
24
+ const items = Object.entries(registry).flatMap(([signature, value]) => {
25
+ const config = typeof value === 'string'
26
+ ? { file: value, enabled: true, aliases: [] }
27
+ : { enabled: true, aliases: [], ...value }
28
+ const file = join(projectRoot, 'app/Commands', `${config.file}.ts`)
29
+ if (!existsSync(file))
30
+ return []
29
31
 
30
- return [parseCommandSource(
31
- readFileSync(file, 'utf8'),
32
- relative(projectRoot, file),
33
- signature,
34
- config.aliases,
35
- statSync(file).mtime.toISOString(),
36
- )]
37
- })
32
+ return [parseCommandSource(
33
+ readFileSync(file, 'utf8'),
34
+ relative(projectRoot, file),
35
+ signature,
36
+ config.aliases,
37
+ statSync(file).mtime.toISOString(),
38
+ )]
39
+ })
38
40
 
39
- return {
40
- items,
41
- stats: {
42
- total: items.length,
43
- aliases: items.reduce((sum, item) => sum + (item.aliases?.length || 0), 0),
44
- options: items.reduce((sum, item) => sum + (item.options?.length || 0), 0),
45
- registered: Object.keys(registry).length,
46
- },
41
+ return {
42
+ items,
43
+ stats: {
44
+ total: items.length,
45
+ aliases: items.reduce((sum, item) => sum + (item.aliases?.length || 0), 0),
46
+ options: items.reduce((sum, item) => sum + (item.options?.length || 0), 0),
47
+ registered: Object.keys(registry).length,
48
+ },
49
+ }
50
+ }
51
+ catch (error) {
52
+ return dashboardOperationalError(error, 'Command sources could not be loaded.', 'CommandIndexAction')
47
53
  }
48
54
  },
49
55
  })
@@ -1,4 +1,5 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { dashboardOperationalError } from '../dashboard-response'
2
3
  import { readEnvironmentFile } from './environment-file'
3
4
 
4
5
  export default new Action({
@@ -8,9 +9,14 @@ export default new Action({
8
9
  apiResponse: true,
9
10
 
10
11
  async handle() {
11
- return Response.json(
12
- { environment: await readEnvironmentFile() },
13
- { headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' } },
14
- )
12
+ try {
13
+ return Response.json(
14
+ { environment: await readEnvironmentFile() },
15
+ { headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' } },
16
+ )
17
+ }
18
+ catch (error) {
19
+ return dashboardOperationalError(error, 'Environment file could not be loaded.', 'EnvironmentIndexAction')
20
+ }
15
21
  },
16
22
  })
@@ -1,6 +1,7 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { response } from '@stacksjs/router'
4
+ import { dashboardOperationalError } from '../dashboard-response'
4
5
  import { updateEnvironmentFile } from './environment-file'
5
6
 
6
7
  export default new Action({
@@ -18,7 +19,13 @@ export default new Action({
18
19
  if (typeof revision !== 'string' || !/^[a-f0-9]{64}$/.test(revision))
19
20
  return response.json({ message: 'A valid environment revision is required.' }, 422)
20
21
 
21
- const result = await updateEnvironmentFile(content, revision)
22
+ let result
23
+ try {
24
+ result = await updateEnvironmentFile(content, revision)
25
+ }
26
+ catch (error) {
27
+ return dashboardOperationalError(error, 'Environment file could not be saved.', 'EnvironmentUpdateAction', 500)
28
+ }
22
29
  if (result.issues?.length) {
23
30
  const errors = Object.fromEntries(result.issues.map((issue, index) => [
24
31
  issue.line ? String(issue.line) : `file-${index}`,