@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.
Files changed (90) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +9 -1
  2. package/ai/skills/stacks-dashboard/SKILL.md +71 -14
  3. package/ai/skills/stacks-deploy/SKILL.md +18 -0
  4. package/ai/skills/stacks-desktop/SKILL.md +11 -2
  5. package/ai/skills/stacks-stx/SKILL.md +25 -0
  6. package/app/Actions/Auth/LoginAction.ts +11 -2
  7. package/app/Actions/Auth/RefreshTokenAction.ts +6 -2
  8. package/app/Actions/Auth/RegisterAction.ts +6 -2
  9. package/app/Actions/Auth/VerifyTwoFactorLoginAction.ts +6 -2
  10. package/app/Actions/Dashboard/Deployments/CreateDeployment.ts +64 -44
  11. package/app/Actions/Dashboard/Deployments/CreateDeploymentRollback.ts +26 -0
  12. package/app/Actions/Dashboard/Deployments/PreviewDeployment.ts +39 -0
  13. package/app/Actions/Dashboard/Deployments/PreviewDeploymentRollback.ts +22 -0
  14. package/app/Actions/Dashboard/Deployments/deployment-input.test.ts +12 -2
  15. package/app/Actions/Dashboard/Deployments/deployment-input.ts +5 -3
  16. package/app/Actions/Dashboard/Deployments/deployment-preview.test.ts +29 -0
  17. package/app/Actions/Dashboard/Deployments/deployment-preview.ts +41 -0
  18. package/app/Actions/Dashboard/Deployments/deployment-rollback.test.ts +16 -0
  19. package/app/Actions/Dashboard/Deployments/deployment-rollback.ts +89 -0
  20. package/app/Actions/Dashboard/Operations/AuditIndexAction.ts +25 -0
  21. package/app/Actions/Dashboard/Operations/ChangeApprovalAction.ts +28 -0
  22. package/app/Actions/Dashboard/Operations/ChangeIndexAction.ts +32 -0
  23. package/app/Actions/Dashboard/Operations/IncidentIndexAction.ts +32 -0
  24. package/app/Actions/Dashboard/Operations/IncidentUpdateAction.ts +34 -0
  25. package/app/Actions/Dashboard/Operations/MigrationApplyAction.ts +30 -0
  26. package/app/Actions/Dashboard/Operations/MigrationIndexAction.ts +24 -0
  27. package/app/Actions/Dashboard/Operations/MigrationReconcileAction.ts +23 -0
  28. package/app/Actions/Dashboard/Operations/RecoveryDestinationStoreAction.ts +91 -0
  29. package/app/Actions/Dashboard/Operations/RecoveryDestinationTestAction.ts +34 -0
  30. package/app/Actions/Dashboard/Operations/RecoveryIndexAction.ts +58 -0
  31. package/app/Actions/Dashboard/Operations/RecoveryPolicyStoreAction.ts +90 -0
  32. package/app/Actions/Dashboard/Operations/RecoveryProtectAction.ts +32 -0
  33. package/app/Actions/Dashboard/Operations/RecoveryRestoreAction.ts +72 -0
  34. package/app/Actions/Dashboard/Operations/RecoveryRetentionAction.ts +17 -0
  35. package/app/Actions/Dashboard/Operations/RecoveryRunAction.ts +25 -0
  36. package/app/Actions/Dashboard/Operations/RecoveryVerifyAction.ts +24 -0
  37. package/app/Actions/Dashboard/Operations/SchedulerIndexAction.ts +34 -0
  38. package/app/Actions/Dashboard/Operations/SchedulerRunAction.ts +33 -0
  39. package/app/Actions/Dashboard/Operations/SchedulerToggleAction.ts +36 -0
  40. package/app/Actions/Dashboard/Operations/control-plane.ts +150 -0
  41. package/app/Actions/Dashboard/Operations/migration-operations.ts +80 -0
  42. package/app/Actions/Dashboard/Operations/operations-runtime.ts +11 -0
  43. package/app/Actions/Dashboard/Operations/recovery-input.test.ts +22 -0
  44. package/app/Actions/Dashboard/Operations/recovery-input.ts +33 -0
  45. package/app/Actions/Dashboard/Operations/recovery-runtime.ts +99 -0
  46. package/app/Actions/Dashboard/Operations/scheduler-operations.test.ts +23 -0
  47. package/app/Actions/Dashboard/Operations/scheduler-operations.ts +138 -0
  48. package/functions/deployments.ts +18 -0
  49. package/functions/operations.ts +361 -0
  50. package/ide/vscode/package.json +1 -1
  51. package/package.json +1 -1
  52. package/resources/components/Dashboard/Auth/ForgotPasswordDashboard.stx +0 -2
  53. package/resources/components/Dashboard/Auth/LoginDashboard.stx +1 -1
  54. package/resources/components/Dashboard/Auth/RegisterDashboard.stx +1 -3
  55. package/resources/components/Dashboard/Ci/CiDashboard.stx +4 -4
  56. package/resources/components/Dashboard/Deployments/DeploymentList.stx +147 -14
  57. package/resources/components/Dashboard/Deployments/DeploymentPreviewDialog.stx +118 -0
  58. package/resources/components/Dashboard/GlobalSearch.stx +87 -91
  59. package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +9 -435
  60. package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +416 -0
  61. package/resources/components/Dashboard/Modals/BaseModal.stx +13 -36
  62. package/resources/components/Dashboard/Modals/Popups/Alert.stx +18 -44
  63. package/resources/components/Dashboard/Operations/AuditDashboard.stx +16 -0
  64. package/resources/components/Dashboard/Operations/ChangeDashboard.stx +66 -0
  65. package/resources/components/Dashboard/Operations/IncidentDashboard.stx +39 -0
  66. package/resources/components/Dashboard/Operations/MigrationDashboard.stx +146 -0
  67. package/resources/components/Dashboard/Operations/OperationHistory.stx +61 -0
  68. package/resources/components/Dashboard/Operations/OperationsNavigation.stx +32 -0
  69. package/resources/components/Dashboard/Operations/RecoveryDashboard.stx +229 -0
  70. package/resources/components/Dashboard/Operations/RecoveryDestinationDialog.stx +126 -0
  71. package/resources/components/Dashboard/Operations/RecoveryJobList.stx +32 -0
  72. package/resources/components/Dashboard/Operations/RecoveryPointTable.stx +82 -0
  73. package/resources/components/Dashboard/Operations/RecoveryPolicyDialog.stx +155 -0
  74. package/resources/components/Dashboard/Operations/RecoveryRestoreDialog.stx +90 -0
  75. package/resources/components/Dashboard/Operations/SchedulerDashboard.stx +144 -0
  76. package/resources/components/Dashboard/Operations/SchedulerTaskTable.stx +81 -0
  77. package/resources/components/Dashboard/Settings/SettingsDashboard.stx +45 -3
  78. package/resources/components/Dashboard/UI/Modal.stx +43 -14
  79. package/resources/components/Dashboard/UI/Table.stx +30 -25
  80. package/resources/functions/dashboard/sidebar.ts +1 -0
  81. package/routes/dashboard-api.ts +23 -0
  82. package/routes/dashboard.ts +1 -0
  83. package/views/dashboard/AUDIT.md +21 -16
  84. package/views/dashboard/operations/audit.stx +5 -0
  85. package/views/dashboard/operations/changes.stx +5 -0
  86. package/views/dashboard/operations/incidents.stx +5 -0
  87. package/views/dashboard/operations/migrations.stx +8 -0
  88. package/views/dashboard/operations/recovery.stx +8 -0
  89. package/views/dashboard/operations/scheduler.stx +8 -0
  90. package/views/dashboard/stores/ci.ts +2 -2
@@ -0,0 +1,33 @@
1
+ import type { BackupResourceKind, JsonValue } from '@stacksjs/ts-cloud'
2
+
3
+ export const RECOVERY_KINDS: BackupResourceKind[] = [
4
+ 'managed_database',
5
+ 'logical_database',
6
+ 'volume',
7
+ 'files',
8
+ 'control_plane',
9
+ 'infrastructure',
10
+ ]
11
+
12
+ export function stringValue(value: unknown): string {
13
+ return typeof value === 'string' ? value.trim() : ''
14
+ }
15
+
16
+ export function stringList(value: unknown): string[] {
17
+ return Array.isArray(value)
18
+ ? value.map(stringValue).filter(Boolean)
19
+ : typeof value === 'string'
20
+ ? value.split(',').map(entry => entry.trim()).filter(Boolean)
21
+ : []
22
+ }
23
+
24
+ export function positiveNumber(value: unknown, fallback: number, maximum = Number.MAX_SAFE_INTEGER): number {
25
+ const number = Number(value)
26
+ return Number.isFinite(number) && number > 0 ? Math.min(maximum, number) : fallback
27
+ }
28
+
29
+ export function jsonObject(value: unknown): Record<string, JsonValue> {
30
+ if (!value || typeof value !== 'object' || Array.isArray(value))
31
+ return {}
32
+ return value as Record<string, JsonValue>
33
+ }
@@ -0,0 +1,99 @@
1
+ import type {
2
+ BackupDestination,
3
+ BackupPolicy,
4
+ BackupSourceAdapter,
5
+ JsonValue,
6
+ } from '@stacksjs/ts-cloud'
7
+ import { join } from 'node:path'
8
+ import process from 'node:process'
9
+ import {
10
+ AwsDatabaseBackupSource,
11
+ AwsInfrastructureBackupSource,
12
+ BackupCoordinator,
13
+ BackupStore,
14
+ ControlPlaneBackupSource,
15
+ createBackupQueueHandlers,
16
+ DataServiceStore,
17
+ DockerVolumeBackupSource,
18
+ DurableOperationQueue,
19
+ DurableQueueWorker,
20
+ EncryptedDataSecretStore,
21
+ FilesystemBackupSource,
22
+ LogicalDatabaseBackupSource,
23
+ resolveAuthEncryptionKey,
24
+ S3BackupDestinationAdapter,
25
+ } from '@stacksjs/ts-cloud'
26
+ import { operationsControlPlane, operationsEnvironment } from './control-plane'
27
+
28
+ export interface RecoveryRuntime {
29
+ store: BackupStore
30
+ coordinator: BackupCoordinator
31
+ queue: DurableOperationQueue
32
+ worker: DurableQueueWorker
33
+ dataServices: DataServiceStore
34
+ secrets: EncryptedDataSecretStore
35
+ }
36
+
37
+ let runtime: RecoveryRuntime | undefined
38
+
39
+ export function recoveryRuntime(): RecoveryRuntime {
40
+ if (runtime)
41
+ return runtime
42
+
43
+ const controlPlane = operationsControlPlane()
44
+ const store = new BackupStore(controlPlane.store)
45
+ const queue = new DurableOperationQueue(controlPlane.store, { workerId: `stacks-dashboard:${process.pid}` })
46
+ const coordinator = new BackupCoordinator(store, queue)
47
+ const dataServices = new DataServiceStore(controlPlane.store)
48
+ const secrets = new EncryptedDataSecretStore(controlPlane.store, resolveAuthEncryptionKey(process.cwd()))
49
+ const destination = new S3BackupDestinationAdapter(secrets)
50
+
51
+ const resolveSource = (policy: BackupPolicy): BackupSourceAdapter | undefined => {
52
+ if (policy.resourceKind === 'managed_database')
53
+ return new AwsDatabaseBackupSource(dataServices)
54
+ if (policy.resourceKind === 'logical_database')
55
+ return new LogicalDatabaseBackupSource(dataServices, secrets)
56
+ if (policy.resourceKind === 'volume')
57
+ return new DockerVolumeBackupSource()
58
+ if (policy.resourceKind === 'files')
59
+ return new FilesystemBackupSource(process.cwd(), undefined, join(process.cwd(), 'storage', 'cloud', 'restores'))
60
+ if (policy.resourceKind === 'control_plane')
61
+ return new ControlPlaneBackupSource(controlPlane.store, process.cwd(), join(process.cwd(), 'storage', 'cloud', 'restores'))
62
+ if (policy.resourceKind === 'infrastructure')
63
+ return new AwsInfrastructureBackupSource()
64
+ return undefined
65
+ }
66
+
67
+ const handlers = createBackupQueueHandlers({
68
+ store,
69
+ queue,
70
+ resolveSource,
71
+ resolveDestination: (value: BackupDestination) => value.provider === 'aws_backup' ? undefined : destination,
72
+ })
73
+ const worker = new DurableQueueWorker(queue, handlers, {
74
+ parallelism: 2,
75
+ pollIntervalMs: 2_000,
76
+ onError: error => console.error('[dashboard/recovery] worker failed:', error),
77
+ }).start()
78
+
79
+ runtime = { store, coordinator, queue, worker, dataServices, secrets }
80
+ return runtime
81
+ }
82
+
83
+ export function recoveryEnvironment() {
84
+ const controlPlane = operationsControlPlane()
85
+ const environment = operationsEnvironment(controlPlane)
86
+ if (!environment)
87
+ throw new Error('No cloud environment is configured for recovery operations.')
88
+ return { controlPlane, environment }
89
+ }
90
+
91
+ export function safeRecoveryDestination(destination: BackupDestination): Record<string, JsonValue> {
92
+ return {
93
+ ...destination,
94
+ credentialRef: null,
95
+ encryptionKeyRef: null,
96
+ credentialsConfigured: Boolean(destination.credentialRef),
97
+ clientEncryptionConfigured: Boolean(destination.encryptionKeyRef),
98
+ }
99
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { parseSchedulerRegistry, SchedulerOperationError } from './scheduler-operations'
3
+
4
+ describe('scheduler operations', () => {
5
+ it('parses the Buddy scheduler registry marker', () => {
6
+ const tasks = parseSchedulerRegistry([
7
+ 'loading application',
8
+ 'STACKS_SCHEDULE_JSON={"jobs":[{"name":"Cleanup","pattern":"0 * * * *","timezone":"UTC","nextRun":"2026-08-12T12:00:00.000Z","enabled":false}],"locks":[]}',
9
+ ].join('\n'))
10
+
11
+ expect(tasks).toEqual([{
12
+ name: 'Cleanup',
13
+ pattern: '0 * * * *',
14
+ timezone: 'UTC',
15
+ nextRun: '2026-08-12T12:00:00.000Z',
16
+ enabled: false,
17
+ }])
18
+ })
19
+
20
+ it('rejects unmarked output instead of guessing', () => {
21
+ expect(() => parseSchedulerRegistry('{"jobs":[]}')).toThrow(SchedulerOperationError)
22
+ })
23
+ })
@@ -0,0 +1,138 @@
1
+ import { join } from 'node:path'
2
+ import process from 'node:process'
3
+
4
+ export interface SchedulerTask {
5
+ name: string
6
+ pattern: string
7
+ timezone: string
8
+ nextRun: string | null
9
+ enabled: boolean
10
+ }
11
+
12
+ interface BuddyResult {
13
+ stdout: string
14
+ stderr: string
15
+ exitCode: number
16
+ }
17
+
18
+ interface SchedulerRegistryPayload {
19
+ jobs?: unknown
20
+ }
21
+
22
+ export class SchedulerOperationError extends Error {
23
+ constructor(message: string) {
24
+ super(message)
25
+ this.name = 'SchedulerOperationError'
26
+ }
27
+ }
28
+
29
+ async function runBuddy(args: string[], timeoutMs = 30_000): Promise<BuddyResult> {
30
+ const subprocess = Bun.spawn([join(process.cwd(), 'buddy'), ...args], {
31
+ cwd: process.cwd(),
32
+ env: { ...process.env, NO_COLOR: '1' },
33
+ stdout: 'pipe',
34
+ stderr: 'pipe',
35
+ })
36
+
37
+ let timeout: ReturnType<typeof setTimeout> | undefined
38
+ const timedOut = new Promise<never>((_resolve, reject) => {
39
+ timeout = setTimeout(() => {
40
+ subprocess.kill()
41
+ reject(new SchedulerOperationError(`buddy ${args[0]} timed out.`))
42
+ }, timeoutMs)
43
+ })
44
+
45
+ try {
46
+ const [stdout, stderr, exitCode] = await Promise.race([
47
+ Promise.all([
48
+ new Response(subprocess.stdout).text(),
49
+ new Response(subprocess.stderr).text(),
50
+ subprocess.exited,
51
+ ]),
52
+ timedOut,
53
+ ])
54
+ return { stdout, stderr, exitCode }
55
+ }
56
+ finally {
57
+ if (timeout)
58
+ clearTimeout(timeout)
59
+ }
60
+ }
61
+
62
+ export function parseSchedulerRegistry(output: string): SchedulerTask[] {
63
+ const marker = 'STACKS_SCHEDULE_JSON='
64
+ const line = output.split(/\r?\n/).find(entry => entry.startsWith(marker))
65
+ if (!line)
66
+ throw new SchedulerOperationError('The scheduler registry did not return machine-readable output.')
67
+
68
+ let payload: SchedulerRegistryPayload
69
+ try {
70
+ payload = JSON.parse(line.slice(marker.length)) as SchedulerRegistryPayload
71
+ }
72
+ catch {
73
+ throw new SchedulerOperationError('The scheduler registry returned invalid JSON.')
74
+ }
75
+
76
+ if (!Array.isArray(payload.jobs))
77
+ throw new SchedulerOperationError('The scheduler registry response did not include a task list.')
78
+
79
+ return payload.jobs.map((value) => {
80
+ if (!value || typeof value !== 'object')
81
+ throw new SchedulerOperationError('The scheduler registry returned an invalid task.')
82
+ const job = value as Record<string, unknown>
83
+ if (typeof job.name !== 'string' || !job.name.trim())
84
+ throw new SchedulerOperationError('A scheduled task is missing its name.')
85
+ return {
86
+ name: job.name,
87
+ pattern: typeof job.pattern === 'string' ? job.pattern : '',
88
+ timezone: typeof job.timezone === 'string' ? job.timezone : 'UTC',
89
+ nextRun: typeof job.nextRun === 'string' ? job.nextRun : null,
90
+ enabled: job.enabled !== false,
91
+ }
92
+ })
93
+ }
94
+
95
+ export async function listSchedulerTasks(): Promise<SchedulerTask[]> {
96
+ const result = await runBuddy(['schedule:list', '--json'])
97
+ if (result.exitCode !== 0)
98
+ throw new SchedulerOperationError(result.stderr.trim() || 'The scheduler registry could not be loaded.')
99
+ return parseSchedulerRegistry(result.stdout)
100
+ }
101
+
102
+ async function requireSchedulerTask(name: string): Promise<SchedulerTask> {
103
+ const normalized = name.trim()
104
+ const task = (await listSchedulerTasks()).find(entry => entry.name === normalized)
105
+ if (!task)
106
+ throw new SchedulerOperationError(`Scheduled task "${normalized}" was not found.`)
107
+ return task
108
+ }
109
+
110
+ export async function runScheduledTask(name: string): Promise<{ message: string, task: string }> {
111
+ const task = await requireSchedulerTask(name)
112
+ if (!task.enabled)
113
+ throw new SchedulerOperationError(`Scheduled task "${task.name}" is paused.`)
114
+ const result = await runBuddy(['schedule:run-one', task.name], 5 * 60_000)
115
+ if (result.exitCode !== 0)
116
+ throw new SchedulerOperationError(result.stderr.trim() || `Scheduled task "${task.name}" failed.`)
117
+ return { message: `Scheduled task ${task.name} completed.`, task: task.name }
118
+ }
119
+
120
+ export async function setScheduledTaskEnabled(name: string, enabled: boolean): Promise<{ enabled: boolean, message: string, task: string }> {
121
+ const task = await requireSchedulerTask(name)
122
+ if (task.enabled === enabled) {
123
+ return {
124
+ enabled,
125
+ message: `Scheduled task ${task.name} is already ${enabled ? 'active' : 'paused'}.`,
126
+ task: task.name,
127
+ }
128
+ }
129
+
130
+ const result = await runBuddy([enabled ? 'schedule:enable' : 'schedule:disable', task.name])
131
+ if (result.exitCode !== 0)
132
+ throw new SchedulerOperationError(result.stderr.trim() || `Scheduled task "${task.name}" could not be updated.`)
133
+ return {
134
+ enabled,
135
+ message: `Scheduled task ${task.name} ${enabled ? 'resumed' : 'paused'}.`,
136
+ task: task.name,
137
+ }
138
+ }
@@ -35,6 +35,16 @@ export interface DeploymentTerminal {
35
35
  exists: boolean
36
36
  }
37
37
 
38
+ export interface DeploymentRollbackPlan {
39
+ revision: string
40
+ environment: string
41
+ site: string | null
42
+ release: string | null
43
+ target: string
44
+ output: string
45
+ warnings: string[]
46
+ }
47
+
38
48
  export interface DeployScriptUpdate {
39
49
  success: boolean
40
50
  path: string
@@ -142,3 +152,11 @@ export async function updateDeployScript(content: string): Promise<DeployScriptU
142
152
  export async function fetchDeploymentTerminal(): Promise<DeploymentTerminal> {
143
153
  return dashboardApi<DeploymentTerminal>('/api/dashboard/deployments/terminal')
144
154
  }
155
+
156
+ export function previewDeploymentRollback(input: { environment: string, site?: string, release?: string }): Promise<{ success: boolean, plan: DeploymentRollbackPlan }> {
157
+ return dashboardApi('/api/dashboard/deployments/rollback/preview', { method: 'POST', body: input })
158
+ }
159
+
160
+ export function runDeploymentRollback(input: { environment: string, site?: string, release?: string, revision: string, confirmation: string }): Promise<{ success: boolean, message: string }> {
161
+ return dashboardApi('/api/dashboard/deployments/rollback', { method: 'POST', body: input })
162
+ }
@@ -0,0 +1,361 @@
1
+ import { dashboardApi } from './dashboard-api'
2
+
3
+ export interface SchedulerTask {
4
+ name: string
5
+ pattern: string
6
+ timezone: string
7
+ nextRun: string | null
8
+ enabled: boolean
9
+ }
10
+
11
+ export interface OperatorOperation {
12
+ id: string
13
+ actorName: string
14
+ kind: string
15
+ state: 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'timed_out'
16
+ input: Record<string, unknown>
17
+ output: Record<string, unknown>
18
+ error?: string
19
+ startedAt?: string
20
+ finishedAt?: string
21
+ createdAt: string
22
+ }
23
+
24
+ export interface SchedulerOperationsResponse {
25
+ tasks: SchedulerTask[]
26
+ operations: OperatorOperation[]
27
+ summary: {
28
+ total: number
29
+ active: number
30
+ paused: number
31
+ nextRun: string | null
32
+ }
33
+ }
34
+
35
+ export interface SchedulerMutationResponse {
36
+ success: boolean
37
+ message: string
38
+ task: string
39
+ enabled?: boolean
40
+ operation: OperatorOperation
41
+ }
42
+
43
+ export function fetchSchedulerOperations(): Promise<SchedulerOperationsResponse> {
44
+ return dashboardApi<SchedulerOperationsResponse>('/api/dashboard/operations/scheduler')
45
+ }
46
+
47
+ export function runSchedulerTask(name: string): Promise<SchedulerMutationResponse> {
48
+ return dashboardApi<SchedulerMutationResponse>(`/api/dashboard/operations/scheduler/${encodeURIComponent(name)}/run`, {
49
+ method: 'POST',
50
+ })
51
+ }
52
+
53
+ export function updateSchedulerTask(name: string, enabled: boolean): Promise<SchedulerMutationResponse> {
54
+ return dashboardApi<SchedulerMutationResponse>(`/api/dashboard/operations/scheduler/${encodeURIComponent(name)}`, {
55
+ method: 'PATCH',
56
+ body: { enabled },
57
+ })
58
+ }
59
+
60
+ export type BackupResourceKind = 'managed_database' | 'logical_database' | 'volume' | 'files' | 'control_plane' | 'infrastructure'
61
+
62
+ export interface RecoveryDestination {
63
+ id: string
64
+ name: string
65
+ provider: 'aws_s3' | 's3_compatible' | 'aws_backup'
66
+ bucket?: string
67
+ region?: string
68
+ status: 'untested' | 'healthy' | 'failing' | 'disabled'
69
+ credentialsConfigured: boolean
70
+ clientEncryptionConfigured: boolean
71
+ lastTestedAt?: string
72
+ lastError?: string
73
+ }
74
+
75
+ export interface RecoveryPolicy {
76
+ id: string
77
+ destinationId: string
78
+ name: string
79
+ resourceKind: BackupResourceKind
80
+ schedule: string
81
+ timezone: string
82
+ expectedRpoMinutes: number
83
+ expectedRtoMinutes: number
84
+ enabled: boolean
85
+ nextRunAt?: string
86
+ lastRunAt?: string
87
+ }
88
+
89
+ export interface RecoveryPoint {
90
+ id: string
91
+ policyId?: string
92
+ kind: BackupResourceKind
93
+ pointInTime: string
94
+ sizeBytes: number
95
+ status: 'pending' | 'available' | 'failed' | 'deleting' | 'deleted'
96
+ verificationState: 'unverified' | 'verifying' | 'verified' | 'corrupt' | 'failed'
97
+ verifiedAt?: string
98
+ held: boolean
99
+ pinned: boolean
100
+ expiresAt?: string
101
+ lockedUntil?: string
102
+ }
103
+
104
+ export interface RecoveryJob {
105
+ id: string
106
+ policyId?: string
107
+ recoveryPointId?: string
108
+ operationId?: string
109
+ kind: 'backup' | 'restore' | 'verify' | 'drill' | 'cleanup'
110
+ status: 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'cleanup_required'
111
+ progress: Record<string, unknown>
112
+ error?: string
113
+ startedAt?: string
114
+ finishedAt?: string
115
+ createdAt: string
116
+ }
117
+
118
+ export interface RecoveryCoverage {
119
+ policy: RecoveryPolicy
120
+ lastRecoveryPoint?: RecoveryPoint
121
+ missedRpo: boolean
122
+ unverified: number
123
+ destinationHealthy: boolean
124
+ }
125
+
126
+ export interface RecoveryResource {
127
+ id: string
128
+ kind: string
129
+ name: string
130
+ slug: string
131
+ }
132
+
133
+ export interface RecoveryDataService {
134
+ id: string
135
+ name: string
136
+ engine: string
137
+ provider: string
138
+ status: string
139
+ }
140
+
141
+ export interface RecoveryOperationsResponse {
142
+ environment: { id: string, name: string, slug: string }
143
+ coverage: RecoveryCoverage[]
144
+ destinations: RecoveryDestination[]
145
+ policies: RecoveryPolicy[]
146
+ recoveryPoints: RecoveryPoint[]
147
+ jobs: RecoveryJob[]
148
+ resources: RecoveryResource[]
149
+ dataServices: RecoveryDataService[]
150
+ summary: {
151
+ policies: number
152
+ protected: number
153
+ missedRpo: number
154
+ unverified: number
155
+ queuedJobs: number
156
+ latestPoint: string | null
157
+ }
158
+ }
159
+
160
+ export interface RecoveryDestinationInput {
161
+ name: string
162
+ provider: RecoveryDestination['provider']
163
+ bucket?: string
164
+ endpoint?: string
165
+ prefix?: string
166
+ region?: string
167
+ allowPrivate?: boolean
168
+ forcePathStyle?: boolean
169
+ encryption: 'provider' | 'client_side' | 'both'
170
+ encryptionKey?: string
171
+ lockDays?: number
172
+ credentials?: { accessKeyId?: string, secretAccessKey?: string, sessionToken?: string }
173
+ }
174
+
175
+ export interface RecoveryPolicyInput {
176
+ name: string
177
+ destinationId: string
178
+ resourceKind: BackupResourceKind
179
+ resourceId?: string
180
+ dataServiceId?: string
181
+ includePatterns: string[]
182
+ excludePatterns: string[]
183
+ schedule: string
184
+ timezone: string
185
+ keepLast: number
186
+ expireAfterDays: number
187
+ compression: 'none' | 'gzip' | 'zstd'
188
+ expectedRpoMinutes: number
189
+ expectedRtoMinutes: number
190
+ enabled: boolean
191
+ }
192
+
193
+ export interface RestorePlanInput {
194
+ mode: 'isolated' | 'in_place'
195
+ targetName: string
196
+ target?: Record<string, unknown>
197
+ drill?: boolean
198
+ execute?: boolean
199
+ confirm?: string
200
+ downtimeAcknowledged?: boolean
201
+ safetyBackupId?: string
202
+ }
203
+
204
+ export function fetchRecoveryOperations(): Promise<RecoveryOperationsResponse> {
205
+ return dashboardApi<RecoveryOperationsResponse>('/api/dashboard/operations/recovery')
206
+ }
207
+
208
+ export function createRecoveryDestination(input: RecoveryDestinationInput): Promise<{ success: boolean, destination: RecoveryDestination }> {
209
+ return dashboardApi('/api/dashboard/operations/recovery/destinations', { method: 'POST', body: input })
210
+ }
211
+
212
+ export function testRecoveryDestination(id: string): Promise<{ success: boolean, message: string, destination: RecoveryDestination }> {
213
+ return dashboardApi(`/api/dashboard/operations/recovery/destinations/${encodeURIComponent(id)}/test`, { method: 'POST' })
214
+ }
215
+
216
+ export function createRecoveryPolicy(input: RecoveryPolicyInput): Promise<{ success: boolean, policy: RecoveryPolicy }> {
217
+ return dashboardApi('/api/dashboard/operations/recovery/policies', { method: 'POST', body: input })
218
+ }
219
+
220
+ export function runRecoveryPolicy(id: string): Promise<{ success: boolean, message: string, job: RecoveryJob }> {
221
+ return dashboardApi(`/api/dashboard/operations/recovery/policies/${encodeURIComponent(id)}/run`, { method: 'POST' })
222
+ }
223
+
224
+ export function verifyRecoveryPoint(id: string): Promise<{ success: boolean, message: string, job: RecoveryJob }> {
225
+ return dashboardApi(`/api/dashboard/operations/recovery/points/${encodeURIComponent(id)}/verify`, { method: 'POST' })
226
+ }
227
+
228
+ export function planRecoveryRestore(id: string, input: RestorePlanInput): Promise<{ success: boolean, plan: { mode: string, target: Record<string, unknown>, warnings: string[] }, productionExecutionCreated: boolean }> {
229
+ return dashboardApi(`/api/dashboard/operations/recovery/points/${encodeURIComponent(id)}/restore`, { method: 'POST', body: input })
230
+ }
231
+
232
+ export function protectRecoveryPoint(id: string, input: { pinned?: boolean, held?: boolean }): Promise<{ success: boolean, recoveryPoint: RecoveryPoint }> {
233
+ return dashboardApi(`/api/dashboard/operations/recovery/points/${encodeURIComponent(id)}/protection`, { method: 'PATCH', body: input })
234
+ }
235
+
236
+ export function runRecoveryRetention(): Promise<{ success: boolean, message: string, jobs: RecoveryJob[] }> {
237
+ return dashboardApi('/api/dashboard/operations/recovery/retention', { method: 'POST' })
238
+ }
239
+
240
+ export interface MigrationPlanOperation {
241
+ kind: string
242
+ table: string
243
+ column?: string
244
+ sql?: string
245
+ destructive: boolean
246
+ }
247
+
248
+ export interface MigrationLedgerEntry {
249
+ file: string
250
+ status: string
251
+ recorded: boolean
252
+ effects: Array<{ kind: string, table?: string, name: string }>
253
+ }
254
+
255
+ export interface MigrationOperationsResponse {
256
+ environment: string
257
+ dialect: string
258
+ operations: MigrationPlanOperation[]
259
+ revision: string
260
+ applied: number
261
+ ledger: {
262
+ supported: boolean
263
+ drift: boolean
264
+ entries: MigrationLedgerEntry[]
265
+ orphans: Array<{ migration: string, renamedTo?: string }>
266
+ counts: Record<string, number>
267
+ }
268
+ reconciliation: Record<string, unknown>
269
+ operatorOperations: OperatorOperation[]
270
+ summary: { pending: number, destructive: number, drift: boolean, ledgerIssues: number }
271
+ }
272
+
273
+ export function fetchMigrationOperations(): Promise<MigrationOperationsResponse> {
274
+ return dashboardApi('/api/dashboard/operations/migrations')
275
+ }
276
+
277
+ export function applyMigrationOperations(revision: string, confirmation: string): Promise<{ success: boolean, message: string, operation: OperatorOperation }> {
278
+ return dashboardApi('/api/dashboard/operations/migrations/apply', { method: 'POST', body: { revision, confirmation } })
279
+ }
280
+
281
+ export function reconcileMigrationLedger(revision: string, confirmation: string): Promise<{ success: boolean, result: Record<string, unknown>, operation: OperatorOperation }> {
282
+ return dashboardApi('/api/dashboard/operations/migrations/reconcile', { method: 'POST', body: { revision, confirmation } })
283
+ }
284
+
285
+ export interface ReleaseApprovalItem {
286
+ id: string
287
+ sourceSha?: string
288
+ kind: string
289
+ strategy: string
290
+ createdAt: string
291
+ approvals: Array<{ decision: string, comment?: string, createdAt: string }>
292
+ }
293
+
294
+ export interface ChangeOperationsResponse {
295
+ environment?: { id: string, name: string, slug: string }
296
+ migrations: MigrationOperationsResponse
297
+ pendingReleases: ReleaseApprovalItem[]
298
+ activeOperations: OperatorOperation[]
299
+ recentOperations: OperatorOperation[]
300
+ summary: { migrationChanges: number, destructiveChanges: number, approvals: number, activeOperations: number }
301
+ }
302
+
303
+ export function fetchChangeOperations(): Promise<ChangeOperationsResponse> {
304
+ return dashboardApi('/api/dashboard/operations/changes')
305
+ }
306
+
307
+ export function decideRelease(id: string, decision: 'approved' | 'rejected', comment = ''): Promise<{ success: boolean }> {
308
+ return dashboardApi(`/api/dashboard/operations/changes/releases/${encodeURIComponent(id)}/decision`, { method: 'POST', body: { decision, comment } })
309
+ }
310
+
311
+ export interface IncidentAlert {
312
+ id: string
313
+ state: string
314
+ severity: string
315
+ title: string
316
+ occurrenceCount: number
317
+ ownerName?: string
318
+ acknowledgedAt?: string
319
+ silencedUntil?: string
320
+ firstSeenAt: string
321
+ lastSeenAt: string
322
+ }
323
+
324
+ export interface IncidentOperationsResponse {
325
+ alerts: IncidentAlert[]
326
+ rules: Array<{ id: string, name: string, severity: string, enabled: boolean }>
327
+ healthChecks: Array<{ id: string, name: string, kind: string, target: string, enabled: boolean }>
328
+ channels: Array<{ id: string, name: string, kind: string, status: string }>
329
+ deliveries: Array<Record<string, unknown>>
330
+ operations: OperatorOperation[]
331
+ summary: { firing: number, critical: number, acknowledged: number, rules: number }
332
+ }
333
+
334
+ export function fetchIncidentOperations(): Promise<IncidentOperationsResponse> {
335
+ return dashboardApi('/api/dashboard/operations/incidents')
336
+ }
337
+
338
+ export function updateIncident(id: string, input: { action: 'acknowledge' | 'assign_self' | 'unassign' | 'silence', until?: string }): Promise<{ success: boolean, alert: IncidentAlert }> {
339
+ return dashboardApi(`/api/dashboard/operations/incidents/${encodeURIComponent(id)}`, { method: 'PATCH', body: input })
340
+ }
341
+
342
+ export interface AuditEvent {
343
+ id: string
344
+ sequence: number
345
+ operationId?: string
346
+ actorName: string
347
+ type: string
348
+ level: string
349
+ payload: unknown
350
+ createdAt: string
351
+ }
352
+
353
+ export interface AuditOperationsResponse {
354
+ events: AuditEvent[]
355
+ operations: OperatorOperation[]
356
+ summary: { events: number, operators: number, failures: number, latestSequence: number }
357
+ }
358
+
359
+ export function fetchAuditOperations(): Promise<AuditOperationsResponse> {
360
+ return dashboardApi('/api/dashboard/operations/audit')
361
+ }
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.70.365",
5
+ "version": "0.70.367",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",