@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.
- package/ai/skills/stacks-browse/SKILL.md +5 -1
- package/ai/skills/stacks-browse/scripts/browse.ts +61 -6
- package/app/Actions/Dashboard/Actions/GetActions.ts +15 -9
- package/app/Actions/Dashboard/Analytics/EventStoreAction.ts +21 -9
- package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +12 -3
- package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +12 -3
- package/app/Actions/Dashboard/DashboardHealthAction.ts +34 -14
- package/app/Actions/Dashboard/DashboardHomeAction.test.ts +1 -1
- package/app/Actions/Dashboard/DashboardHomeAction.ts +31 -4
- package/app/Actions/Dashboard/DashboardStatsAction.ts +6 -1
- package/app/Actions/Dashboard/Email/InboxSendAction.ts +14 -7
- package/app/Actions/Dashboard/Infrastructure/CommandIndexAction.ts +33 -27
- package/app/Actions/Dashboard/Infrastructure/EnvironmentIndexAction.ts +10 -4
- package/app/Actions/Dashboard/Infrastructure/EnvironmentUpdateAction.ts +8 -1
- package/app/Actions/Dashboard/Infrastructure/RequestIndexAction.ts +24 -18
- package/app/Actions/Dashboard/Infrastructure/ServerIndexAction.ts +15 -9
- package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +9 -2
- package/app/Actions/Dashboard/Marketing/CampaignCancelAction.ts +23 -5
- package/app/Actions/Dashboard/Marketing/CampaignDestroyAction.ts +26 -17
- package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +49 -43
- package/app/Actions/Dashboard/Marketing/CampaignScheduleAction.ts +31 -25
- package/app/Actions/Dashboard/Marketing/CampaignSendAction.ts +28 -22
- package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +15 -10
- package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +19 -12
- package/app/Actions/Dashboard/Marketing/ListDestroyAction.ts +34 -25
- package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +32 -26
- package/app/Actions/Dashboard/Marketing/ListStoreAction.ts +31 -18
- package/app/Actions/Dashboard/Marketing/ListUpdateAction.ts +35 -19
- package/app/Actions/Dashboard/Marketing/SocialPostDestroyAction.ts +15 -5
- package/app/Actions/Dashboard/Marketing/SocialPostIndexAction.ts +11 -5
- package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +19 -13
- package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +22 -12
- package/app/Actions/Dashboard/Marketing/campaign-delivery.ts +34 -0
- package/app/Actions/Dashboard/Marketing/marketing-list-records.test.ts +10 -0
- package/app/Actions/Dashboard/Marketing/marketing-list-records.ts +10 -0
- package/app/Actions/Dashboard/Marketing/marketing-response.ts +28 -0
- package/app/Actions/Dashboard/Marketing/social-post-records.test.ts +13 -0
- package/app/Actions/Dashboard/Marketing/social-post-records.ts +22 -4
- package/app/Actions/Dashboard/Models/GetModels.ts +14 -7
- package/app/Actions/Dashboard/Models/GetSubscriberCount.ts +8 -1
- package/app/Actions/Dashboard/Models/GetUserCount.ts +8 -1
- package/app/Actions/Dashboard/Releases/ReleaseIndexAction.ts +11 -5
- package/app/Actions/Dashboard/Settings/MailSettingsGetAction.ts +10 -4
- package/app/Actions/Dashboard/Settings/MailSettingsUpdateAction.ts +8 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/UI/Modal.stx +60 -10
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
+
try {
|
|
12
|
+
const items = await discoverActionSources(process.cwd())
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
})
|
|
@@ -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.
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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}`,
|