@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.
- package/ai/skills/stacks-browse/SKILL.md +5 -1
- package/ai/skills/stacks-browse/scripts/browse.ts +61 -6
- package/ai/skills/stacks-orm/SKILL.md +6 -0
- package/app/Actions/Dashboard/Actions/GetActions.ts +15 -9
- package/app/Actions/Dashboard/Analytics/EventStoreAction.ts +21 -9
- package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +12 -3
- package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +12 -3
- package/app/Actions/Dashboard/Content/AuthorDestroyAction.ts +23 -9
- package/app/Actions/Dashboard/Content/AuthorIndexAction.ts +32 -26
- package/app/Actions/Dashboard/Content/AuthorStoreAction.ts +44 -24
- package/app/Actions/Dashboard/Content/AuthorUpdateAction.ts +48 -26
- package/app/Actions/Dashboard/Content/CategoryDestroyAction.ts +27 -8
- package/app/Actions/Dashboard/Content/CategoryIndexAction.ts +21 -15
- package/app/Actions/Dashboard/Content/CategoryStoreAction.ts +47 -29
- package/app/Actions/Dashboard/Content/CommentDestroyAction.ts +17 -4
- package/app/Actions/Dashboard/Content/CommentIndexAction.ts +23 -17
- package/app/Actions/Dashboard/Content/CommentUpdateAction.ts +30 -12
- package/app/Actions/Dashboard/Content/ContentDashboardAction.ts +43 -37
- package/app/Actions/Dashboard/Content/PageDestroyAction.ts +17 -4
- package/app/Actions/Dashboard/Content/PageIndexAction.ts +23 -17
- package/app/Actions/Dashboard/Content/PageStoreAction.ts +33 -18
- package/app/Actions/Dashboard/Content/PageUpdateAction.ts +40 -22
- package/app/Actions/Dashboard/Content/PostDestroyAction.ts +24 -11
- package/app/Actions/Dashboard/Content/PostIndexAction.ts +55 -49
- package/app/Actions/Dashboard/Content/PostStoreAction.ts +43 -33
- package/app/Actions/Dashboard/Content/PostUpdateAction.ts +46 -34
- package/app/Actions/Dashboard/Content/TagDestroyAction.ts +27 -8
- package/app/Actions/Dashboard/Content/TagIndexAction.ts +31 -25
- package/app/Actions/Dashboard/Content/TagStoreAction.ts +44 -23
- package/app/Actions/Dashboard/Content/content-input.ts +4 -4
- package/app/Actions/Dashboard/Content/post-input.ts +60 -8
- package/app/Actions/Dashboard/DashboardHealthAction.ts +34 -14
- package/app/Actions/Dashboard/DashboardHomeAction.test.ts +1 -1
- package/app/Actions/Dashboard/DashboardHomeAction.ts +31 -4
- package/app/Actions/Dashboard/DashboardStatsAction.ts +6 -1
- package/app/Actions/Dashboard/Email/InboxSendAction.ts +14 -7
- package/app/Actions/Dashboard/Infrastructure/CommandIndexAction.ts +33 -27
- package/app/Actions/Dashboard/Infrastructure/EnvironmentIndexAction.ts +10 -4
- package/app/Actions/Dashboard/Infrastructure/EnvironmentUpdateAction.ts +8 -1
- package/app/Actions/Dashboard/Infrastructure/RequestIndexAction.ts +24 -18
- package/app/Actions/Dashboard/Infrastructure/ServerIndexAction.ts +15 -9
- package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +9 -2
- package/app/Actions/Dashboard/Marketing/CampaignCancelAction.ts +23 -5
- package/app/Actions/Dashboard/Marketing/CampaignDestroyAction.ts +26 -17
- package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +49 -43
- package/app/Actions/Dashboard/Marketing/CampaignScheduleAction.ts +31 -25
- package/app/Actions/Dashboard/Marketing/CampaignSendAction.ts +28 -22
- package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +15 -10
- package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +19 -12
- package/app/Actions/Dashboard/Marketing/ListDestroyAction.ts +34 -25
- package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +32 -26
- package/app/Actions/Dashboard/Marketing/ListStoreAction.ts +31 -18
- package/app/Actions/Dashboard/Marketing/ListUpdateAction.ts +35 -19
- package/app/Actions/Dashboard/Marketing/SocialPostDestroyAction.ts +15 -5
- package/app/Actions/Dashboard/Marketing/SocialPostIndexAction.ts +11 -5
- package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +19 -13
- package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +22 -12
- package/app/Actions/Dashboard/Marketing/campaign-delivery.ts +34 -0
- package/app/Actions/Dashboard/Marketing/marketing-list-records.test.ts +10 -0
- package/app/Actions/Dashboard/Marketing/marketing-list-records.ts +10 -0
- package/app/Actions/Dashboard/Marketing/marketing-response.ts +28 -0
- package/app/Actions/Dashboard/Marketing/social-post-records.test.ts +13 -0
- package/app/Actions/Dashboard/Marketing/social-post-records.ts +22 -4
- package/app/Actions/Dashboard/Models/GetModels.ts +14 -7
- package/app/Actions/Dashboard/Models/GetSubscriberCount.ts +8 -1
- package/app/Actions/Dashboard/Models/GetUserCount.ts +8 -1
- package/app/Actions/Dashboard/Releases/ReleaseIndexAction.ts +11 -5
- package/app/Actions/Dashboard/Settings/MailSettingsGetAction.ts +10 -4
- package/app/Actions/Dashboard/Settings/MailSettingsUpdateAction.ts +8 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/UI/Modal.stx +60 -10
- 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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
41
|
+
if (duplicate)
|
|
42
|
+
return null
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
const now = timestamp()
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
58
|
+
const id = insertedId(result)
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
if (!id)
|
|
61
|
+
throw new Error('Tag insert did not return an id.')
|
|
56
62
|
|
|
57
|
-
|
|
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 (
|
|
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 (
|
|
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 }
|
|
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
|
|
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
|
-
?
|
|
89
|
+
? database.selectFrom('authors').select(['id']).where('id', '=', payload.authorId).executeTakeFirst()
|
|
90
90
|
: Promise.resolve(undefined),
|
|
91
91
|
payload.categoryIds.length
|
|
92
|
-
?
|
|
92
|
+
? database.selectFrom('categories').whereIn('id', payload.categoryIds).select(['id']).execute()
|
|
93
93
|
: Promise.resolve([]),
|
|
94
94
|
payload.tagIds.length
|
|
95
|
-
?
|
|
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
|
-
|
|
14
|
-
|
|
14
|
+
try {
|
|
15
|
+
const health = await checkApplicationHealth()
|
|
16
|
+
const memory = process.memoryUsage()
|
|
17
|
+
const checks = Object.fromEntries(Object.entries(health.checks).map(([name, check]) => {
|
|
18
|
+
if (!check.ok) {
|
|
19
|
+
dashboardOperationalIssue(
|
|
20
|
+
check.message,
|
|
21
|
+
'Dependency probe failed.',
|
|
22
|
+
`DashboardHealthAction.${name}`,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
return [name, {
|
|
26
|
+
...check,
|
|
27
|
+
message: check.ok ? undefined : 'Dependency probe failed.',
|
|
28
|
+
}]
|
|
29
|
+
}))
|
|
15
30
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
return {
|
|
32
|
+
...health,
|
|
33
|
+
checks,
|
|
34
|
+
durationMs: Math.max(0, Math.round(performance.now() - startedAt)),
|
|
35
|
+
runtime: {
|
|
36
|
+
bunVersion: process.versions.bun || '',
|
|
37
|
+
nodeVersion: process.versions.node || '',
|
|
38
|
+
platform: process.platform,
|
|
39
|
+
architecture: process.arch,
|
|
40
|
+
uptimeSeconds: Math.max(0, Math.floor(process.uptime())),
|
|
41
|
+
residentMemoryBytes: memory.rss,
|
|
42
|
+
heapUsedBytes: memory.heapUsed,
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return dashboardOperationalError(error, 'System health could not be loaded.', 'DashboardHealthAction')
|
|
28
48
|
}
|
|
29
49
|
},
|
|
30
50
|
})
|
|
@@ -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.
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
})
|