@stacksjs/defaults 0.70.365 → 0.70.367
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 +9 -1
- package/ai/skills/stacks-dashboard/SKILL.md +71 -14
- package/ai/skills/stacks-deploy/SKILL.md +18 -0
- package/ai/skills/stacks-desktop/SKILL.md +11 -2
- package/ai/skills/stacks-stx/SKILL.md +25 -0
- package/app/Actions/Auth/LoginAction.ts +11 -2
- package/app/Actions/Auth/RefreshTokenAction.ts +6 -2
- package/app/Actions/Auth/RegisterAction.ts +6 -2
- package/app/Actions/Auth/VerifyTwoFactorLoginAction.ts +6 -2
- package/app/Actions/Dashboard/Deployments/CreateDeployment.ts +64 -44
- package/app/Actions/Dashboard/Deployments/CreateDeploymentRollback.ts +26 -0
- package/app/Actions/Dashboard/Deployments/PreviewDeployment.ts +39 -0
- package/app/Actions/Dashboard/Deployments/PreviewDeploymentRollback.ts +22 -0
- package/app/Actions/Dashboard/Deployments/deployment-input.test.ts +12 -2
- package/app/Actions/Dashboard/Deployments/deployment-input.ts +5 -3
- package/app/Actions/Dashboard/Deployments/deployment-preview.test.ts +29 -0
- package/app/Actions/Dashboard/Deployments/deployment-preview.ts +41 -0
- package/app/Actions/Dashboard/Deployments/deployment-rollback.test.ts +16 -0
- package/app/Actions/Dashboard/Deployments/deployment-rollback.ts +89 -0
- package/app/Actions/Dashboard/Operations/AuditIndexAction.ts +25 -0
- package/app/Actions/Dashboard/Operations/ChangeApprovalAction.ts +28 -0
- package/app/Actions/Dashboard/Operations/ChangeIndexAction.ts +32 -0
- package/app/Actions/Dashboard/Operations/IncidentIndexAction.ts +32 -0
- package/app/Actions/Dashboard/Operations/IncidentUpdateAction.ts +34 -0
- package/app/Actions/Dashboard/Operations/MigrationApplyAction.ts +30 -0
- package/app/Actions/Dashboard/Operations/MigrationIndexAction.ts +24 -0
- package/app/Actions/Dashboard/Operations/MigrationReconcileAction.ts +23 -0
- package/app/Actions/Dashboard/Operations/RecoveryDestinationStoreAction.ts +91 -0
- package/app/Actions/Dashboard/Operations/RecoveryDestinationTestAction.ts +34 -0
- package/app/Actions/Dashboard/Operations/RecoveryIndexAction.ts +58 -0
- package/app/Actions/Dashboard/Operations/RecoveryPolicyStoreAction.ts +90 -0
- package/app/Actions/Dashboard/Operations/RecoveryProtectAction.ts +32 -0
- package/app/Actions/Dashboard/Operations/RecoveryRestoreAction.ts +72 -0
- package/app/Actions/Dashboard/Operations/RecoveryRetentionAction.ts +17 -0
- package/app/Actions/Dashboard/Operations/RecoveryRunAction.ts +25 -0
- package/app/Actions/Dashboard/Operations/RecoveryVerifyAction.ts +24 -0
- package/app/Actions/Dashboard/Operations/SchedulerIndexAction.ts +34 -0
- package/app/Actions/Dashboard/Operations/SchedulerRunAction.ts +33 -0
- package/app/Actions/Dashboard/Operations/SchedulerToggleAction.ts +36 -0
- package/app/Actions/Dashboard/Operations/control-plane.ts +150 -0
- package/app/Actions/Dashboard/Operations/migration-operations.ts +80 -0
- package/app/Actions/Dashboard/Operations/operations-runtime.ts +11 -0
- package/app/Actions/Dashboard/Operations/recovery-input.test.ts +22 -0
- package/app/Actions/Dashboard/Operations/recovery-input.ts +33 -0
- package/app/Actions/Dashboard/Operations/recovery-runtime.ts +99 -0
- package/app/Actions/Dashboard/Operations/scheduler-operations.test.ts +23 -0
- package/app/Actions/Dashboard/Operations/scheduler-operations.ts +138 -0
- package/functions/deployments.ts +18 -0
- package/functions/operations.ts +361 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/Auth/ForgotPasswordDashboard.stx +0 -2
- package/resources/components/Dashboard/Auth/LoginDashboard.stx +1 -1
- package/resources/components/Dashboard/Auth/RegisterDashboard.stx +1 -3
- package/resources/components/Dashboard/Ci/CiDashboard.stx +4 -4
- package/resources/components/Dashboard/Deployments/DeploymentList.stx +147 -14
- package/resources/components/Dashboard/Deployments/DeploymentPreviewDialog.stx +118 -0
- package/resources/components/Dashboard/GlobalSearch.stx +87 -91
- package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +9 -435
- package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +416 -0
- package/resources/components/Dashboard/Modals/BaseModal.stx +13 -36
- package/resources/components/Dashboard/Modals/Popups/Alert.stx +18 -44
- package/resources/components/Dashboard/Operations/AuditDashboard.stx +16 -0
- package/resources/components/Dashboard/Operations/ChangeDashboard.stx +66 -0
- package/resources/components/Dashboard/Operations/IncidentDashboard.stx +39 -0
- package/resources/components/Dashboard/Operations/MigrationDashboard.stx +146 -0
- package/resources/components/Dashboard/Operations/OperationHistory.stx +61 -0
- package/resources/components/Dashboard/Operations/OperationsNavigation.stx +32 -0
- package/resources/components/Dashboard/Operations/RecoveryDashboard.stx +229 -0
- package/resources/components/Dashboard/Operations/RecoveryDestinationDialog.stx +126 -0
- package/resources/components/Dashboard/Operations/RecoveryJobList.stx +32 -0
- package/resources/components/Dashboard/Operations/RecoveryPointTable.stx +82 -0
- package/resources/components/Dashboard/Operations/RecoveryPolicyDialog.stx +155 -0
- package/resources/components/Dashboard/Operations/RecoveryRestoreDialog.stx +90 -0
- package/resources/components/Dashboard/Operations/SchedulerDashboard.stx +144 -0
- package/resources/components/Dashboard/Operations/SchedulerTaskTable.stx +81 -0
- package/resources/components/Dashboard/Settings/SettingsDashboard.stx +45 -3
- package/resources/components/Dashboard/UI/Modal.stx +43 -14
- package/resources/components/Dashboard/UI/Table.stx +30 -25
- package/resources/functions/dashboard/sidebar.ts +1 -0
- package/routes/dashboard-api.ts +23 -0
- package/routes/dashboard.ts +1 -0
- package/views/dashboard/AUDIT.md +21 -16
- package/views/dashboard/operations/audit.stx +5 -0
- package/views/dashboard/operations/changes.stx +5 -0
- package/views/dashboard/operations/incidents.stx +5 -0
- package/views/dashboard/operations/migrations.stx +8 -0
- package/views/dashboard/operations/recovery.stx +8 -0
- package/views/dashboard/operations/scheduler.stx +8 -0
- package/views/dashboard/stores/ci.ts +2 -2
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script client>
|
|
2
|
+
import type { SchedulerTask } from '../../../../functions/operations'
|
|
3
|
+
|
|
4
|
+
const tasks = useReactiveProp('tasks', [])
|
|
5
|
+
const loading = useReactiveProp('loading', false)
|
|
6
|
+
const busyTask = useReactiveProp('busyTask', '')
|
|
7
|
+
const emit = defineEmits()
|
|
8
|
+
|
|
9
|
+
function rows(): SchedulerTask[] {
|
|
10
|
+
return tasks() as SchedulerTask[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatDate(value: string | null): string {
|
|
14
|
+
if (!value)
|
|
15
|
+
return 'Not scheduled'
|
|
16
|
+
const date = new Date(value)
|
|
17
|
+
return Number.isNaN(date.getTime())
|
|
18
|
+
? value
|
|
19
|
+
: new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(date)
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<div class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-xl dark:border-neutral-700">
|
|
24
|
+
<div :if="loading()" class="px-5 py-14 text-center text-gray-500 text-sm dark:text-neutral-400">
|
|
25
|
+
Loading scheduled tasks
|
|
26
|
+
</div>
|
|
27
|
+
<div :if="!loading() && rows().length === 0" class="px-5 py-14 text-center">
|
|
28
|
+
<span aria-hidden="true" class="block mb-3 mx-auto h-9 w-9 text-gray-400 i-hugeicons-calendar-03"></span>
|
|
29
|
+
<p class="font-semibold text-gray-900 dark:text-white">No scheduled tasks registered</p>
|
|
30
|
+
<p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Add a rate to an app job or register a task in app/Scheduler.ts.</p>
|
|
31
|
+
</div>
|
|
32
|
+
<div :if="rows().length > 0" class="overflow-x-auto">
|
|
33
|
+
<table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700">
|
|
34
|
+
<thead class="bg-gray-50 dark:bg-neutral-700/50">
|
|
35
|
+
<tr>
|
|
36
|
+
<th class="px-4 py-3 font-medium text-gray-500 text-left text-xs uppercase dark:text-neutral-300">Task</th>
|
|
37
|
+
<th class="px-4 py-3 font-medium text-gray-500 text-left text-xs uppercase dark:text-neutral-300">Schedule</th>
|
|
38
|
+
<th class="px-4 py-3 font-medium text-gray-500 text-left text-xs uppercase dark:text-neutral-300">Next run</th>
|
|
39
|
+
<th class="px-4 py-3 font-medium text-gray-500 text-left text-xs uppercase dark:text-neutral-300">Status</th>
|
|
40
|
+
<th class="px-4 py-3"><span class="sr-only">Actions</span></th>
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
|
|
44
|
+
<template :for="task in rows()">
|
|
45
|
+
<tr>
|
|
46
|
+
<td class="px-4 py-3">
|
|
47
|
+
<p class="font-medium text-gray-900 text-sm dark:text-white">{{ task.name }}</p>
|
|
48
|
+
<p class="mt-0.5 text-gray-500 text-xs dark:text-neutral-400">{{ task.timezone }}</p>
|
|
49
|
+
</td>
|
|
50
|
+
<td class="px-4 py-3 font-mono text-gray-600 text-sm whitespace-nowrap dark:text-neutral-300">{{ task.pattern || 'Not configured' }}</td>
|
|
51
|
+
<td class="px-4 py-3 text-gray-600 text-sm whitespace-nowrap dark:text-neutral-300">{{ formatDate(task.nextRun) }}</td>
|
|
52
|
+
<td class="px-4 py-3">
|
|
53
|
+
<Badge :variant="task.enabled ? 'success' : 'warning'" dot>{{ task.enabled ? 'Active' : 'Paused' }}</Badge>
|
|
54
|
+
</td>
|
|
55
|
+
<td class="px-4 py-3 text-right whitespace-nowrap">
|
|
56
|
+
<div class="inline-flex gap-2 items-center">
|
|
57
|
+
<Button
|
|
58
|
+
size="sm"
|
|
59
|
+
:disabled="busyTask() !== '' || !task.enabled"
|
|
60
|
+
:loading="busyTask() === task.name + ':run'"
|
|
61
|
+
@click="emit('run', task.name)"
|
|
62
|
+
>
|
|
63
|
+
Run now
|
|
64
|
+
</Button>
|
|
65
|
+
<Button
|
|
66
|
+
size="sm"
|
|
67
|
+
variant="outline"
|
|
68
|
+
:disabled="busyTask() !== ''"
|
|
69
|
+
:loading="busyTask() === task.name + ':toggle'"
|
|
70
|
+
@click="emit('toggle', { name: task.name, enabled: !task.enabled })"
|
|
71
|
+
>
|
|
72
|
+
{{ task.enabled ? 'Pause' : 'Resume' }}
|
|
73
|
+
</Button>
|
|
74
|
+
</div>
|
|
75
|
+
</td>
|
|
76
|
+
</tr>
|
|
77
|
+
</template>
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
@@ -42,6 +42,7 @@ const files = state<ConfigFile[]>([])
|
|
|
42
42
|
const fields = state<ConfigField[]>([])
|
|
43
43
|
const drafts = reactive<Record<string, string | number | boolean>>({})
|
|
44
44
|
const selected = state('')
|
|
45
|
+
const pendingSelection = state('')
|
|
45
46
|
const query = state('')
|
|
46
47
|
const loading = state(true)
|
|
47
48
|
const saving = state(false)
|
|
@@ -58,6 +59,7 @@ const filteredFiles = derived(() => {
|
|
|
58
59
|
|
|
59
60
|
const selectedFile = derived(() => files().find(file => file.name === selected()))
|
|
60
61
|
const editableCount = derived(() => fields().filter(field => field.editable).length)
|
|
62
|
+
const isDirty = derived(() => fields().some(field => field.editable && drafts[field.key] !== field.value))
|
|
61
63
|
|
|
62
64
|
function displayValue(field: ConfigField): string | number {
|
|
63
65
|
if (field.editable)
|
|
@@ -101,7 +103,6 @@ async function selectFile(name: string): Promise<void> {
|
|
|
101
103
|
if (!body.ok)
|
|
102
104
|
throw new Error(body.error || `Unable to read config/${name}.ts`)
|
|
103
105
|
|
|
104
|
-
fields.set(body.fields)
|
|
105
106
|
for (const key of Object.keys(drafts))
|
|
106
107
|
delete drafts[key]
|
|
107
108
|
Object.assign(
|
|
@@ -112,6 +113,7 @@ async function selectFile(name: string): Promise<void> {
|
|
|
112
113
|
.map((field: ConfigField) => [field.key, field.value]),
|
|
113
114
|
),
|
|
114
115
|
)
|
|
116
|
+
fields.set(body.fields)
|
|
115
117
|
}
|
|
116
118
|
catch (cause) {
|
|
117
119
|
error.set(cause instanceof Error ? cause.message : `Unable to read config/${name}.ts`)
|
|
@@ -121,6 +123,34 @@ async function selectFile(name: string): Promise<void> {
|
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
function requestSelectFile(name: string): void {
|
|
127
|
+
if (name === selected())
|
|
128
|
+
return
|
|
129
|
+
if (isDirty()) {
|
|
130
|
+
pendingSelection.set(name)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
void selectFile(name)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function cancelPendingSelection(): void {
|
|
137
|
+
pendingSelection.set('')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function discardAndSelect(): void {
|
|
141
|
+
const name = pendingSelection()
|
|
142
|
+
pendingSelection.set('')
|
|
143
|
+
if (name)
|
|
144
|
+
void selectFile(name)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function handleBeforeUnload(event: BeforeUnloadEvent): void {
|
|
148
|
+
if (!isDirty())
|
|
149
|
+
return
|
|
150
|
+
event.preventDefault()
|
|
151
|
+
event.returnValue = ''
|
|
152
|
+
}
|
|
153
|
+
|
|
124
154
|
async function save(): Promise<void> {
|
|
125
155
|
const file = selected()
|
|
126
156
|
if (!file || saving())
|
|
@@ -154,6 +184,7 @@ async function save(): Promise<void> {
|
|
|
154
184
|
}
|
|
155
185
|
}
|
|
156
186
|
|
|
187
|
+
useEventListener('beforeunload', handleBeforeUnload)
|
|
157
188
|
void loadFiles()
|
|
158
189
|
</script>
|
|
159
190
|
|
|
@@ -185,7 +216,7 @@ void loadFiles()
|
|
|
185
216
|
<button
|
|
186
217
|
type="button"
|
|
187
218
|
:class="selected() === file.name ? 'flex items-center gap-3 px-3 py-2.5 w-full text-left text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-500/10 rounded-lg' : 'flex items-center gap-3 px-3 py-2.5 w-full text-left text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-white/5 rounded-lg'"
|
|
188
|
-
@click="
|
|
219
|
+
@click="requestSelectFile(file.name)"
|
|
189
220
|
>
|
|
190
221
|
<span class="flex-none h-4 w-4 i-hugeicons-settings-02"></span>
|
|
191
222
|
<span class="flex-1 font-medium text-sm truncate">{{ file.title }}</span>
|
|
@@ -220,8 +251,9 @@ void loadFiles()
|
|
|
220
251
|
<p class="mt-1 font-mono text-neutral-500 text-xs dark:text-neutral-400">config/{{ selectedFile().name }}.ts</p>
|
|
221
252
|
</div>
|
|
222
253
|
<div class="flex gap-2 items-center">
|
|
254
|
+
<Badge :if="isDirty()" variant="warning" size="sm">Unsaved changes</Badge>
|
|
223
255
|
<Badge variant="default" size="sm">{{ editableCount() }} editable</Badge>
|
|
224
|
-
<Button variant="primary" size="sm" :disabled="saving() || editableCount() === 0" @click="save()">
|
|
256
|
+
<Button variant="primary" size="sm" :disabled="saving() || editableCount() === 0 || !isDirty()" @click="save()">
|
|
225
257
|
{{ saving() ? 'Saving...' : 'Save changes' }}
|
|
226
258
|
</Button>
|
|
227
259
|
</div>
|
|
@@ -284,5 +316,15 @@ void loadFiles()
|
|
|
284
316
|
@endif
|
|
285
317
|
</Card>
|
|
286
318
|
</div>
|
|
319
|
+
|
|
320
|
+
<ConfirmDialog
|
|
321
|
+
:isOpen="Boolean(pendingSelection())"
|
|
322
|
+
title="Discard unsaved changes?"
|
|
323
|
+
description="Switching configuration files will discard the edits in this file."
|
|
324
|
+
confirmLabel="Discard changes"
|
|
325
|
+
variant="warning"
|
|
326
|
+
@close="cancelPendingSelection"
|
|
327
|
+
@confirm="discardAndSelect"
|
|
328
|
+
/>
|
|
287
329
|
</div>
|
|
288
330
|
</template>
|
|
@@ -3,12 +3,29 @@ interface ModalProps {
|
|
|
3
3
|
isOpen?: boolean
|
|
4
4
|
title?: string
|
|
5
5
|
description?: string
|
|
6
|
+
ariaLabel?: string
|
|
6
7
|
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
|
|
8
|
+
position?: 'center' | 'top'
|
|
9
|
+
panelClassName?: string
|
|
10
|
+
contentClassName?: string
|
|
11
|
+
showCloseButton?: boolean
|
|
7
12
|
closable?: boolean
|
|
8
13
|
role?: 'dialog' | 'alertdialog'
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
const {
|
|
16
|
+
const {
|
|
17
|
+
isOpen = false,
|
|
18
|
+
title = '',
|
|
19
|
+
description = '',
|
|
20
|
+
ariaLabel = '',
|
|
21
|
+
size = 'md',
|
|
22
|
+
position = 'center',
|
|
23
|
+
panelClassName = '',
|
|
24
|
+
contentClassName = 'p-6',
|
|
25
|
+
showCloseButton = true,
|
|
26
|
+
closable = true,
|
|
27
|
+
role = 'dialog',
|
|
28
|
+
} = defineProps<ModalProps>()
|
|
12
29
|
|
|
13
30
|
const sizeClasses = {
|
|
14
31
|
sm: 'max-w-sm',
|
|
@@ -17,32 +34,41 @@ const sizeClasses = {
|
|
|
17
34
|
xl: 'max-w-4xl',
|
|
18
35
|
full: 'max-w-[calc(100vw-2rem)] max-h-[calc(100vh-2rem)]'
|
|
19
36
|
}
|
|
37
|
+
|
|
38
|
+
const positionClasses = {
|
|
39
|
+
center: 'items-center justify-center p-4',
|
|
40
|
+
top: 'items-start justify-center px-4 pb-4 pt-[15vh]',
|
|
41
|
+
}
|
|
20
42
|
</script>
|
|
21
43
|
<script client>
|
|
22
44
|
const open = useReactiveProp('isOpen', false)
|
|
23
45
|
const liveClosable = useReactiveProp('closable', true)
|
|
24
46
|
const liveTitle = useReactiveProp('title', '')
|
|
25
47
|
const liveDescription = useReactiveProp('description', '')
|
|
48
|
+
const liveAriaLabel = useReactiveProp('ariaLabel', '')
|
|
26
49
|
const liveRole = useReactiveProp('role', 'dialog')
|
|
50
|
+
const liveShowCloseButton = useReactiveProp('showCloseButton', true)
|
|
27
51
|
const dialog = useRef<HTMLDialogElement>('dashboardModalDialog')
|
|
28
52
|
const activeElement = useActiveElement()
|
|
29
53
|
const scrollLocked = useScrollLock()
|
|
30
54
|
const emit = defineEmits()
|
|
31
55
|
let restoreFocus: HTMLElement | null = null
|
|
32
56
|
|
|
33
|
-
|
|
57
|
+
type ModalCloseReason = 'backdrop' | 'button' | 'escape'
|
|
58
|
+
|
|
59
|
+
function closeModal(reason: ModalCloseReason): void {
|
|
34
60
|
if (!liveClosable()) return
|
|
35
|
-
emit('close')
|
|
61
|
+
emit('close', reason)
|
|
36
62
|
}
|
|
37
63
|
|
|
38
64
|
function handleCancel(event: Event): void {
|
|
39
65
|
event.preventDefault()
|
|
40
|
-
closeModal()
|
|
66
|
+
closeModal('escape')
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
function handleBackdropClick(event: MouseEvent): void {
|
|
44
70
|
if (event.target === event.currentTarget)
|
|
45
|
-
closeModal()
|
|
71
|
+
closeModal('backdrop')
|
|
46
72
|
}
|
|
47
73
|
|
|
48
74
|
effect(() => {
|
|
@@ -61,8 +87,11 @@ effect(() => {
|
|
|
61
87
|
const autofocusTarget = element.querySelector<HTMLElement>('[autofocus]:not([disabled])')
|
|
62
88
|
if (autofocusTarget)
|
|
63
89
|
autofocusTarget.focus()
|
|
64
|
-
else
|
|
65
|
-
|
|
90
|
+
else {
|
|
91
|
+
const currentFocus = peek(() => activeElement.value)
|
|
92
|
+
if (!(currentFocus instanceof HTMLElement && element.contains(currentFocus)))
|
|
93
|
+
element.focus()
|
|
94
|
+
}
|
|
66
95
|
})
|
|
67
96
|
return
|
|
68
97
|
}
|
|
@@ -80,17 +109,17 @@ effect(() => {
|
|
|
80
109
|
ref="dashboardModalDialog"
|
|
81
110
|
:role="liveRole()"
|
|
82
111
|
aria-modal="true"
|
|
83
|
-
:aria-label="liveTitle() || 'Dialog'"
|
|
112
|
+
:aria-label="liveAriaLabel() || liveTitle() || 'Dialog'"
|
|
84
113
|
class="fixed inset-0 overflow-y-auto z-[55] m-0 p-0 h-dvh max-h-none max-w-none w-screen bg-transparent dashboard-modal-dialog dashboard-modal-layer"
|
|
85
114
|
@cancel="handleCancel"
|
|
86
115
|
>
|
|
87
116
|
<!-- Modal container -->
|
|
88
|
-
<div class="flex
|
|
117
|
+
<div class="flex min-h-full {{ positionClasses[position] }}" @click="handleBackdropClick">
|
|
89
118
|
<div
|
|
90
|
-
class="relative w-full rounded-xl bg-white dark:bg-neutral-900 shadow-2xl {{ sizeClasses[size] }}"
|
|
119
|
+
class="relative w-full rounded-xl bg-white dark:bg-neutral-900 shadow-2xl {{ sizeClasses[size] }} {{ panelClassName }}"
|
|
91
120
|
>
|
|
92
121
|
<!-- Header -->
|
|
93
|
-
<div :if="liveTitle() || liveClosable()" class="flex gap-4 items-start justify-between p-6 border-b border-neutral-200 dark:border-neutral-700">
|
|
122
|
+
<div :if="liveTitle() || (liveClosable() && liveShowCloseButton())" class="flex gap-4 items-start justify-between p-6 border-b border-neutral-200 dark:border-neutral-700">
|
|
94
123
|
<div class="flex gap-3 items-start min-w-0">
|
|
95
124
|
<slot name="header-leading" />
|
|
96
125
|
<div class="min-w-0">
|
|
@@ -106,12 +135,12 @@ effect(() => {
|
|
|
106
135
|
<div class="flex flex-shrink-0 gap-2 items-center">
|
|
107
136
|
<slot name="header-trailing" />
|
|
108
137
|
<Button
|
|
109
|
-
:if="liveClosable()"
|
|
138
|
+
:if="liveClosable() && liveShowCloseButton()"
|
|
110
139
|
variant="ghost"
|
|
111
140
|
size="sm"
|
|
112
141
|
iconOnly
|
|
113
142
|
ariaLabel="Close modal"
|
|
114
|
-
@click="closeModal"
|
|
143
|
+
@click="closeModal('button')"
|
|
115
144
|
>
|
|
116
145
|
<div class="h-5 w-5 i-hugeicons-cancel-01"></div>
|
|
117
146
|
</Button>
|
|
@@ -120,7 +149,7 @@ effect(() => {
|
|
|
120
149
|
|
|
121
150
|
<!-- Body -->
|
|
122
151
|
@if($slots.default)
|
|
123
|
-
<div class="
|
|
152
|
+
<div class="{{ contentClassName }}">
|
|
124
153
|
<slot />
|
|
125
154
|
</div>
|
|
126
155
|
@endif
|
|
@@ -326,12 +326,7 @@ function clearFilters(): void {
|
|
|
326
326
|
|
|
327
327
|
/* ----------------------------------------------------------------- export */
|
|
328
328
|
|
|
329
|
-
|
|
330
|
-
* CSV of what is on screen: current search, filters and sort, all columns
|
|
331
|
-
* that are visible, every page rather than just the current one. Exporting
|
|
332
|
-
* the raw prop would hand back something the viewer never asked for.
|
|
333
|
-
*/
|
|
334
|
-
function exportCsv(): void {
|
|
329
|
+
function csvBlob(): Blob {
|
|
335
330
|
const cols = visibleColumns()
|
|
336
331
|
const escape = (value: any): string => {
|
|
337
332
|
const text = value === null || value === undefined ? '' : String(value)
|
|
@@ -343,15 +338,21 @@ function exportCsv(): void {
|
|
|
343
338
|
...sorted().map(row => cols.map(column => escape(rawValue(row, column.key))).join(',')),
|
|
344
339
|
]
|
|
345
340
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
341
|
+
return new Blob([`${lines.join('\n')}`], { type: 'text/csv;charset=utf-8' })
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* CSV of what is on screen: current search, filters and sort, all columns
|
|
346
|
+
* that are visible, every page rather than just the current one. Exporting
|
|
347
|
+
* the raw prop would hand back something the viewer never asked for.
|
|
348
|
+
* useObjectUrl owns URL revocation while the native download link owns the
|
|
349
|
+
* browser interaction.
|
|
350
|
+
*/
|
|
351
|
+
const exportBlob = derived(() => exportable() ? csvBlob() : null)
|
|
352
|
+
const exportUrl = useObjectUrl(exportBlob)
|
|
353
|
+
const exportDownload = derived(() => `${exportFilename()}.csv`)
|
|
354
|
+
|
|
355
|
+
function recordExport(): void {
|
|
355
356
|
emit('export', sorted().length)
|
|
356
357
|
}
|
|
357
358
|
|
|
@@ -420,14 +421,14 @@ function cellClass(column: DashboardTableColumn, index: number): string {
|
|
|
420
421
|
/>
|
|
421
422
|
</div>
|
|
422
423
|
|
|
423
|
-
<
|
|
424
|
+
<Button
|
|
424
425
|
:if="hasActiveFilters()"
|
|
425
|
-
|
|
426
|
+
variant="ghost"
|
|
427
|
+
size="xs"
|
|
426
428
|
@click="clearFilters()"
|
|
427
|
-
class="px-2.5 py-1.5 text-neutral-600 text-xs dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-white"
|
|
428
429
|
>
|
|
429
430
|
Clear filters
|
|
430
|
-
</
|
|
431
|
+
</Button>
|
|
431
432
|
|
|
432
433
|
<slot name="toolbar" />
|
|
433
434
|
</div>
|
|
@@ -463,15 +464,19 @@ function cellClass(column: DashboardTableColumn, index: number): string {
|
|
|
463
464
|
</div>
|
|
464
465
|
</div>
|
|
465
466
|
|
|
466
|
-
<
|
|
467
|
+
<Button
|
|
467
468
|
:if="exportable()"
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
469
|
+
tag="a"
|
|
470
|
+
variant="outline"
|
|
471
|
+
size="sm"
|
|
472
|
+
:href="exportUrl.value || ''"
|
|
473
|
+
:download="exportDownload()"
|
|
474
|
+
:disabled="!exportUrl.value"
|
|
475
|
+
@click="recordExport()"
|
|
471
476
|
>
|
|
472
477
|
<span aria-hidden="true" class="h-3.5 w-3.5 i-hugeicons-download-04"></span>
|
|
473
478
|
Export CSV
|
|
474
|
-
</
|
|
479
|
+
</Button>
|
|
475
480
|
</div>
|
|
476
481
|
</div>
|
|
477
482
|
|
|
@@ -588,7 +593,7 @@ function cellClass(column: DashboardTableColumn, index: number): string {
|
|
|
588
593
|
<span aria-hidden="true" class="mb-3 p-3 h-12 w-12 text-neutral-400 bg-neutral-100 dark:bg-neutral-800 rounded-full i-hugeicons-search-remove"></span>
|
|
589
594
|
<p class="font-medium text-neutral-700 text-sm dark:text-neutral-200">No matches</p>
|
|
590
595
|
<p class="mt-1 text-neutral-500 text-sm dark:text-neutral-400">No rows match the current search or filters.</p>
|
|
591
|
-
<
|
|
596
|
+
<Button variant="ghost" size="sm" class="mt-3" @click="clearFilters()">Clear filters</Button>
|
|
592
597
|
</div>
|
|
593
598
|
|
|
594
599
|
<div :if="visibleRows().length === 0 && !loading() && !hasActiveFilters()" class="flex flex-col items-center justify-center px-4 py-12">
|
|
@@ -281,6 +281,7 @@ export function buildNavSections(
|
|
|
281
281
|
// (`/notifications/dashboard`); the bare `/notifications` redirects.
|
|
282
282
|
sections.push(['app', 'app', [
|
|
283
283
|
{ to: '/deployments', icon: 'rocket', text: 'Deployments' },
|
|
284
|
+
{ to: '/operations/scheduler', icon: 'calendar-03', text: 'Operations' },
|
|
284
285
|
{ to: '/requests', icon: 'api', text: 'Requests' },
|
|
285
286
|
{ to: '/realtime', icon: 'link', text: 'Realtime' },
|
|
286
287
|
{ to: '/actions', icon: 'bolt', text: 'Actions' },
|
package/routes/dashboard-api.ts
CHANGED
|
@@ -161,6 +161,9 @@ route.group({ prefix: '/api/dashboard', apiResponse: true }, () => {
|
|
|
161
161
|
guard(route.delete('/commerce/print-logs/{id}', 'Actions/Commerce/ReceiptDestroyAction'))
|
|
162
162
|
guard(route.get('/deployments', 'Actions/Dashboard/Deployments/GetDeployments'))
|
|
163
163
|
guard(route.post('/deployments', 'Actions/Dashboard/Deployments/CreateDeployment'))
|
|
164
|
+
guard(route.post('/deployments/preview', 'Actions/Dashboard/Deployments/PreviewDeployment'))
|
|
165
|
+
guard(route.post('/deployments/rollback/preview', 'Actions/Dashboard/Deployments/PreviewDeploymentRollback'))
|
|
166
|
+
guard(route.post('/deployments/rollback', 'Actions/Dashboard/Deployments/CreateDeploymentRollback'))
|
|
164
167
|
guard(route.get('/deployments/count', 'Actions/Dashboard/Deployments/GetDeploymentCount'))
|
|
165
168
|
guard(route.get('/deployments/recent', 'Actions/Dashboard/Deployments/GetRecentDeployments'))
|
|
166
169
|
guard(route.get('/deployments/avg-time', 'Actions/Dashboard/Deployments/GetAverageDeploymentTime'))
|
|
@@ -168,6 +171,26 @@ route.group({ prefix: '/api/dashboard', apiResponse: true }, () => {
|
|
|
168
171
|
guard(route.put('/deployments/script', 'Actions/Dashboard/Deployments/UpdateDeployScript'))
|
|
169
172
|
guard(route.get('/deployments/terminal', 'Actions/Dashboard/Deployments/GetDeploymentLiveTerminalOutput'))
|
|
170
173
|
guard(route.get('/deployments/{id}', 'Actions/Dashboard/Deployments/GetDeployment'))
|
|
174
|
+
guard(route.get('/operations/scheduler', 'Actions/Dashboard/Operations/SchedulerIndexAction'))
|
|
175
|
+
guard(route.post('/operations/scheduler/{name}/run', 'Actions/Dashboard/Operations/SchedulerRunAction'))
|
|
176
|
+
guard(route.patch('/operations/scheduler/{name}', 'Actions/Dashboard/Operations/SchedulerToggleAction'))
|
|
177
|
+
guard(route.get('/operations/recovery', 'Actions/Dashboard/Operations/RecoveryIndexAction'))
|
|
178
|
+
guard(route.post('/operations/recovery/destinations', 'Actions/Dashboard/Operations/RecoveryDestinationStoreAction'))
|
|
179
|
+
guard(route.post('/operations/recovery/destinations/{id}/test', 'Actions/Dashboard/Operations/RecoveryDestinationTestAction'))
|
|
180
|
+
guard(route.post('/operations/recovery/policies', 'Actions/Dashboard/Operations/RecoveryPolicyStoreAction'))
|
|
181
|
+
guard(route.post('/operations/recovery/policies/{id}/run', 'Actions/Dashboard/Operations/RecoveryRunAction'))
|
|
182
|
+
guard(route.post('/operations/recovery/points/{id}/verify', 'Actions/Dashboard/Operations/RecoveryVerifyAction'))
|
|
183
|
+
guard(route.post('/operations/recovery/points/{id}/restore', 'Actions/Dashboard/Operations/RecoveryRestoreAction'))
|
|
184
|
+
guard(route.patch('/operations/recovery/points/{id}/protection', 'Actions/Dashboard/Operations/RecoveryProtectAction'))
|
|
185
|
+
guard(route.post('/operations/recovery/retention', 'Actions/Dashboard/Operations/RecoveryRetentionAction'))
|
|
186
|
+
guard(route.get('/operations/migrations', 'Actions/Dashboard/Operations/MigrationIndexAction'))
|
|
187
|
+
guard(route.post('/operations/migrations/apply', 'Actions/Dashboard/Operations/MigrationApplyAction'))
|
|
188
|
+
guard(route.post('/operations/migrations/reconcile', 'Actions/Dashboard/Operations/MigrationReconcileAction'))
|
|
189
|
+
guard(route.get('/operations/changes', 'Actions/Dashboard/Operations/ChangeIndexAction'))
|
|
190
|
+
guard(route.post('/operations/changes/releases/{id}/decision', 'Actions/Dashboard/Operations/ChangeApprovalAction'))
|
|
191
|
+
guard(route.get('/operations/incidents', 'Actions/Dashboard/Operations/IncidentIndexAction'))
|
|
192
|
+
guard(route.patch('/operations/incidents/{id}', 'Actions/Dashboard/Operations/IncidentUpdateAction'))
|
|
193
|
+
guard(route.get('/operations/audit', 'Actions/Dashboard/Operations/AuditIndexAction'))
|
|
171
194
|
guard(route.get('/data/activity', 'Actions/Dashboard/Data/ActivityIndexAction'))
|
|
172
195
|
guard(route.get('/data/users', 'Actions/Dashboard/Data/UserIndexAction'))
|
|
173
196
|
guard(route.get('/data/teams', 'Actions/Dashboard/Data/TeamIndexAction'))
|
package/routes/dashboard.ts
CHANGED
|
@@ -647,6 +647,7 @@ route.group({ prefix: '/deployments', middleware: 'auth' }, () => {
|
|
|
647
647
|
route.get('/recent', 'Actions/Dashboard/Deployments/GetRecentDeployments')
|
|
648
648
|
route.get('/avg-time', 'Actions/Dashboard/Deployments/GetAverageDeploymentTime')
|
|
649
649
|
route.post('/', 'Actions/Dashboard/Deployments/CreateDeployment')
|
|
650
|
+
route.post('/preview', 'Actions/Dashboard/Deployments/PreviewDeployment')
|
|
650
651
|
route.get('/script', 'Actions/Dashboard/Deployments/GetDeployScript')
|
|
651
652
|
route.put('/script', 'Actions/Dashboard/Deployments/UpdateDeployScript')
|
|
652
653
|
route.get('/terminal', 'Actions/Dashboard/Deployments/GetDeploymentLiveTerminalOutput')
|
package/views/dashboard/AUDIT.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Dashboard audit
|
|
2
2
|
|
|
3
|
-
Last refreshed: 2026-
|
|
3
|
+
Last refreshed: 2026-08-11
|
|
4
4
|
|
|
5
5
|
This is the current verification record for `./buddy dev --dashboard`. It
|
|
6
6
|
replaces the May 2026 migration inventory, whose per-file counts no longer
|
|
@@ -11,23 +11,25 @@ described the componentized dashboard.
|
|
|
11
11
|
| Surface | Count |
|
|
12
12
|
|---|---:|
|
|
13
13
|
| Dashboard STX view files | 120 |
|
|
14
|
-
| Dashboard STX components |
|
|
15
|
-
| Dashboard Actions |
|
|
14
|
+
| Dashboard STX components | 303 |
|
|
15
|
+
| Dashboard Actions | 368 |
|
|
16
16
|
| Registered `/api/dashboard/*` routes | 283 |
|
|
17
|
-
| Framework model files |
|
|
18
|
-
| Framework models declaring `useApi` |
|
|
19
|
-
| Direct `fetch()` calls in dashboard views, components,
|
|
17
|
+
| Framework model files | 75 |
|
|
18
|
+
| Framework models declaring `useApi` | 64 |
|
|
19
|
+
| Direct `fetch()` calls in dashboard views, components, and stores | 0 |
|
|
20
20
|
|
|
21
|
-
The
|
|
22
|
-
records: order idempotency keys, order items,
|
|
23
|
-
|
|
21
|
+
The eleven models without `useApi` are internal, relationship-owned, or
|
|
22
|
+
provider-owned records: driver pings, order idempotency keys, order items,
|
|
23
|
+
errors, jobs, failed jobs, payment methods, payment products, payment
|
|
24
|
+
transactions, social accounts, and subscriptions. They are not missing generic
|
|
25
|
+
CRUD surfaces.
|
|
24
26
|
|
|
25
27
|
## Verified contracts
|
|
26
28
|
|
|
27
29
|
### Rendering and navigation
|
|
28
30
|
|
|
29
|
-
- `./buddy dev --dashboard` starts on port 3002 and renders with STX 0.2.
|
|
30
|
-
and Crosswind 0.2.
|
|
31
|
+
- `./buddy dev --dashboard` starts on port 3002 and renders with STX 0.2.176
|
|
32
|
+
and Crosswind 0.2.16.
|
|
31
33
|
- 108 static route views render with HTTP 200.
|
|
32
34
|
- Full-page and STX fragment requests render for every static route.
|
|
33
35
|
- STX prewarms the dependency-aware rendered shell cache with four workers.
|
|
@@ -37,12 +39,14 @@ payment-provider records. They are not missing generic CRUD surfaces.
|
|
|
37
39
|
a dashboard composable invalidates the associated client bundle.
|
|
38
40
|
- The componentized catch-all renders the native not-found page with HTTP 404
|
|
39
41
|
for both full-page and STX fragment requests.
|
|
40
|
-
-
|
|
42
|
+
- A dependency-free browser crawl followed the complete rendered link graph
|
|
43
|
+
across 261 distinct page URLs with no HTTP, console, request, or horizontal
|
|
44
|
+
overflow failure.
|
|
41
45
|
- Rendered pages contain no unresolved PascalCase component tags.
|
|
42
46
|
- Rendered pages contain no duplicate emitted IDs or broken
|
|
43
47
|
`aria-labelledby` references.
|
|
44
48
|
- The live render and navigation audit covers all 108 static destinations,
|
|
45
|
-
all
|
|
49
|
+
all 75 discovered model destinations, and the remaining eight parameterized
|
|
46
50
|
route families without a full-page or STX fragment failure.
|
|
47
51
|
- Headless desktop and narrow-viewport visual checks cover the componentized
|
|
48
52
|
Terms of Service and Privacy Policy routes linked from registration.
|
|
@@ -92,9 +96,9 @@ payment-provider records. They are not missing generic CRUD surfaces.
|
|
|
92
96
|
|
|
93
97
|
### Data integrity
|
|
94
98
|
|
|
95
|
-
- The
|
|
99
|
+
- The 129 live GET dashboard API routes return JSON or their documented text
|
|
96
100
|
payload with no hidden HTTP-200 error body.
|
|
97
|
-
- A fresh live
|
|
101
|
+
- A fresh live audit returned 118 successful responses and 11 intentional
|
|
98
102
|
validation, authentication, or missing-resource responses, with no 5xx,
|
|
99
103
|
method mismatch, transport failure, or HTML fallback.
|
|
100
104
|
- Generic model routes reject malformed slugs with HTTP 400 before deriving
|
|
@@ -118,6 +122,7 @@ payment-provider records. They are not missing generic CRUD surfaces.
|
|
|
118
122
|
bun storage/framework/defaults/ai/skills/stacks-dashboard/scripts/audit.ts
|
|
119
123
|
# Or target a non-default origin:
|
|
120
124
|
bun storage/framework/defaults/ai/skills/stacks-dashboard/scripts/audit.ts --base-url http://127.0.0.1:3002
|
|
125
|
+
bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts crawl http://localhost:3002/ --max 500 --settle 350 --summary
|
|
121
126
|
bun test tests/unit/dashboard-*.test.ts
|
|
122
127
|
bun test storage/framework/core/actions/tests/dev-csrf.test.ts
|
|
123
128
|
bun run typecheck
|
|
@@ -128,7 +133,7 @@ bunx --bun pickier .
|
|
|
128
133
|
The focused contracts cover buttons, native STX bindings, route and Action
|
|
129
134
|
method alignment, model reads and writes, commerce mutations, deployment
|
|
130
135
|
guards, navigation source, sidebar behavior, toasts, and skill documentation.
|
|
131
|
-
The 2026-
|
|
136
|
+
The 2026-08-11 focused run completed 226 tests with 5,424 assertions and no
|
|
132
137
|
failures, followed by both TypeScript checks and a clean repository lint.
|
|
133
138
|
|
|
134
139
|
## Remaining verification boundaries
|
|
@@ -24,8 +24,8 @@ interface RepoStatus {
|
|
|
24
24
|
updatedAt: string | null
|
|
25
25
|
runUrl: string | null
|
|
26
26
|
failedJobs: FailedJob[]
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
buddyBotPRs: number
|
|
28
|
+
buddyBotPRsUrl: string | null
|
|
29
29
|
actionsPRs: number
|
|
30
30
|
actionsPRsUrl: string | null
|
|
31
31
|
}
|