@stacksjs/defaults 0.75.263 → 0.75.266
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 +3 -0
- package/ai/skills/stacks-browse/scripts/browse.ts +73 -14
- package/ai/skills/stacks-native/CATALOG.md +4 -4
- package/ide/vscode/package.json +1 -1
- package/package.json +4 -4
- package/resources/components/Dashboard/App/RequestsOverview.stx +75 -58
- package/resources/components/Dashboard/App/SourceInventory.stx +68 -59
- package/resources/components/Dashboard/Auth/AccessTokens.stx +57 -37
- package/resources/components/Dashboard/Billing/BillingTransactionHistory.stx +27 -33
- package/resources/components/Dashboard/Commerce/CommercePaymentsTable.stx +85 -49
- package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +63 -66
- package/resources/components/Dashboard/Commerce/CommerceVariantsTable.stx +67 -23
- package/resources/components/Dashboard/Content/BlogDashboard.stx +73 -67
- package/resources/components/Dashboard/Content/FileManagerDashboard.stx +39 -12
- package/resources/components/Dashboard/Data/DataRecords.stx +85 -88
- package/resources/components/Dashboard/Deployments/DeploymentTable.stx +49 -64
- package/resources/components/Dashboard/Infrastructure/DnsDashboard.stx +46 -28
- package/resources/components/Dashboard/Operations/RecoveryPointTable.stx +72 -61
- package/resources/components/Dashboard/Operations/SchedulerTaskTable.stx +74 -70
- package/resources/components/Dashboard/Queue/QueueDashboard.stx +27 -61
- package/resources/components/Dashboard/Releases/ReleaseDashboard.stx +35 -61
- package/resources/components/Dashboard/Teams/TeamPeopleDashboard.stx +127 -143
|
@@ -37,6 +37,8 @@ A candidate is validated with `--version` before use, so a dead wrapper (e.g. a
|
|
|
37
37
|
shim pointing at an uninstalled `.app`) is skipped automatically. If none launch, it tells
|
|
38
38
|
you to `brew install --cask chromium` or set `BROWSE_BROWSER`.
|
|
39
39
|
|
|
40
|
+
Startup waits up to 15 seconds for the running browser's valid DevTools port. An exited process or expired deadline fails launch; page readiness and scenario assertions have separate budgets.
|
|
41
|
+
|
|
40
42
|
## Default Stacks dev URLs
|
|
41
43
|
|
|
42
44
|
- Frontend: `http://localhost:3000`
|
|
@@ -121,6 +123,7 @@ On a failed step, execution stops and the report retains its number and error,
|
|
|
121
123
|
the completed steps, final page state and requested screenshot.
|
|
122
124
|
Use `{"action":"assert","selector":"...","absent":true}` to verify that an element is missing or hidden after an interaction.
|
|
123
125
|
Use `{"action":"assert","selector":"button","text":"Save","focused":true}` to verify keyboard focus and focus restoration.
|
|
126
|
+
For asynchronous results, use `{"action":"assert","selector":"button","text":"Sound on","timeoutMs":2000}`. This observes the actual visible result until it matches or the bounded budget expires. Ordinary assertions remain immediate. Only assertions accept `timeoutMs`, an integer from 0 to 30000; protocol and evaluation failures are not retried.
|
|
124
127
|
|
|
125
128
|
### Crawl (whole-site browser audit)
|
|
126
129
|
```bash
|
|
@@ -174,6 +174,28 @@ class Cdp {
|
|
|
174
174
|
|
|
175
175
|
interface Session { proc: ReturnType<typeof spawn>, port: number, userDataDir: string, browser: string }
|
|
176
176
|
|
|
177
|
+
export async function waitForBrowserPort(proc: { exitCode: number | null }, portFile: string, timeoutMs: number): Promise<number | null> {
|
|
178
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 30_000)
|
|
179
|
+
throw new TypeError('Browser startup timeout must be an integer between 1 and 30000.')
|
|
180
|
+
const deadline = performance.now() + timeoutMs
|
|
181
|
+
while (performance.now() < deadline && proc.exitCode === null) {
|
|
182
|
+
try {
|
|
183
|
+
const line = readFileSync(portFile, 'utf8').split('\n')[0]?.trim()
|
|
184
|
+
const port = Number(line)
|
|
185
|
+
if (Number.isInteger(port) && port > 0 && port <= 65535) {
|
|
186
|
+
if (performance.now() >= deadline || proc.exitCode !== null) return null
|
|
187
|
+
return port
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
|
192
|
+
}
|
|
193
|
+
const remaining = deadline - performance.now()
|
|
194
|
+
if (remaining > 0) await Bun.sleep(Math.min(50, remaining))
|
|
195
|
+
}
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
177
199
|
async function tryLaunch(browser: string): Promise<Session | null> {
|
|
178
200
|
const userDataDir = join(tmpdir(), `stacks-browse-${process.pid}-${Math.floor(Number(process.hrtime.bigint() % 1000000n))}`)
|
|
179
201
|
mkdirSync(userDataDir, { recursive: true })
|
|
@@ -196,18 +218,19 @@ async function tryLaunch(browser: string): Promise<Session | null> {
|
|
|
196
218
|
|
|
197
219
|
// Chromium writes the chosen port to <user-data-dir>/DevToolsActivePort.
|
|
198
220
|
const portFile = join(userDataDir, 'DevToolsActivePort')
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
221
|
+
let port: number | null = null
|
|
222
|
+
try {
|
|
223
|
+
port = await waitForBrowserPort(proc, portFile, 15_000)
|
|
224
|
+
if (port !== null) return { proc, port, userDataDir, browser }
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
if (port === null) {
|
|
228
|
+
try { proc.kill() }
|
|
229
|
+
catch { /* ignore */ }
|
|
230
|
+
try { rmSync(userDataDir, { recursive: true, force: true }) }
|
|
231
|
+
catch { /* a failed launch must not mask the next browser candidate */ }
|
|
204
232
|
}
|
|
205
|
-
await Bun.sleep(50)
|
|
206
233
|
}
|
|
207
|
-
try { proc.kill() }
|
|
208
|
-
catch { /* ignore */ }
|
|
209
|
-
try { rmSync(userDataDir, { recursive: true, force: true }) }
|
|
210
|
-
catch { /* a failed launch must not mask the next browser candidate */ }
|
|
211
234
|
return null
|
|
212
235
|
}
|
|
213
236
|
|
|
@@ -600,9 +623,12 @@ interface ScenarioStep {
|
|
|
600
623
|
key?: string
|
|
601
624
|
ms?: number
|
|
602
625
|
settle?: number
|
|
626
|
+
timeoutMs?: number
|
|
603
627
|
}
|
|
604
628
|
|
|
605
|
-
|
|
629
|
+
class ScenarioAssertionError extends Error {}
|
|
630
|
+
|
|
631
|
+
export function parseScenarioStep(value: string): ScenarioStep {
|
|
606
632
|
let parsed: unknown
|
|
607
633
|
try {
|
|
608
634
|
parsed = JSON.parse(value)
|
|
@@ -633,11 +659,41 @@ function parseScenarioStep(value: string): ScenarioStep {
|
|
|
633
659
|
throw new TypeError('Scenario action "press" requires a key.')
|
|
634
660
|
if (action === 'wait' && (!Number.isFinite(step.ms) || Number(step.ms) < 0 || Number(step.ms) > 30_000))
|
|
635
661
|
throw new TypeError('Scenario action "wait" requires ms between 0 and 30000.')
|
|
662
|
+
if (step.timeoutMs !== undefined && (action !== 'assert' || !Number.isSafeInteger(step.timeoutMs) || Number(step.timeoutMs) < 0 || Number(step.timeoutMs) > 30_000))
|
|
663
|
+
throw new TypeError('Scenario timeoutMs is supported only by assert actions and must be an integer between 0 and 30000.')
|
|
636
664
|
|
|
637
665
|
return step as unknown as ScenarioStep
|
|
638
666
|
}
|
|
639
667
|
|
|
640
|
-
async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<string, unknown>> {
|
|
668
|
+
export async function runScenarioStep(cdp: Pick<Cdp, 'send'>, step: ScenarioStep): Promise<Record<string, unknown>> {
|
|
669
|
+
if (step.action === 'assert' && step.timeoutMs) {
|
|
670
|
+
const deadline = performance.now() + step.timeoutMs
|
|
671
|
+
let last: ScenarioAssertionError | undefined
|
|
672
|
+
const expired = () => new Error(`${last?.message || `Scenario assert failed (${step.selector || ''})`}; timed out after ${step.timeoutMs}ms`)
|
|
673
|
+
while (true) {
|
|
674
|
+
const budget = deadline - performance.now()
|
|
675
|
+
if (budget <= 0) throw expired()
|
|
676
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
677
|
+
try {
|
|
678
|
+
const expiry = Promise.withResolvers<never>()
|
|
679
|
+
timeout = setTimeout(() => expiry.reject(expired()), budget)
|
|
680
|
+
const answer = await Promise.race([
|
|
681
|
+
runScenarioStep(cdp, { ...step, timeoutMs: 0 }),
|
|
682
|
+
expiry.promise,
|
|
683
|
+
])
|
|
684
|
+
if (performance.now() > deadline) throw expired()
|
|
685
|
+
return answer
|
|
686
|
+
}
|
|
687
|
+
catch (error) {
|
|
688
|
+
if (!(error instanceof ScenarioAssertionError)) throw error
|
|
689
|
+
last = error
|
|
690
|
+
const remaining = deadline - performance.now()
|
|
691
|
+
if (remaining <= 0) throw error
|
|
692
|
+
await Bun.sleep(Math.min(50, remaining))
|
|
693
|
+
}
|
|
694
|
+
finally { if (timeout !== undefined) clearTimeout(timeout) }
|
|
695
|
+
}
|
|
696
|
+
}
|
|
641
697
|
if (step.action === 'wait') {
|
|
642
698
|
await Bun.sleep(Number(step.ms))
|
|
643
699
|
return { action: step.action, ms: Number(step.ms), ok: true }
|
|
@@ -740,8 +796,11 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
|
|
|
740
796
|
returnByValue: true,
|
|
741
797
|
})
|
|
742
798
|
const value = result.result?.value as Record<string, unknown> | undefined
|
|
743
|
-
if (!value?.ok)
|
|
799
|
+
if (!value?.ok) {
|
|
800
|
+
if (step.action === 'assert' && value)
|
|
801
|
+
throw new ScenarioAssertionError(`Scenario assert failed: ${String(value.error || 'unknown error')} (${step.selector || ''})`)
|
|
744
802
|
throw new Error(`Scenario ${step.action} failed: ${String(value?.error || 'unknown error')} (${step.selector || step.key || ''})`)
|
|
803
|
+
}
|
|
745
804
|
if (step.action === 'click') {
|
|
746
805
|
const point = value.clickPoint as { x: number, y: number }
|
|
747
806
|
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: point.x, y: point.y, button: 'left', clickCount: 1 })
|
|
@@ -1196,7 +1255,7 @@ async function main() {
|
|
|
1196
1255
|
}
|
|
1197
1256
|
}
|
|
1198
1257
|
|
|
1199
|
-
main().catch((e) => {
|
|
1258
|
+
if (import.meta.main) main().catch((e) => {
|
|
1200
1259
|
console.error('browse error:', e?.message || e)
|
|
1201
1260
|
process.exit(1)
|
|
1202
1261
|
})
|
|
@@ -21,7 +21,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
|
|
|
21
21
|
| `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 119 |
|
|
22
22
|
| `@stacksjs/browser` | [`stacks-browser`](../stacks-browser/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser/src/index.ts) | 8 |
|
|
23
23
|
| `@stacksjs/browser-extension` | [`stacks-browser-extension`](../stacks-browser-extension/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser-extension/src/index.ts) | 7 |
|
|
24
|
-
| `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) |
|
|
24
|
+
| `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 164 |
|
|
25
25
|
| `@stacksjs/build` | [`stacks-build`](../stacks-build/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/build/src/index.ts) | 3 |
|
|
26
26
|
| `@stacksjs/cache` | [`stacks-cache`](../stacks-cache/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/cache/src/index.ts) | 14 |
|
|
27
27
|
| `@stacksjs/calendar-api` | [`stacks-calendar`](../stacks-calendar/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/calendar-api/src/index.ts) | 2 |
|
|
@@ -79,7 +79,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
|
|
|
79
79
|
| `@stacksjs/scheduler` | [`stacks-scheduler`](../stacks-scheduler/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/scheduler/src/index.ts) | 14 |
|
|
80
80
|
| `@stacksjs/search-engine` | [`stacks-search-engine`](../stacks-search-engine/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/search-engine/src/index.ts) | 10 |
|
|
81
81
|
| `@stacksjs/security` | [`stacks-security`](../stacks-security/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/security/src/index.ts) | 11 |
|
|
82
|
-
| `@stacksjs/server` | [`stacks-server`](../stacks-server/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/index.ts) |
|
|
82
|
+
| `@stacksjs/server` | [`stacks-server`](../stacks-server/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/index.ts) | 26 |
|
|
83
83
|
| `@stacksjs/shell` | [`stacks-shell`](../stacks-shell/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/shell/src/index.ts) | 1 |
|
|
84
84
|
| `@stacksjs/sites` | [`stacks-sites`](../stacks-sites/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/sites/src/index.ts) | 3 |
|
|
85
85
|
| `@stacksjs/skills` | [`stacks-skills`](../stacks-skills/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/skills/src/index.ts) | 1 |
|
|
@@ -215,7 +215,7 @@ Read the entrypoint and package export map for re-exported or type-only surfaces
|
|
|
215
215
|
|
|
216
216
|
Local source modules: [src/commands/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/commands/index.ts).
|
|
217
217
|
|
|
218
|
-
Evidence directory: [
|
|
218
|
+
Evidence directory: [164 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
|
|
219
219
|
|
|
220
220
|
### @stacksjs/build
|
|
221
221
|
|
|
@@ -763,7 +763,7 @@ Declared runtime exports: `server`.
|
|
|
763
763
|
|
|
764
764
|
Local source modules: [src/config.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/config.ts), [src/controllers/base.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/controllers/base.ts), [src/imports.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/imports.ts), [src/maintenance.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/maintenance.ts), [src/proxy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/proxy.ts), [src/redirects.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/redirects.ts), [src/rewrites.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/rewrites.ts), [src/security-headers.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/security-headers.ts), [src/shadowed-defaults.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/src/shadowed-defaults.ts).
|
|
765
765
|
|
|
766
|
-
Evidence directory: [
|
|
766
|
+
Evidence directory: [26 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/server/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
|
|
767
767
|
|
|
768
768
|
### @stacksjs/shell
|
|
769
769
|
|
package/ide/vscode/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"publisher": "Stacks",
|
|
3
3
|
"name": "vscode-stacks",
|
|
4
4
|
"displayName": "Stacks",
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.266",
|
|
6
6
|
"description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"funding": "https://github.com/sponsors/chrisbbreuer",
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.266",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@iconify-json/f7": "^1.2.2",
|
|
47
47
|
"@iconify-json/hugeicons": "^1.2.35",
|
|
48
|
-
"@stacksjs/mobile": "0.75.
|
|
49
|
-
"@stacksjs/sanitizer": "0.2.
|
|
50
|
-
"@stacksjs/stx": "0.2.
|
|
48
|
+
"@stacksjs/mobile": "0.75.266",
|
|
49
|
+
"@stacksjs/sanitizer": "0.2.663",
|
|
50
|
+
"@stacksjs/stx": "0.2.663",
|
|
51
51
|
"@xterm/addon-fit": "^0.11.0",
|
|
52
52
|
"@xterm/xterm": "^6.0.0",
|
|
53
53
|
"ts-qr-codes": "^0.1.8"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
3
5
|
|
|
@@ -67,29 +69,75 @@ const filteredRequests = derived(() => {
|
|
|
67
69
|
})
|
|
68
70
|
})
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
function statusClass(status: number): string {
|
|
72
|
+
/**
|
|
73
|
+
* Pill colours by tone rather than by value, so every status code and every
|
|
74
|
+
* method the table has not named still lands on a colour, as the hand-rolled
|
|
75
|
+
* pills did.
|
|
76
|
+
*/
|
|
77
|
+
function statusTone(status: number): string {
|
|
78
78
|
if (status >= 500)
|
|
79
|
-
return '
|
|
79
|
+
return 'server'
|
|
80
80
|
if (status >= 400)
|
|
81
|
-
return '
|
|
81
|
+
return 'client'
|
|
82
82
|
if (status >= 300)
|
|
83
|
-
return '
|
|
84
|
-
return '
|
|
83
|
+
return 'redirect'
|
|
84
|
+
return 'ok'
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function
|
|
87
|
+
function methodTone(method: string): string {
|
|
88
88
|
if (method === 'GET')
|
|
89
|
-
return '
|
|
89
|
+
return 'read'
|
|
90
90
|
if (method === 'DELETE')
|
|
91
|
-
return '
|
|
92
|
-
return '
|
|
91
|
+
return 'delete'
|
|
92
|
+
return 'write'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const COLUMNS = [
|
|
96
|
+
{
|
|
97
|
+
key: 'methodTone',
|
|
98
|
+
label: 'Method',
|
|
99
|
+
type: 'badge' as const,
|
|
100
|
+
sortable: true,
|
|
101
|
+
badgeLabelKey: 'method',
|
|
102
|
+
badgeClasses: {
|
|
103
|
+
read: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40',
|
|
104
|
+
delete: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/40',
|
|
105
|
+
write: 'text-violet-700 dark:text-violet-300 bg-violet-50 dark:bg-violet-950/40',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{ key: 'path', label: 'Path', sortable: true, class: 'max-w-md font-mono text-neutral-800 text-xs truncate dark:text-neutral-200' },
|
|
109
|
+
{
|
|
110
|
+
key: 'statusTone',
|
|
111
|
+
label: 'Status',
|
|
112
|
+
type: 'badge' as const,
|
|
113
|
+
sortable: true,
|
|
114
|
+
badgeLabelKey: 'status',
|
|
115
|
+
badgeClasses: {
|
|
116
|
+
server: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-950/40',
|
|
117
|
+
client: 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40',
|
|
118
|
+
redirect: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40',
|
|
119
|
+
ok: 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/40',
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{ key: 'durationMs', label: 'Duration (ms)', type: 'number' as const, sortable: true, align: 'right' as const, class: 'font-mono whitespace-nowrap' },
|
|
123
|
+
{ key: 'ipAddress', label: 'Client', sortable: true, emptyText: 'Not recorded' },
|
|
124
|
+
{ key: 'createdAt', label: 'Recorded', type: 'datetime' as const, sortable: true, align: 'right' as const, emptyText: 'Unknown', class: 'whitespace-nowrap' },
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
const ROW_ACTIONS = [
|
|
128
|
+
{ action: 'inspect', label: 'Inspect request', icon: 'i-hugeicons-view' },
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
const requestRows = derived(() => filteredRequests().map(request => ({
|
|
132
|
+
...request,
|
|
133
|
+
methodTone: methodTone(request.method),
|
|
134
|
+
statusTone: statusTone(request.status),
|
|
135
|
+
durationMs: Math.round(request.durationMs * 10) / 10,
|
|
136
|
+
})))
|
|
137
|
+
|
|
138
|
+
function handleRowAction(payload: { action: string, row: RequestRow }): void {
|
|
139
|
+
if (payload.action === 'inspect')
|
|
140
|
+
inspectRequest(payload.row)
|
|
93
141
|
}
|
|
94
142
|
|
|
95
143
|
function inspectRequest(request: RequestRow): void {
|
|
@@ -184,49 +232,18 @@ onMount(() => {
|
|
|
184
232
|
</div>
|
|
185
233
|
</div>
|
|
186
234
|
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
:title="search() || methodFilter() !== 'all' || statusFilter() !== 'all' ? 'No matching requests' : 'No requests recorded'"
|
|
196
|
-
description="Request records appear after the application writes to the Request model."
|
|
235
|
+
<Table
|
|
236
|
+
:columns="COLUMNS"
|
|
237
|
+
:data="requestRows()"
|
|
238
|
+
:loading="loading()"
|
|
239
|
+
:row-actions="ROW_ACTIONS"
|
|
240
|
+
action-label-key="path"
|
|
241
|
+
paginated
|
|
242
|
+
:page-size="25"
|
|
243
|
+
:empty-title="search() || methodFilter() !== 'all' || statusFilter() !== 'all' ? 'No matching requests' : 'No requests recorded'"
|
|
244
|
+
empty-description="Request records appear after the application writes to the Request model."
|
|
245
|
+
@row-action="handleRowAction"
|
|
197
246
|
/>
|
|
198
|
-
|
|
199
|
-
<div :if="!loading() && filteredRequests().length > 0" class="overflow-x-auto">
|
|
200
|
-
<table class="w-full">
|
|
201
|
-
<thead class="bg-neutral-50 dark:bg-neutral-800/50">
|
|
202
|
-
<tr>
|
|
203
|
-
<th class="pl-5 pr-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Request</th>
|
|
204
|
-
<th class="px-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Status</th>
|
|
205
|
-
<th class="px-4 py-3 font-semibold text-neutral-500 text-right text-xs tracking-wider uppercase dark:text-neutral-400">Duration</th>
|
|
206
|
-
<th class="px-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Client</th>
|
|
207
|
-
<th class="px-4 py-3 font-semibold text-neutral-500 text-right text-xs tracking-wider uppercase dark:text-neutral-400">Recorded</th>
|
|
208
|
-
<th class="pr-5 py-3 font-semibold text-neutral-500 text-right text-xs tracking-wider uppercase dark:text-neutral-400">Details</th>
|
|
209
|
-
</tr>
|
|
210
|
-
</thead>
|
|
211
|
-
<tbody class="divide-neutral-100 divide-y dark:divide-neutral-700">
|
|
212
|
-
<tr :for="request in filteredRequests()">
|
|
213
|
-
<td class="pl-5 pr-4 py-3.5">
|
|
214
|
-
<div class="flex gap-2 items-center">
|
|
215
|
-
<span :class="'inline-flex px-2 py-0.5 font-medium text-xs rounded ' + methodClass(request.method)">{{ request.method }}</span>
|
|
216
|
-
<code class="max-w-md text-neutral-800 text-xs truncate dark:text-neutral-200">{{ request.path }}</code>
|
|
217
|
-
</div>
|
|
218
|
-
</td>
|
|
219
|
-
<td class="px-4 py-3.5"><span :class="'inline-flex px-2 py-0.5 font-medium text-xs rounded ' + statusClass(request.status)">{{ request.status }}</span></td>
|
|
220
|
-
<td class="px-4 py-3.5 font-mono text-neutral-600 text-right text-xs whitespace-nowrap dark:text-neutral-300">{{ request.durationMs.toFixed(1) }} ms</td>
|
|
221
|
-
<td class="px-4 py-3.5 text-neutral-500 text-xs dark:text-neutral-400">{{ request.ipAddress || 'Not recorded' }}</td>
|
|
222
|
-
<td class="px-4 py-3.5 text-neutral-500 text-right text-xs whitespace-nowrap dark:text-neutral-400">{{ formatDate(request.createdAt) }}</td>
|
|
223
|
-
<td class="pr-5 py-3.5 text-right">
|
|
224
|
-
<Button variant="ghost" size="sm" @click="inspectRequest(request)">Inspect</Button>
|
|
225
|
-
</td>
|
|
226
|
-
</tr>
|
|
227
|
-
</tbody>
|
|
228
|
-
</table>
|
|
229
|
-
</div>
|
|
230
247
|
</Card>
|
|
231
248
|
|
|
232
249
|
<RequestDetailsDialog :request="selectedRequest()" @close="closeInspector" />
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import('../UI/Table')
|
|
2
|
+
|
|
1
3
|
<script client>
|
|
2
4
|
import { dashboardApi } from '../../../../functions/dashboard-api'
|
|
3
5
|
|
|
@@ -44,9 +46,7 @@ const page = derived(() => mode() === 'actions'
|
|
|
44
46
|
empty: 'No Action modules were found.',
|
|
45
47
|
scaffold: 'buddy make:action MyAction',
|
|
46
48
|
icon: 'i-hugeicons-energy',
|
|
47
|
-
detailLabel: 'Method',
|
|
48
49
|
note: 'Application files override matching framework defaults.',
|
|
49
|
-
showOrigin: true,
|
|
50
50
|
stats: [
|
|
51
51
|
{ key: 'total', label: 'Total Actions' },
|
|
52
52
|
{ key: 'application', label: 'Application Overrides' },
|
|
@@ -60,9 +60,7 @@ const page = derived(() => mode() === 'actions'
|
|
|
60
60
|
empty: 'No command files in app/Commands.',
|
|
61
61
|
scaffold: 'buddy make:command my-command',
|
|
62
62
|
icon: 'i-hugeicons-command-line',
|
|
63
|
-
detailLabel: 'Aliases and options',
|
|
64
63
|
note: 'Every file in app/Commands is a command. The optional app/Commands.ts only orders, aliases or disables them.',
|
|
65
|
-
showOrigin: false,
|
|
66
64
|
stats: [
|
|
67
65
|
{ key: 'total', label: 'Commands' },
|
|
68
66
|
{ key: 'registered', label: 'Registry Entries' },
|
|
@@ -90,13 +88,6 @@ function statValue(key: string): number {
|
|
|
90
88
|
return Number(stats()[key] || 0)
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
function formatDate(value: string): string {
|
|
94
|
-
if (!value)
|
|
95
|
-
return 'Unknown'
|
|
96
|
-
const date = new Date(value)
|
|
97
|
-
return Number.isNaN(date.getTime()) ? 'Unknown' : date.toLocaleString()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
91
|
function formatAliases(item: SourceItem): string {
|
|
101
92
|
return item.aliases?.length ? item.aliases.join(', ') : 'None'
|
|
102
93
|
}
|
|
@@ -105,14 +96,67 @@ function formatOptions(item: SourceItem): string {
|
|
|
105
96
|
return item.options?.length ? item.options.join(', ') : 'None'
|
|
106
97
|
}
|
|
107
98
|
|
|
108
|
-
|
|
99
|
+
/** Read-only methods, writes, and the catch-all, as the hand-rolled pill coloured them. */
|
|
100
|
+
function methodTone(method = 'ANY'): string {
|
|
109
101
|
if (['GET', 'HEAD', 'OPTIONS'].includes(method))
|
|
110
|
-
return '
|
|
102
|
+
return 'read'
|
|
111
103
|
if (method === 'ANY')
|
|
112
|
-
return '
|
|
113
|
-
return '
|
|
104
|
+
return 'any'
|
|
105
|
+
return 'write'
|
|
114
106
|
}
|
|
115
107
|
|
|
108
|
+
const NAME_COLUMN = { key: 'name', label: 'Name', sortable: true, subtitleKey: 'description', class: 'font-medium text-neutral-900 dark:text-white' }
|
|
109
|
+
const PATH_COLUMN = { key: 'path', label: 'Path', sortable: true, class: 'font-mono text-neutral-600 text-xs dark:text-neutral-300' }
|
|
110
|
+
const MODIFIED_COLUMN = { key: 'modifiedAt', label: 'Modified', type: 'datetime' as const, sortable: true, align: 'right' as const, emptyText: 'Unknown', class: 'whitespace-nowrap' }
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Actions show a method and where the file comes from. Commands have one
|
|
114
|
+
* root, so their second column is what can be typed instead.
|
|
115
|
+
*/
|
|
116
|
+
const COLUMNS_BY_MODE = {
|
|
117
|
+
actions: [
|
|
118
|
+
NAME_COLUMN,
|
|
119
|
+
{
|
|
120
|
+
key: 'methodTone',
|
|
121
|
+
label: 'Method',
|
|
122
|
+
type: 'badge' as const,
|
|
123
|
+
sortable: true,
|
|
124
|
+
badgeLabelKey: 'methodLabel',
|
|
125
|
+
badgeClasses: {
|
|
126
|
+
read: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40',
|
|
127
|
+
any: 'text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-800',
|
|
128
|
+
write: 'text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/40',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
key: 'origin',
|
|
133
|
+
label: 'Origin',
|
|
134
|
+
type: 'badge' as const,
|
|
135
|
+
sortable: true,
|
|
136
|
+
badgeClasses: {
|
|
137
|
+
Application: 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/40',
|
|
138
|
+
Framework: 'text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-800',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
PATH_COLUMN,
|
|
142
|
+
MODIFIED_COLUMN,
|
|
143
|
+
],
|
|
144
|
+
commands: [
|
|
145
|
+
NAME_COLUMN,
|
|
146
|
+
{ key: 'aliasesLabel', label: 'Aliases', sortable: true, subtitleKey: 'optionsLabel', class: 'text-neutral-700 text-xs dark:text-neutral-300' },
|
|
147
|
+
PATH_COLUMN,
|
|
148
|
+
MODIFIED_COLUMN,
|
|
149
|
+
],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const itemRows = derived(() => filteredItems().map(item => ({
|
|
153
|
+
...item,
|
|
154
|
+
methodTone: methodTone(item.method),
|
|
155
|
+
methodLabel: item.method || 'ANY',
|
|
156
|
+
aliasesLabel: `Aliases: ${formatAliases(item)}`,
|
|
157
|
+
optionsLabel: `Options: ${formatOptions(item)}`,
|
|
158
|
+
})))
|
|
159
|
+
|
|
116
160
|
async function loadInventory(): Promise<void> {
|
|
117
161
|
loading.set(true)
|
|
118
162
|
loadError.set('')
|
|
@@ -205,50 +249,15 @@ onMount(() => {
|
|
|
205
249
|
</label>
|
|
206
250
|
</div>
|
|
207
251
|
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
:
|
|
215
|
-
:
|
|
216
|
-
:
|
|
217
|
-
:description="search() ? 'Try a different name, path, method, or alias.' : 'Use the scaffold command to add one through the native framework workflow.'"
|
|
252
|
+
<Table
|
|
253
|
+
:columns="COLUMNS_BY_MODE[mode()]"
|
|
254
|
+
:data="itemRows()"
|
|
255
|
+
:loading="loading()"
|
|
256
|
+
row-key="path"
|
|
257
|
+
paginated
|
|
258
|
+
:page-size="25"
|
|
259
|
+
:empty-title="search() ? 'No matching source files' : page().empty"
|
|
260
|
+
:empty-description="search() ? 'Try a different name, path, method, or alias.' : 'Use the scaffold command to add one through the native framework workflow.'"
|
|
218
261
|
/>
|
|
219
|
-
|
|
220
|
-
<div :if="!loading() && filteredItems().length > 0" class="overflow-x-auto">
|
|
221
|
-
<table class="w-full">
|
|
222
|
-
<thead class="bg-neutral-50 dark:bg-neutral-800/50">
|
|
223
|
-
<tr>
|
|
224
|
-
<th class="pl-5 pr-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Name</th>
|
|
225
|
-
<th class="px-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">{{ page().detailLabel }}</th>
|
|
226
|
-
<th :if="page().showOrigin" class="px-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Origin</th>
|
|
227
|
-
<th class="px-4 py-3 font-semibold text-left text-neutral-500 text-xs tracking-wider uppercase dark:text-neutral-400">Path</th>
|
|
228
|
-
<th class="pr-5 py-3 font-semibold text-neutral-500 text-right text-xs tracking-wider uppercase dark:text-neutral-400">Modified</th>
|
|
229
|
-
</tr>
|
|
230
|
-
</thead>
|
|
231
|
-
<tbody class="divide-neutral-100 divide-y dark:divide-neutral-700">
|
|
232
|
-
<tr :for="item in filteredItems()">
|
|
233
|
-
<td class="pl-5 pr-4 py-3.5">
|
|
234
|
-
<p class="font-medium text-neutral-900 text-sm dark:text-white">{{ item.name }}</p>
|
|
235
|
-
<p class="mt-0.5 max-w-md text-neutral-500 text-xs dark:text-neutral-400">{{ item.description }}</p>
|
|
236
|
-
</td>
|
|
237
|
-
<td class="px-4 py-3.5">
|
|
238
|
-
<span :if="mode() === 'actions'" :class="'inline-flex px-2 py-0.5 font-medium text-xs rounded ' + methodClass(item.method)">{{ item.method || 'ANY' }}</span>
|
|
239
|
-
<div :if="mode() === 'commands'" class="space-y-1 text-xs">
|
|
240
|
-
<p class="text-neutral-700 dark:text-neutral-300"><span class="text-neutral-500 dark:text-neutral-400">Aliases:</span> {{ formatAliases(item) }}</p>
|
|
241
|
-
<p class="text-neutral-700 dark:text-neutral-300"><span class="text-neutral-500 dark:text-neutral-400">Options:</span> {{ formatOptions(item) }}</p>
|
|
242
|
-
</div>
|
|
243
|
-
</td>
|
|
244
|
-
<td :if="page().showOrigin" class="px-4 py-3.5">
|
|
245
|
-
<span :class="'inline-flex px-2 py-0.5 font-medium text-xs rounded ' + (item.origin === 'Application' ? 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/40' : 'text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-800')">{{ item.origin }}</span>
|
|
246
|
-
</td>
|
|
247
|
-
<td class="px-4 py-3.5 font-mono text-neutral-600 text-xs dark:text-neutral-300">{{ item.path }}</td>
|
|
248
|
-
<td class="pr-5 py-3.5 text-neutral-500 text-right text-xs whitespace-nowrap dark:text-neutral-400">{{ formatDate(item.modifiedAt) }}</td>
|
|
249
|
-
</tr>
|
|
250
|
-
</tbody>
|
|
251
|
-
</table>
|
|
252
|
-
</div>
|
|
253
262
|
</Card>
|
|
254
263
|
</div>
|