@stacksjs/defaults 0.70.356 → 0.70.358

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +5 -1
  2. package/ai/skills/stacks-browse/scripts/browse.ts +61 -6
  3. package/ai/skills/stacks-orm/SKILL.md +6 -0
  4. package/app/Actions/Dashboard/Actions/GetActions.ts +15 -9
  5. package/app/Actions/Dashboard/Analytics/EventStoreAction.ts +21 -9
  6. package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +12 -3
  7. package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +12 -3
  8. package/app/Actions/Dashboard/Content/AuthorDestroyAction.ts +23 -9
  9. package/app/Actions/Dashboard/Content/AuthorIndexAction.ts +32 -26
  10. package/app/Actions/Dashboard/Content/AuthorStoreAction.ts +44 -24
  11. package/app/Actions/Dashboard/Content/AuthorUpdateAction.ts +48 -26
  12. package/app/Actions/Dashboard/Content/CategoryDestroyAction.ts +27 -8
  13. package/app/Actions/Dashboard/Content/CategoryIndexAction.ts +21 -15
  14. package/app/Actions/Dashboard/Content/CategoryStoreAction.ts +47 -29
  15. package/app/Actions/Dashboard/Content/CommentDestroyAction.ts +17 -4
  16. package/app/Actions/Dashboard/Content/CommentIndexAction.ts +23 -17
  17. package/app/Actions/Dashboard/Content/CommentUpdateAction.ts +30 -12
  18. package/app/Actions/Dashboard/Content/ContentDashboardAction.ts +43 -37
  19. package/app/Actions/Dashboard/Content/PageDestroyAction.ts +17 -4
  20. package/app/Actions/Dashboard/Content/PageIndexAction.ts +23 -17
  21. package/app/Actions/Dashboard/Content/PageStoreAction.ts +33 -18
  22. package/app/Actions/Dashboard/Content/PageUpdateAction.ts +40 -22
  23. package/app/Actions/Dashboard/Content/PostDestroyAction.ts +24 -11
  24. package/app/Actions/Dashboard/Content/PostIndexAction.ts +55 -49
  25. package/app/Actions/Dashboard/Content/PostStoreAction.ts +43 -33
  26. package/app/Actions/Dashboard/Content/PostUpdateAction.ts +46 -34
  27. package/app/Actions/Dashboard/Content/TagDestroyAction.ts +27 -8
  28. package/app/Actions/Dashboard/Content/TagIndexAction.ts +31 -25
  29. package/app/Actions/Dashboard/Content/TagStoreAction.ts +44 -23
  30. package/app/Actions/Dashboard/Content/content-input.ts +4 -4
  31. package/app/Actions/Dashboard/Content/post-input.ts +60 -8
  32. package/app/Actions/Dashboard/DashboardHealthAction.ts +34 -14
  33. package/app/Actions/Dashboard/DashboardHomeAction.test.ts +1 -1
  34. package/app/Actions/Dashboard/DashboardHomeAction.ts +31 -4
  35. package/app/Actions/Dashboard/DashboardStatsAction.ts +6 -1
  36. package/app/Actions/Dashboard/Email/InboxSendAction.ts +14 -7
  37. package/app/Actions/Dashboard/Infrastructure/CommandIndexAction.ts +33 -27
  38. package/app/Actions/Dashboard/Infrastructure/EnvironmentIndexAction.ts +10 -4
  39. package/app/Actions/Dashboard/Infrastructure/EnvironmentUpdateAction.ts +8 -1
  40. package/app/Actions/Dashboard/Infrastructure/RequestIndexAction.ts +24 -18
  41. package/app/Actions/Dashboard/Infrastructure/ServerIndexAction.ts +15 -9
  42. package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +9 -2
  43. package/app/Actions/Dashboard/Marketing/CampaignCancelAction.ts +23 -5
  44. package/app/Actions/Dashboard/Marketing/CampaignDestroyAction.ts +26 -17
  45. package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +49 -43
  46. package/app/Actions/Dashboard/Marketing/CampaignScheduleAction.ts +31 -25
  47. package/app/Actions/Dashboard/Marketing/CampaignSendAction.ts +28 -22
  48. package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +15 -10
  49. package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +19 -12
  50. package/app/Actions/Dashboard/Marketing/ListDestroyAction.ts +34 -25
  51. package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +32 -26
  52. package/app/Actions/Dashboard/Marketing/ListStoreAction.ts +31 -18
  53. package/app/Actions/Dashboard/Marketing/ListUpdateAction.ts +35 -19
  54. package/app/Actions/Dashboard/Marketing/SocialPostDestroyAction.ts +15 -5
  55. package/app/Actions/Dashboard/Marketing/SocialPostIndexAction.ts +11 -5
  56. package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +19 -13
  57. package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +22 -12
  58. package/app/Actions/Dashboard/Marketing/campaign-delivery.ts +34 -0
  59. package/app/Actions/Dashboard/Marketing/marketing-list-records.test.ts +10 -0
  60. package/app/Actions/Dashboard/Marketing/marketing-list-records.ts +10 -0
  61. package/app/Actions/Dashboard/Marketing/marketing-response.ts +28 -0
  62. package/app/Actions/Dashboard/Marketing/social-post-records.test.ts +13 -0
  63. package/app/Actions/Dashboard/Marketing/social-post-records.ts +22 -4
  64. package/app/Actions/Dashboard/Models/GetModels.ts +14 -7
  65. package/app/Actions/Dashboard/Models/GetSubscriberCount.ts +8 -1
  66. package/app/Actions/Dashboard/Models/GetUserCount.ts +8 -1
  67. package/app/Actions/Dashboard/Releases/ReleaseIndexAction.ts +11 -5
  68. package/app/Actions/Dashboard/Settings/MailSettingsGetAction.ts +10 -4
  69. package/app/Actions/Dashboard/Settings/MailSettingsUpdateAction.ts +8 -1
  70. package/ide/vscode/package.json +1 -1
  71. package/package.json +1 -1
  72. package/resources/components/Dashboard/UI/Modal.stx +60 -10
  73. package/views/dashboard/stores/auth.ts +18 -2
@@ -1,8 +1,10 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
+ import { isUniqueViolation, transaction } from '@stacksjs/orm'
4
5
  import { response } from '@stacksjs/router'
5
6
  import { randomUUIDv7 } from 'bun'
7
+ import { dashboardOperationalError } from '../dashboard-response'
6
8
  import { findRow, insertedId, slugify, str, timestamp } from './content-input'
7
9
 
8
10
  /**
@@ -26,34 +28,53 @@ export default new Action({
26
28
  if (!slug)
27
29
  return response.json({ message: 'Slug could not be derived from the name; enter one.' }, 422)
28
30
 
29
- const duplicate = await db
30
- .selectFrom('tags')
31
- .select(['id'])
32
- .where('slug', '=', slug)
33
- .executeTakeFirst()
31
+ try {
32
+ const tag = await transaction(async (rawTrx) => {
33
+ const trx = rawTrx as unknown as typeof db
34
+ const duplicate = await trx
35
+ .selectFrom('tags')
36
+ .select(['id'])
37
+ .where('slug', '=', slug)
38
+ .orWhere('name', '=', name)
39
+ .executeTakeFirst()
34
40
 
35
- if (duplicate)
36
- return response.json({ message: 'A tag with that slug already exists.' }, 422)
41
+ if (duplicate)
42
+ return null
37
43
 
38
- const now = timestamp()
44
+ const now = timestamp()
39
45
 
40
- const result = await db
41
- .insertInto('tags')
42
- .values({
43
- uuid: randomUUIDv7(),
44
- name,
45
- slug,
46
- description,
47
- created_at: now,
48
- updated_at: now,
49
- } as any)
50
- .executeTakeFirst()
46
+ const result = await trx
47
+ .insertInto('tags')
48
+ .values({
49
+ uuid: randomUUIDv7(),
50
+ name,
51
+ slug,
52
+ description,
53
+ created_at: now,
54
+ updated_at: now,
55
+ } as any)
56
+ .executeTakeFirst()
51
57
 
52
- const id = insertedId(result)
58
+ const id = insertedId(result)
53
59
 
54
- if (!id)
55
- return response.json({ message: 'Could not create tag.' }, 500)
60
+ if (!id)
61
+ throw new Error('Tag insert did not return an id.')
56
62
 
57
- return response.json(await findRow('tags', id), 201)
63
+ const created = await findRow('tags', id, trx)
64
+ if (!created)
65
+ throw new Error('Created tag could not be loaded.')
66
+ return created
67
+ })
68
+
69
+ if (!tag)
70
+ return response.json({ message: 'A tag with that name or slug already exists.' }, 422)
71
+
72
+ return response.json(tag, 201)
73
+ }
74
+ catch (error) {
75
+ if (isUniqueViolation(error))
76
+ return response.json({ message: 'A tag with that name or slug already exists.' }, 422)
77
+ return dashboardOperationalError(error, 'Tag could not be created.', 'TagStoreAction', 500)
78
+ }
58
79
  },
59
80
  })
@@ -53,13 +53,13 @@ export function slugify(value: string): string {
53
53
  * `table` is a literal at every call site; the cast is only here because the
54
54
  * query builder's types want a table union rather than a `string`.
55
55
  */
56
- export async function findRow(table: string, id: number): Promise<unknown> {
57
- return await (db as any).selectFrom(table).selectAll().where('id', '=', id).executeTakeFirst()
56
+ export async function findRow(table: string, id: number, database: typeof db = db): Promise<unknown> {
57
+ return await (database as any).selectFrom(table).selectAll().where('id', '=', id).executeTakeFirst()
58
58
  }
59
59
 
60
60
  /** Whether a row with this id exists — the 404 check every write shares. */
61
- export async function rowExists(table: string, id: number): Promise<boolean> {
62
- const row = await (db as any).selectFrom(table).select(['id']).where('id', '=', id).executeTakeFirst()
61
+ export async function rowExists(table: string, id: number, database: typeof db = db): Promise<boolean> {
62
+ const row = await (database as any).selectFrom(table).select(['id']).where('id', '=', id).executeTakeFirst()
63
63
 
64
64
  return Boolean(row)
65
65
  }
@@ -1,8 +1,8 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { db } from '@stacksjs/database'
3
- import { str } from './content-input'
3
+ import { insertedId, str, timestamp } from './content-input'
4
4
 
5
- export { insertedId, timestamp } from './content-input'
5
+ export { insertedId, timestamp }
6
6
 
7
7
  export interface PostPayload {
8
8
  title: string
@@ -42,8 +42,8 @@ export function publishedAtFor(status: string, existing: string | null, now: str
42
42
  }
43
43
 
44
44
  /** Reads a post row back after a write — see `findRow` for why writes re-select. */
45
- export async function findPost(id: number): Promise<unknown> {
46
- return await db.selectFrom('posts').selectAll().where('id', '=', id).executeTakeFirst()
45
+ export async function findPost(id: number, database: typeof db = db): Promise<unknown> {
46
+ return await database.selectFrom('posts').selectAll().where('id', '=', id).executeTakeFirst()
47
47
  }
48
48
 
49
49
  function ids(value: unknown): number[] {
@@ -83,16 +83,16 @@ export function invalidPostContent(payload: PostPayload): string | null {
83
83
  return null
84
84
  }
85
85
 
86
- export async function invalidPostReference(payload: PostPayload): Promise<string | null> {
86
+ export async function invalidPostReference(payload: PostPayload, database: typeof db = db): Promise<string | null> {
87
87
  const [author, categoryRows, tagRows] = await Promise.all([
88
88
  payload.authorId
89
- ? db.selectFrom('authors').select(['id']).where('id', '=', payload.authorId).executeTakeFirst()
89
+ ? database.selectFrom('authors').select(['id']).where('id', '=', payload.authorId).executeTakeFirst()
90
90
  : Promise.resolve(undefined),
91
91
  payload.categoryIds.length
92
- ? db.selectFrom('categories').whereIn('id', payload.categoryIds).select(['id']).execute()
92
+ ? database.selectFrom('categories').whereIn('id', payload.categoryIds).select(['id']).execute()
93
93
  : Promise.resolve([]),
94
94
  payload.tagIds.length
95
- ? db.selectFrom('tags').whereIn('id', payload.tagIds).select(['id']).execute()
95
+ ? database.selectFrom('tags').whereIn('id', payload.tagIds).select(['id']).execute()
96
96
  : Promise.resolve([]),
97
97
  ])
98
98
 
@@ -106,6 +106,58 @@ export async function invalidPostReference(payload: PostPayload): Promise<string
106
106
  return null
107
107
  }
108
108
 
109
+ async function replacePostPivot(
110
+ database: typeof db,
111
+ table: 'categorizable_models' | 'taggable_models',
112
+ foreignKey: 'category_id' | 'tag_id',
113
+ ownerKey: 'categorizable_id' | 'taggable_id',
114
+ typeKey: 'categorizable_type' | 'taggable_type',
115
+ postId: number,
116
+ relatedIds: number[],
117
+ now: string,
118
+ ): Promise<void> {
119
+ await (database as any)
120
+ .deleteFrom(table)
121
+ .where(ownerKey, '=', postId)
122
+ .where(typeKey, '=', 'posts')
123
+ .execute()
124
+
125
+ if (relatedIds.length === 0)
126
+ return
127
+
128
+ await (database as any)
129
+ .insertInto(table)
130
+ .values(relatedIds.map(relatedId => ({
131
+ [foreignKey]: relatedId,
132
+ [ownerKey]: postId,
133
+ [typeKey]: 'posts',
134
+ created_at: now,
135
+ updated_at: now,
136
+ })))
137
+ .execute()
138
+ }
139
+
140
+ /** Replaces both post pivot sets on the caller's database or transaction. */
141
+ export async function syncPostRelations(database: typeof db, postId: number, payload: Pick<PostPayload, 'categoryIds' | 'tagIds'>): Promise<void> {
142
+ const now = timestamp()
143
+ await replacePostPivot(database, 'categorizable_models', 'category_id', 'categorizable_id', 'categorizable_type', postId, payload.categoryIds, now)
144
+ await replacePostPivot(database, 'taggable_models', 'tag_id', 'taggable_id', 'taggable_type', postId, payload.tagIds, now)
145
+ }
146
+
147
+ /** Removes both post pivot sets on the caller's database or transaction. */
148
+ export async function detachPostRelations(database: typeof db, postId: number): Promise<void> {
149
+ await (database as any)
150
+ .deleteFrom('categorizable_models')
151
+ .where('categorizable_id', '=', postId)
152
+ .where('categorizable_type', '=', 'posts')
153
+ .execute()
154
+ await (database as any)
155
+ .deleteFrom('taggable_models')
156
+ .where('taggable_id', '=', postId)
157
+ .where('taggable_type', '=', 'posts')
158
+ .execute()
159
+ }
160
+
109
161
  /** Maps a dashboard request body onto the writable post columns. */
110
162
  export function postPayload(request: RequestInstance): PostPayload {
111
163
  return {
@@ -1,6 +1,7 @@
1
1
  import process from 'node:process'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { checkApplicationHealth } from '@stacksjs/router'
4
+ import { dashboardOperationalError, dashboardOperationalIssue } from './dashboard-response'
4
5
 
5
6
  export default new Action({
6
7
  name: 'DashboardHealthAction',
@@ -10,21 +11,40 @@ export default new Action({
10
11
 
11
12
  async handle() {
12
13
  const startedAt = performance.now()
13
- const health = await checkApplicationHealth()
14
- const memory = process.memoryUsage()
14
+ try {
15
+ const health = await checkApplicationHealth()
16
+ const memory = process.memoryUsage()
17
+ const checks = Object.fromEntries(Object.entries(health.checks).map(([name, check]) => {
18
+ if (!check.ok) {
19
+ dashboardOperationalIssue(
20
+ check.message,
21
+ 'Dependency probe failed.',
22
+ `DashboardHealthAction.${name}`,
23
+ )
24
+ }
25
+ return [name, {
26
+ ...check,
27
+ message: check.ok ? undefined : 'Dependency probe failed.',
28
+ }]
29
+ }))
15
30
 
16
- return {
17
- ...health,
18
- durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
19
- runtime: {
20
- bunVersion: process.versions.bun || '',
21
- nodeVersion: process.versions.node || '',
22
- platform: process.platform,
23
- architecture: process.arch,
24
- uptimeSeconds: Math.max(0, Math.floor(process.uptime())),
25
- residentMemoryBytes: memory.rss,
26
- heapUsedBytes: memory.heapUsed,
27
- },
31
+ return {
32
+ ...health,
33
+ checks,
34
+ durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
35
+ runtime: {
36
+ bunVersion: process.versions.bun || '',
37
+ nodeVersion: process.versions.node || '',
38
+ platform: process.platform,
39
+ architecture: process.arch,
40
+ uptimeSeconds: Math.max(0, Math.floor(process.uptime())),
41
+ residentMemoryBytes: memory.rss,
42
+ heapUsedBytes: memory.heapUsed,
43
+ },
44
+ }
45
+ }
46
+ catch (error) {
47
+ return dashboardOperationalError(error, 'System health could not be loaded.', 'DashboardHealthAction')
28
48
  }
29
49
  },
30
50
  })
@@ -28,7 +28,7 @@ describe('dashboard HTTP metrics', () => {
28
28
  name: 'Cache',
29
29
  status: 'critical',
30
30
  latency: '1500ms',
31
- detail: 'timeout',
31
+ detail: 'Dependency probe failed.',
32
32
  })
33
33
  })
34
34
 
@@ -2,6 +2,7 @@ import { Action } from '@stacksjs/actions'
2
2
  import { Order, Post, Product, Request, User } from '@stacksjs/orm'
3
3
  import { checkApplicationHealth, type ApplicationHealthCheck } from '@stacksjs/router'
4
4
  import { formatRelative, safeGet } from '../../../resources/functions/dashboard/data'
5
+ import { dashboardOperationalIssue } from './dashboard-response'
5
6
 
6
7
  interface HttpRequestSample {
7
8
  duration: number
@@ -28,7 +29,7 @@ export function serializeHealthCheck(name: string, check: ApplicationHealthCheck
28
29
  name: name.charAt(0).toUpperCase() + name.slice(1),
29
30
  status: check.ok ? 'healthy' : 'critical',
30
31
  latency: `${check.ms}ms`,
31
- detail: check.message || '',
32
+ detail: check.ok ? '' : 'Dependency probe failed.',
32
33
  }
33
34
  }
34
35
 
@@ -43,7 +44,11 @@ function issue(source: string, result: PromiseSettledResult<unknown>) {
43
44
  return result.status === 'rejected'
44
45
  ? {
45
46
  source,
46
- message: result.reason instanceof Error ? result.reason.message : 'Query failed.',
47
+ message: dashboardOperationalIssue(
48
+ result.reason,
49
+ `${source} data could not be loaded.`,
50
+ `DashboardHomeAction.${source.toLowerCase().replaceAll(' ', '-')}`,
51
+ ),
47
52
  }
48
53
  : null
49
54
  }
@@ -65,7 +70,7 @@ export default new Action({
65
70
  Request.count(),
66
71
  Request.orderBy('created_at', 'desc').limit(1000).get(),
67
72
  ])
68
- const health = await checkApplicationHealth()
73
+ const healthResult = await Promise.allSettled([checkApplicationHealth()])
69
74
 
70
75
  const [
71
76
  userCount,
@@ -93,7 +98,19 @@ export default new Action({
93
98
  })))
94
99
  : summarizeHttpRequests(0, [])
95
100
 
96
- const services = Object.entries(health.checks).map(([name, check]) => serializeHealthCheck(name, check))
101
+ const health = healthResult[0]
102
+ const services = health.status === 'fulfilled'
103
+ ? Object.entries(health.value.checks).map(([name, check]) => {
104
+ if (!check.ok) {
105
+ dashboardOperationalIssue(
106
+ check.message,
107
+ 'Dependency probe failed.',
108
+ `DashboardHomeAction.health.${name}`,
109
+ )
110
+ }
111
+ return serializeHealthCheck(name, check)
112
+ })
113
+ : []
97
114
 
98
115
  const activities = [
99
116
  ...(recentOrders.status === 'fulfilled' ? recentOrders.value : []).map((order: any) => ({
@@ -127,6 +144,16 @@ export default new Action({
127
144
  const issues = modelResults
128
145
  .map((result, index) => issue(sources[index], result))
129
146
  .filter((entry): entry is { source: string, message: string } => entry !== null)
147
+ if (health.status === 'rejected') {
148
+ issues.push({
149
+ source: 'System health',
150
+ message: dashboardOperationalIssue(
151
+ health.reason,
152
+ 'System health could not be loaded.',
153
+ 'DashboardHomeAction.health',
154
+ ),
155
+ })
156
+ }
130
157
 
131
158
  return { stats, httpMetrics, services, activities, issues }
132
159
  },
@@ -1,5 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { Customer, Order, Post, User } from '@stacksjs/orm'
3
+ import { dashboardOperationalIssue } from './dashboard-response'
3
4
 
4
5
  interface DashboardStatDefinition {
5
6
  title: string
@@ -61,7 +62,11 @@ export default new Action({
61
62
  const issues = results.flatMap((result, index) => result.status === 'rejected'
62
63
  ? [{
63
64
  source: definitions[index].title,
64
- message: result.reason instanceof Error ? result.reason.message : 'Model query failed.',
65
+ message: dashboardOperationalIssue(
66
+ result.reason,
67
+ `${definitions[index].title} data could not be loaded.`,
68
+ `DashboardStatsAction.${definitions[index].title.toLowerCase().replaceAll(' ', '-')}`,
69
+ ),
65
70
  }]
66
71
  : [])
67
72
 
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
3
3
  import { notify } from '@stacksjs/notifications'
4
4
  import { response } from '@stacksjs/router'
5
5
  import { schema } from '@stacksjs/validation'
6
+ import { dashboardOperationalError } from '../dashboard-response'
6
7
 
7
8
  export default new Action({
8
9
  name: 'InboxSendAction',
@@ -29,15 +30,21 @@ export default new Action({
29
30
  const subject = String(request.get('subject') || '').trim()
30
31
  const body = String(request.get('body') || '').trim()
31
32
 
32
- const [result] = await notify(
33
- { email: to },
34
- { subject, body, data: { source: 'dashboard-inbox' } },
35
- ['email'],
36
- { ignorePreferences: true },
37
- )
33
+ let result
34
+ try {
35
+ [result] = await notify(
36
+ { email: to },
37
+ { subject, body, data: { source: 'dashboard-inbox' } },
38
+ ['email'],
39
+ { ignorePreferences: true },
40
+ )
41
+ }
42
+ catch (error) {
43
+ return dashboardOperationalError(error, 'The email could not be sent.', 'InboxSendAction.provider', 502)
44
+ }
38
45
 
39
46
  if (!result?.success)
40
- return response.json({ message: result?.error?.message || 'The email could not be sent.' }, 502)
47
+ return dashboardOperationalError(result?.error, 'The email could not be sent.', 'InboxSendAction.result', 502)
41
48
 
42
49
  return response.json({ success: true })
43
50
  },
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, statSync } from 'node:fs'
3
3
  import { join, relative } from 'node:path'
4
4
  import process from 'node:process'
5
5
  import { parseCommandSource } from '../Source/source-inventory'
6
+ import { dashboardOperationalError } from '../dashboard-response'
6
7
 
7
8
  interface CommandConfig {
8
9
  file: string
@@ -15,35 +16,40 @@ export default new Action({
15
16
  description: 'Lists commands registered by the application.',
16
17
  method: 'GET',
17
18
  async handle() {
18
- const projectRoot = process.cwd()
19
- const registryPath = join(projectRoot, 'app/Commands.ts')
20
- const registryModule = await import(registryPath)
21
- const registry = (registryModule.default || {}) as Record<string, string | CommandConfig>
22
- const items = Object.entries(registry).flatMap(([signature, value]) => {
23
- const config = typeof value === 'string'
24
- ? { file: value, enabled: true, aliases: [] }
25
- : { enabled: true, aliases: [], ...value }
26
- const file = join(projectRoot, 'app/Commands', `${config.file}.ts`)
27
- if (!existsSync(file))
28
- return []
19
+ try {
20
+ const projectRoot = process.cwd()
21
+ const registryPath = join(projectRoot, 'app/Commands.ts')
22
+ const registryModule = await import(registryPath)
23
+ const registry = (registryModule.default || {}) as Record<string, string | CommandConfig>
24
+ const items = Object.entries(registry).flatMap(([signature, value]) => {
25
+ const config = typeof value === 'string'
26
+ ? { file: value, enabled: true, aliases: [] }
27
+ : { enabled: true, aliases: [], ...value }
28
+ const file = join(projectRoot, 'app/Commands', `${config.file}.ts`)
29
+ if (!existsSync(file))
30
+ return []
29
31
 
30
- return [parseCommandSource(
31
- readFileSync(file, 'utf8'),
32
- relative(projectRoot, file),
33
- signature,
34
- config.aliases,
35
- statSync(file).mtime.toISOString(),
36
- )]
37
- })
32
+ return [parseCommandSource(
33
+ readFileSync(file, 'utf8'),
34
+ relative(projectRoot, file),
35
+ signature,
36
+ config.aliases,
37
+ statSync(file).mtime.toISOString(),
38
+ )]
39
+ })
38
40
 
39
- return {
40
- items,
41
- stats: {
42
- total: items.length,
43
- aliases: items.reduce((sum, item) => sum + (item.aliases?.length || 0), 0),
44
- options: items.reduce((sum, item) => sum + (item.options?.length || 0), 0),
45
- registered: Object.keys(registry).length,
46
- },
41
+ return {
42
+ items,
43
+ stats: {
44
+ total: items.length,
45
+ aliases: items.reduce((sum, item) => sum + (item.aliases?.length || 0), 0),
46
+ options: items.reduce((sum, item) => sum + (item.options?.length || 0), 0),
47
+ registered: Object.keys(registry).length,
48
+ },
49
+ }
50
+ }
51
+ catch (error) {
52
+ return dashboardOperationalError(error, 'Command sources could not be loaded.', 'CommandIndexAction')
47
53
  }
48
54
  },
49
55
  })
@@ -1,4 +1,5 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { dashboardOperationalError } from '../dashboard-response'
2
3
  import { readEnvironmentFile } from './environment-file'
3
4
 
4
5
  export default new Action({
@@ -8,9 +9,14 @@ export default new Action({
8
9
  apiResponse: true,
9
10
 
10
11
  async handle() {
11
- return Response.json(
12
- { environment: await readEnvironmentFile() },
13
- { headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' } },
14
- )
12
+ try {
13
+ return Response.json(
14
+ { environment: await readEnvironmentFile() },
15
+ { headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' } },
16
+ )
17
+ }
18
+ catch (error) {
19
+ return dashboardOperationalError(error, 'Environment file could not be loaded.', 'EnvironmentIndexAction')
20
+ }
15
21
  },
16
22
  })
@@ -1,6 +1,7 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { response } from '@stacksjs/router'
4
+ import { dashboardOperationalError } from '../dashboard-response'
4
5
  import { updateEnvironmentFile } from './environment-file'
5
6
 
6
7
  export default new Action({
@@ -18,7 +19,13 @@ export default new Action({
18
19
  if (typeof revision !== 'string' || !/^[a-f0-9]{64}$/.test(revision))
19
20
  return response.json({ message: 'A valid environment revision is required.' }, 422)
20
21
 
21
- const result = await updateEnvironmentFile(content, revision)
22
+ let result
23
+ try {
24
+ result = await updateEnvironmentFile(content, revision)
25
+ }
26
+ catch (error) {
27
+ return dashboardOperationalError(error, 'Environment file could not be saved.', 'EnvironmentUpdateAction', 500)
28
+ }
22
29
  if (result.issues?.length) {
23
30
  const errors = Object.fromEntries(result.issues.map((issue, index) => [
24
31
  issue.line ? String(issue.line) : `file-${index}`,
@@ -1,19 +1,21 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { Request } from '@stacksjs/orm'
3
+ import { dashboardOperationalError } from '../dashboard-response'
3
4
 
4
5
  export default new Action({
5
6
  name: 'RequestIndexAction',
6
7
  description: 'Returns request history data for the dashboard.',
7
8
  method: 'GET',
8
9
  async handle() {
9
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString()
10
- const [allRequests, total, errorCount, averageDurationMs, requestsLastHour] = await Promise.all([
11
- Request.orderByDesc('id').limit(100).get(),
12
- Request.count(),
13
- Request.where('status_code', '>=', 400).count(),
14
- Request.avg('duration_ms'),
15
- Request.where('created_at', '>=', oneHourAgo).count(),
16
- ])
10
+ try {
11
+ const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString()
12
+ const [allRequests, total, errorCount, averageDurationMs, requestsLastHour] = await Promise.all([
13
+ Request.orderByDesc('id').limit(100).get(),
14
+ Request.count(),
15
+ Request.where('status_code', '>=', 400).count(),
16
+ Request.avg('duration_ms'),
17
+ Request.where('created_at', '>=', oneHourAgo).count(),
18
+ ])
17
19
 
18
20
  const requests = allRequests.map(request => ({
19
21
  id: Number(request.get('id') || 0),
@@ -28,16 +30,20 @@ export default new Action({
28
30
  createdAt: String(request.get('created_at') || ''),
29
31
  }))
30
32
 
31
- return {
32
- requests,
33
- stats: {
34
- total,
35
- errorCount,
36
- averageDurationMs: Number(averageDurationMs || 0),
37
- successRate: total > 0 ? ((total - errorCount) / total) * 100 : 0,
38
- requestsLastHour,
39
- requestsPerMinute: requestsLastHour / 60,
40
- },
33
+ return {
34
+ requests,
35
+ stats: {
36
+ total,
37
+ errorCount,
38
+ averageDurationMs: Number(averageDurationMs || 0),
39
+ successRate: total > 0 ? ((total - errorCount) / total) * 100 : 0,
40
+ requestsLastHour,
41
+ requestsPerMinute: requestsLastHour / 60,
42
+ },
43
+ }
44
+ }
45
+ catch (error) {
46
+ return dashboardOperationalError(error, 'Request history could not be loaded.', 'RequestIndexAction')
41
47
  }
42
48
  },
43
49
  })
@@ -1,21 +1,27 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { tsCloud } from '~/config/cloud'
3
3
  import { getDashboardCloudSnapshot } from '../Cloud/cloud-overview'
4
+ import { dashboardOperationalError } from '../dashboard-response'
4
5
 
5
6
  export default new Action({
6
7
  name: 'ServerIndexAction',
7
8
  description: 'Returns configured servers and persisted deployment state.',
8
9
  method: 'GET',
9
10
  async handle() {
10
- const snapshot = await getDashboardCloudSnapshot(tsCloud)
11
- return {
12
- project: snapshot.project,
13
- environments: snapshot.environments,
14
- servers: snapshot.serverDefinitions,
15
- deployments: snapshot.deployments,
16
- network: snapshot.resources.filter(resource => resource.category === 'network'),
17
- events: snapshot.events,
18
- generatedAt: snapshot.generatedAt,
11
+ try {
12
+ const snapshot = await getDashboardCloudSnapshot(tsCloud)
13
+ return {
14
+ project: snapshot.project,
15
+ environments: snapshot.environments,
16
+ servers: snapshot.serverDefinitions,
17
+ deployments: snapshot.deployments,
18
+ network: snapshot.resources.filter(resource => resource.category === 'network'),
19
+ events: snapshot.events,
20
+ generatedAt: snapshot.generatedAt,
21
+ }
22
+ }
23
+ catch (error) {
24
+ return dashboardOperationalError(error, 'Server state could not be loaded.', 'ServerIndexAction')
19
25
  }
20
26
  },
21
27
  })