@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.
- package/ai/skills/stacks-browse/SKILL.md +10 -5
- package/ai/skills/stacks-browse/scripts/browse.ts +109 -13
- package/app/Actions/AI/AskAction.ts +12 -17
- package/app/Actions/AI/SummaryAction.ts +12 -17
- 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/app/Actions/LogAction.ts +11 -11
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/Marketing/CampaignDeleteDialog.stx +11 -12
- package/resources/components/Dashboard/Marketing/MarketingListDeleteDialog.stx +15 -15
- package/resources/components/Dashboard/Marketing/SocialPostDeleteDialog.stx +11 -12
- package/resources/components/Dashboard/UI/ConfirmDialog.stx +51 -53
- package/resources/components/Dashboard/UI/Modal.stx +83 -29
- 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.
|
|
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,
|
|
116
|
-
session
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
975
|
-
|
|
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:
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
25
|
+
try {
|
|
26
|
+
return response.json({
|
|
29
27
|
data: await ask(question),
|
|
30
|
-
}
|
|
28
|
+
})
|
|
31
29
|
}
|
|
32
30
|
catch (error) {
|
|
33
|
-
|
|
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:
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
25
|
+
try {
|
|
26
|
+
return response.json({
|
|
29
27
|
data: await summarize(text),
|
|
30
|
-
}
|
|
28
|
+
})
|
|
31
29
|
}
|
|
32
30
|
catch (error) {
|
|
33
|
-
|
|
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
|
-
|
|
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
|
})
|