@stacksjs/defaults 0.74.3 → 0.74.4

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 (30) hide show
  1. package/ai/skills/stacks-auto-imports/SKILL.md +1 -1
  2. package/ai/skills/stacks-buddy/SKILL.md +53 -3
  3. package/ai/skills/stacks-cloud/SKILL.md +83 -11
  4. package/ai/skills/stacks-commerce/SKILL.md +1 -1
  5. package/ai/skills/stacks-composables/SKILL.md +1 -1
  6. package/ai/skills/stacks-dashboard/SKILL.md +2 -2
  7. package/ai/skills/stacks-deploy/SKILL.md +97 -24
  8. package/ai/skills/stacks-orm/SKILL.md +1 -1
  9. package/ai/skills/stacks-types/SKILL.md +1 -1
  10. package/ai/skills/stacks-writing-for-agents/SKILL.md +1 -1
  11. package/app/Actions/Buddy/CommandsAction.ts +1 -10
  12. package/app/Actions/Dashboard/Analytics/WebAnalyticsAction.ts +2 -7
  13. package/app/Actions/Dashboard/Analytics/web-analytics-provider.ts +103 -0
  14. package/app/Actions/Dashboard/Infrastructure/LogIndexAction.ts +24 -84
  15. package/app/Actions/Dashboard/Infrastructure/log-provider.ts +189 -0
  16. package/app/Actions/Dashboard/dashboard-provider.ts +170 -0
  17. package/app/Actions/Monitoring/ErrorGroupAction.ts +2 -4
  18. package/app/Actions/Monitoring/ErrorIndexAction.ts +2 -4
  19. package/app/Actions/Monitoring/ErrorShowAction.ts +4 -4
  20. package/app/Actions/Monitoring/ErrorStatsAction.ts +2 -4
  21. package/app/Actions/Monitoring/ErrorTimelineAction.ts +2 -4
  22. package/app/Actions/Monitoring/error-provider.ts +130 -0
  23. package/app/Models/EmailIdempotency.ts +4 -1
  24. package/app/Models/EmailSuppression.ts +4 -1
  25. package/app/Models/EmailWebhookEvent.ts +4 -1
  26. package/app/Models/Request.ts +4 -1
  27. package/functions/api-url.test.ts +61 -0
  28. package/functions/api-url.ts +19 -2
  29. package/ide/vscode/package.json +1 -1
  30. package/package.json +2 -2
@@ -1,16 +1,35 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
+ import type { DashboardLogQuery, DashboardLogRange } from './log-provider'
2
3
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database'
4
4
  import { dashboardRequestValue } from '../dashboard-request'
5
5
  import { dashboardOperationalError } from '../dashboard-response'
6
- import { DASHBOARD_LOG_TYPES, normalizeDashboardLog, summarizeDashboardLogTypes } from './log-dashboard'
7
-
8
- const RANGES = ['1', '7', '30', '90', 'all'] as const
6
+ import { DASHBOARD_LOG_TYPES } from './log-dashboard'
7
+ import { DASHBOARD_LOG_RANGES, readDashboardLogs } from './log-provider'
9
8
 
10
9
  function queryValue(request: RequestInstance, key: string): string {
11
10
  return dashboardRequestValue(request, key)
12
11
  }
13
12
 
13
+ /** Reads the filters off the request. Unknown values fall back rather than fail. */
14
+ function logQuery(request: RequestInstance): DashboardLogQuery {
15
+ const requestedType = queryValue(request, 'type').toLowerCase()
16
+ const requestedRange = queryValue(request, 'range').toLowerCase()
17
+
18
+ return {
19
+ page: Math.max(1, Number.parseInt(queryValue(request, 'page') || '1', 10) || 1),
20
+ perPage: Math.min(100, Math.max(10, Number.parseInt(queryValue(request, 'per_page') || '25', 10) || 25)),
21
+ search: queryValue(request, 'search'),
22
+ type: DASHBOARD_LOG_TYPES.includes(requestedType as typeof DASHBOARD_LOG_TYPES[number])
23
+ ? requestedType
24
+ : '',
25
+ source: queryValue(request, 'source').toLowerCase(),
26
+ project: queryValue(request, 'project'),
27
+ range: DASHBOARD_LOG_RANGES.includes(requestedRange as DashboardLogRange)
28
+ ? requestedRange as DashboardLogRange
29
+ : '30',
30
+ }
31
+ }
32
+
14
33
  export default new Action({
15
34
  name: 'LogIndexAction',
16
35
  description: 'Returns filtered, paginated Log model records for the dashboard.',
@@ -19,86 +38,7 @@ export default new Action({
19
38
 
20
39
  async handle(request: RequestInstance) {
21
40
  try {
22
- const requestedPage = Math.max(1, Number.parseInt(queryValue(request, 'page') || '1', 10) || 1)
23
- const perPage = Math.min(100, Math.max(10, Number.parseInt(queryValue(request, 'per_page') || '25', 10) || 25))
24
- const search = queryValue(request, 'search')
25
- const requestedType = queryValue(request, 'type').toLowerCase()
26
- const type = DASHBOARD_LOG_TYPES.includes(requestedType as typeof DASHBOARD_LOG_TYPES[number])
27
- ? requestedType
28
- : ''
29
- const source = queryValue(request, 'source').toLowerCase()
30
- const project = queryValue(request, 'project')
31
- const requestedRange = queryValue(request, 'range').toLowerCase()
32
- const range = RANGES.includes(requestedRange as typeof RANGES[number]) ? requestedRange : '30'
33
- const cutoff = range === 'all' ? 0 : Date.now() - Number(range) * 86400000
34
- const searchPattern = `%${search}%`
35
-
36
- const applyFilters = <T extends {
37
- where: (...args: any[]) => T
38
- whereAny: (columns: string[], operator: string, value: unknown) => T
39
- }>(query: T): T => {
40
- let filtered = query
41
- if (cutoff)
42
- filtered = filtered.where('timestamp', '>=', cutoff)
43
- if (type)
44
- filtered = filtered.where('type', '=', type)
45
- if (source)
46
- filtered = filtered.where('source', '=', source)
47
- if (project)
48
- filtered = filtered.where('project', '=', project)
49
- if (search)
50
- filtered = filtered.whereAny(['message', 'project', 'file', 'stacktrace'], 'like', searchPattern)
51
- return filtered
52
- }
53
-
54
- const [countRow, typeRows, sourceRows, projectRows] = await Promise.all([
55
- applyFilters(db.selectFrom('logs').select(db.fn.count('id').as('count')))
56
- .executeTakeFirst() as Promise<{ count: number | string } | undefined>,
57
- applyFilters(db.selectFrom('logs').select(['type', db.fn.count('id').as('count')]))
58
- .groupBy('type')
59
- .execute() as Promise<Array<{ type: string, count: number | string }>>,
60
- db.selectFrom('logs').select('source').distinct().orderBy('source', 'asc').limit(100).execute() as Promise<Array<{ source: string }>>,
61
- db.selectFrom('logs').select('project').distinct().orderBy('project', 'asc').limit(500).execute() as Promise<Array<{ project: string }>>,
62
- ])
63
-
64
- const total = Number(countRow?.count || 0)
65
- const totalPages = Math.max(1, Math.ceil(total / perPage))
66
- const page = Math.min(requestedPage, totalPages)
67
- const rows = await applyFilters(db.selectFrom('logs').select([
68
- 'id',
69
- 'timestamp',
70
- 'type',
71
- 'source',
72
- 'message',
73
- 'project',
74
- 'stacktrace',
75
- 'file',
76
- 'created_at',
77
- 'updated_at',
78
- ]))
79
- .orderBy('timestamp', 'desc')
80
- .orderBy('id', 'desc')
81
- .limit(perPage)
82
- .offset((page - 1) * perPage)
83
- .execute()
84
-
85
- return {
86
- logs: rows.map(normalizeDashboardLog),
87
- summary: summarizeDashboardLogTypes(typeRows, total),
88
- pagination: {
89
- page,
90
- perPage,
91
- total,
92
- totalPages,
93
- },
94
- options: {
95
- sources: sourceRows.map(row => row.source).filter(Boolean),
96
- projects: projectRows.map(row => row.project).filter(Boolean),
97
- types: [...DASHBOARD_LOG_TYPES],
98
- ranges: [...RANGES],
99
- },
100
- generatedAt: new Date().toISOString(),
101
- }
41
+ return await readDashboardLogs(logQuery(request))
102
42
  }
103
43
  catch (error) {
104
44
  return dashboardOperationalError(error, 'Logs could not be loaded.', 'LogIndexAction')
@@ -0,0 +1,189 @@
1
+ import type { DashboardProviderUnavailable } from '../dashboard-provider'
2
+ import type { DashboardLogRecord, DashboardLogSummary } from './log-dashboard'
3
+ import { db } from '@stacksjs/database'
4
+ import { HQ_READ_UNAVAILABLE, readThroughProvider, resolveDashboardDriver } from '../dashboard-provider'
5
+ import { DASHBOARD_LOG_TYPES, normalizeDashboardLog, summarizeDashboardLogTypes } from './log-dashboard'
6
+
7
+ /**
8
+ * Where the Logs section reads from.
9
+ *
10
+ * The local provider is the query that used to sit inside `LogIndexAction`,
11
+ * moved without changing a line of its logic: the same five statements against
12
+ * the `logs` table, the same filters, the same ordering, and the same clamp of
13
+ * a requested page down to the last page that exists.
14
+ *
15
+ * The payload deliberately reuses `DashboardLogRecord` and
16
+ * `DashboardLogSummary` from `log-dashboard.ts`. Three stx components import
17
+ * those same types, so a provider that returns the wrong shape is a compile
18
+ * error against the declaration the components themselves consume rather than
19
+ * a surprise at runtime.
20
+ */
21
+
22
+ export const DASHBOARD_LOG_RANGES = ['1', '7', '30', '90', 'all'] as const
23
+
24
+ export type DashboardLogRange = typeof DASHBOARD_LOG_RANGES[number]
25
+
26
+ export interface DashboardLogQuery {
27
+ page: number
28
+ perPage: number
29
+ search: string
30
+ /** One of {@link DASHBOARD_LOG_TYPES}, or empty for every level. */
31
+ type: string
32
+ source: string
33
+ project: string
34
+ range: DashboardLogRange
35
+ }
36
+
37
+ export interface DashboardLogPagination {
38
+ page: number
39
+ perPage: number
40
+ total: number
41
+ totalPages: number
42
+ }
43
+
44
+ export interface DashboardLogOptions {
45
+ sources: string[]
46
+ projects: string[]
47
+ types: string[]
48
+ ranges: string[]
49
+ }
50
+
51
+ export interface DashboardLogPayload extends Partial<DashboardProviderUnavailable> {
52
+ logs: DashboardLogRecord[]
53
+ summary: DashboardLogSummary
54
+ pagination: DashboardLogPagination
55
+ options: DashboardLogOptions
56
+ generatedAt: string
57
+ }
58
+
59
+ export interface DashboardLogsProvider {
60
+ logs: (query: DashboardLogQuery) => Promise<DashboardLogPayload>
61
+ }
62
+
63
+ /** An empty page of logs, carrying the reason there is nothing to show. */
64
+ export function emptyLogPayload(reason: string, query?: DashboardLogQuery): DashboardLogPayload {
65
+ return {
66
+ logs: [],
67
+ summary: { total: 0, error: 0, warning: 0, info: 0, success: 0 },
68
+ pagination: {
69
+ page: 1,
70
+ perPage: query?.perPage ?? 25,
71
+ total: 0,
72
+ totalPages: 1,
73
+ },
74
+ options: {
75
+ sources: [],
76
+ projects: [],
77
+ types: [...DASHBOARD_LOG_TYPES],
78
+ ranges: [...DASHBOARD_LOG_RANGES],
79
+ },
80
+ generatedAt: new Date().toISOString(),
81
+ unavailable: reason,
82
+ }
83
+ }
84
+
85
+ function isLogPayload(payload: unknown): boolean {
86
+ if (!payload || typeof payload !== 'object')
87
+ return false
88
+ const candidate = payload as Partial<DashboardLogPayload>
89
+ return Array.isArray(candidate.logs)
90
+ && Boolean(candidate.summary)
91
+ && Boolean(candidate.pagination)
92
+ && Boolean(candidate.options)
93
+ }
94
+
95
+ /**
96
+ * This application's own `logs` table.
97
+ *
98
+ * Not wrapped in a catch. A failure here is a local database failure, and the
99
+ * action has always answered that with its operational error, which is the
100
+ * behaviour every existing caller and test expects.
101
+ */
102
+ export const localLogsProvider: DashboardLogsProvider = {
103
+ async logs(query: DashboardLogQuery): Promise<DashboardLogPayload> {
104
+ const cutoff = query.range === 'all' ? 0 : Date.now() - Number(query.range) * 86400000
105
+ const searchPattern = `%${query.search}%`
106
+
107
+ const applyFilters = <T extends {
108
+ where: (...args: any[]) => T
109
+ whereAny: (columns: string[], operator: string, value: unknown) => T
110
+ }>(builder: T): T => {
111
+ let filtered = builder
112
+ if (cutoff)
113
+ filtered = filtered.where('timestamp', '>=', cutoff)
114
+ if (query.type)
115
+ filtered = filtered.where('type', '=', query.type)
116
+ if (query.source)
117
+ filtered = filtered.where('source', '=', query.source)
118
+ if (query.project)
119
+ filtered = filtered.where('project', '=', query.project)
120
+ if (query.search)
121
+ filtered = filtered.whereAny(['message', 'project', 'file', 'stacktrace'], 'like', searchPattern)
122
+ return filtered
123
+ }
124
+
125
+ const [countRow, typeRows, sourceRows, projectRows] = await Promise.all([
126
+ applyFilters(db.selectFrom('logs').select(db.fn.count('id').as('count')))
127
+ .executeTakeFirst() as Promise<{ count: number | string } | undefined>,
128
+ applyFilters(db.selectFrom('logs').select(['type', db.fn.count('id').as('count')]))
129
+ .groupBy('type')
130
+ .execute() as Promise<Array<{ type: string, count: number | string }>>,
131
+ db.selectFrom('logs').select('source').distinct().orderBy('source', 'asc').limit(100).execute() as Promise<Array<{ source: string }>>,
132
+ db.selectFrom('logs').select('project').distinct().orderBy('project', 'asc').limit(500).execute() as Promise<Array<{ project: string }>>,
133
+ ])
134
+
135
+ const total = Number(countRow?.count || 0)
136
+ const totalPages = Math.max(1, Math.ceil(total / query.perPage))
137
+ const page = Math.min(query.page, totalPages)
138
+ const rows = await applyFilters(db.selectFrom('logs').select([
139
+ 'id',
140
+ 'timestamp',
141
+ 'type',
142
+ 'source',
143
+ 'message',
144
+ 'project',
145
+ 'stacktrace',
146
+ 'file',
147
+ 'created_at',
148
+ 'updated_at',
149
+ ]))
150
+ .orderBy('timestamp', 'desc')
151
+ .orderBy('id', 'desc')
152
+ .limit(query.perPage)
153
+ .offset((page - 1) * query.perPage)
154
+ .execute()
155
+
156
+ return {
157
+ logs: rows.map(normalizeDashboardLog),
158
+ summary: summarizeDashboardLogTypes(typeRows, total),
159
+ pagination: {
160
+ page,
161
+ perPage: query.perPage,
162
+ total,
163
+ totalPages,
164
+ },
165
+ options: {
166
+ sources: sourceRows.map(row => row.source).filter(Boolean),
167
+ projects: projectRows.map(row => row.project).filter(Boolean),
168
+ types: [...DASHBOARD_LOG_TYPES],
169
+ ranges: [...DASHBOARD_LOG_RANGES],
170
+ },
171
+ generatedAt: new Date().toISOString(),
172
+ }
173
+ },
174
+ }
175
+
176
+ /** Reads the Logs section through whichever provider config selects. */
177
+ export async function readDashboardLogs(query: DashboardLogQuery): Promise<DashboardLogPayload> {
178
+ const driver = await resolveDashboardDriver('logs')
179
+
180
+ if (driver.name === 'local')
181
+ return localLogsProvider.logs(query)
182
+
183
+ return readThroughProvider(
184
+ 'logs',
185
+ reason => emptyLogPayload(reason, query),
186
+ async () => emptyLogPayload(HQ_READ_UNAVAILABLE, query),
187
+ isLogPayload,
188
+ )
189
+ }
@@ -0,0 +1,170 @@
1
+ import type { DashboardHqDriverOptions, DashboardProviderName, DashboardProviderSelection } from '@stacksjs/types'
2
+
3
+ /**
4
+ * The seam between a dashboard section and the data behind it.
5
+ *
6
+ * Every section in the dashboard used to reach for a local store directly:
7
+ * the Logs page queried the `logs` table, the Errors pages called into
8
+ * `@stacksjs/commerce`, and web Analytics aggregated the `Request` model. That
9
+ * works exactly once, for the application the dashboard is running inside. It
10
+ * cannot show the logs of a fleet, and it gave the hosted products (loghq,
11
+ * bughq, analyticshq) nowhere to plug in.
12
+ *
13
+ * This module resolves which provider a domain reads from. `local` is the
14
+ * default and is byte-for-byte the behaviour that shipped before the seam
15
+ * existed, including how it fails: a local provider is never wrapped, so a
16
+ * database outage still surfaces the same operational error it always did.
17
+ * Anything else is remote, and remote is wrapped, because a section whose
18
+ * backend is unreachable has to render empty with a reason rather than take
19
+ * the page down.
20
+ *
21
+ * Read paths only. No provider writes.
22
+ */
23
+
24
+ /** The dashboard sections that resolve a provider. */
25
+ export type DashboardProviderDomain = 'logs' | 'errors' | 'analytics'
26
+
27
+ const PROVIDER_NAMES: readonly DashboardProviderName[] = ['local', 'hq']
28
+
29
+ /**
30
+ * Why a section has no data.
31
+ *
32
+ * Carried on the payload beside the empty collections rather than thrown, so
33
+ * the section renders its own empty state instead of an error page. The string
34
+ * is written for whoever is looking at the dashboard, not for a log file.
35
+ */
36
+ export interface DashboardProviderUnavailable {
37
+ unavailable: string
38
+ }
39
+
40
+ export interface ResolvedDashboardDriver {
41
+ name: DashboardProviderName
42
+ options: DashboardHqDriverOptions
43
+ }
44
+
45
+ const resolved = new Map<DashboardProviderDomain, ResolvedDashboardDriver>()
46
+
47
+ /**
48
+ * Forgets every memoized driver choice.
49
+ *
50
+ * Resolution is cached per domain so a request does not re-read config on
51
+ * every query. Tests that walk more than one configuration in a single process
52
+ * need that cache cleared between cases, and clearing it is much cheaper than
53
+ * `mock.module`, which replaces the shared module instance for every other
54
+ * test file in the run and is not undone by `mock.restore()`.
55
+ */
56
+ export function resetDashboardProviders(): void {
57
+ resolved.clear()
58
+ }
59
+
60
+ function readSelection(section: unknown, domain: DashboardProviderDomain): DashboardProviderSelection {
61
+ if (!section || typeof section !== 'object')
62
+ return {}
63
+ const providers = (section as { providers?: unknown }).providers
64
+ if (!providers || typeof providers !== 'object')
65
+ return {}
66
+ const selection = (providers as Record<string, unknown>)[domain]
67
+ return selection && typeof selection === 'object' ? selection as DashboardProviderSelection : {}
68
+ }
69
+
70
+ function selectDriverName(value: unknown, domain: DashboardProviderDomain): DashboardProviderName {
71
+ if (value === undefined || value === null || value === '')
72
+ return 'local'
73
+
74
+ switch (value) {
75
+ case 'local':
76
+ return 'local'
77
+ case 'hq':
78
+ return 'hq'
79
+ default:
80
+ // `satisfies DashboardConfig` is erased at runtime, and config files are
81
+ // ordinary TypeScript an app can hand-edit, so an unknown name reaches
82
+ // here as a plain string. Falling back to local keeps the section
83
+ // rendering; throwing would take it down over a typo.
84
+ console.warn(
85
+ `[dashboard/api] config.dashboard.providers.${domain}.driver is `
86
+ + `"${String(value)}", which is not a known provider. Reading local data instead. `
87
+ + `Available providers: ${PROVIDER_NAMES.join(', ')}.`,
88
+ )
89
+ return 'local'
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Reads a domain's provider choice from config.
95
+ *
96
+ * Config is read through `overridesReady`. A read that happens before the
97
+ * application's own config files have been layered in returns the framework
98
+ * defaults with no error at all, which would silently resolve `local` in an
99
+ * application that had asked for something else.
100
+ */
101
+ export async function resolveDashboardDriver(domain: DashboardProviderDomain): Promise<ResolvedDashboardDriver> {
102
+ const cached = resolved.get(domain)
103
+ if (cached)
104
+ return cached
105
+
106
+ let selection: DashboardProviderSelection = {}
107
+ try {
108
+ const { config, overridesReady } = await import('@stacksjs/config')
109
+ // Rejection means boot could not read the application's config at all.
110
+ // The framework defaults are still a usable answer, and they are local.
111
+ await overridesReady.catch(() => {})
112
+ selection = readSelection(config.dashboard, domain)
113
+ }
114
+ catch (error) {
115
+ console.warn(`[dashboard/api] dashboard provider config for ${domain} could not be read, using local:`, error)
116
+ }
117
+
118
+ const driver: ResolvedDashboardDriver = {
119
+ name: selectDriverName(selection.driver, domain),
120
+ options: selection.drivers?.hq ?? {},
121
+ }
122
+
123
+ resolved.set(domain, driver)
124
+ return driver
125
+ }
126
+
127
+ /**
128
+ * Runs a remote provider so that it cannot take a section down.
129
+ *
130
+ * Three ways a remote read goes wrong, all of which land on the same empty
131
+ * payload plus a reason: it rejects, it never answers, or it answers with
132
+ * something that is not the shape the section renders. The last one matters
133
+ * as much as the first two, because a payload that is merely missing its
134
+ * array reaches the component and fails there instead, which is a blank page
135
+ * rather than an empty state.
136
+ */
137
+ export async function readThroughProvider<T>(
138
+ domain: DashboardProviderDomain,
139
+ empty: (reason: string) => T,
140
+ read: () => Promise<T>,
141
+ isValid: (payload: unknown) => boolean,
142
+ ): Promise<T> {
143
+ try {
144
+ const payload = await read()
145
+ if (!isValid(payload)) {
146
+ console.error(`[dashboard/api] the ${domain} provider answered with an unexpected shape`)
147
+ return empty('This section could not be read from its provider.')
148
+ }
149
+ return payload
150
+ }
151
+ catch (error) {
152
+ console.error(`[dashboard/api] the ${domain} provider could not be read:`, error)
153
+ return empty('This section could not be reached. It is configured to read from a hosted provider.')
154
+ }
155
+ }
156
+
157
+ /**
158
+ * The reason an `hq` provider has no data yet.
159
+ *
160
+ * Both published SDKs are ingest clients. `@loghq/stacks` and `@bughq/stacks`
161
+ * send records and expose no function that reads any back, and a project's
162
+ * ingest key grants no read access by design. The hosted read APIs do exist,
163
+ * but they authenticate a person rather than a machine, so wiring one needs a
164
+ * product decision about service credentials before any code here can help.
165
+ *
166
+ * Selecting `hq` today therefore yields an empty section that says so, which
167
+ * is the honest answer and keeps the degrade path real rather than theoretical.
168
+ */
169
+ export const HQ_READ_UNAVAILABLE
170
+ = 'No data. The hosted provider has no read API yet, so this section cannot be filled from it.'
@@ -1,6 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { errors } from '@stacksjs/commerce'
3
2
  import { response } from '@stacksjs/router'
3
+ import { readErrorsByGroup } from './error-provider'
4
4
 
5
5
  export default new Action({
6
6
  name: 'Error Group',
@@ -14,8 +14,6 @@ export default new Action({
14
14
  return response.json({ error: 'Both type and message are required' }, 400)
15
15
  }
16
16
 
17
- const results = await errors.fetchByGroup(type, message)
18
-
19
- return response.json({ data: results })
17
+ return response.json(await readErrorsByGroup(type, message))
20
18
  },
21
19
  })
@@ -1,14 +1,12 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { errors } from '@stacksjs/commerce'
3
2
  import { response } from '@stacksjs/router'
3
+ import { readGroupedErrors } from './error-provider'
4
4
 
5
5
  export default new Action({
6
6
  name: 'Error Index',
7
7
  description: 'Fetch all grouped errors (Sentry-like aggregation)',
8
8
  method: 'GET',
9
9
  async handle() {
10
- const results = await errors.fetchGrouped()
11
-
12
- return response.json({ data: results })
10
+ return response.json(await readGroupedErrors())
13
11
  },
14
12
  })
@@ -1,6 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { errors } from '@stacksjs/commerce'
3
2
  import { response } from '@stacksjs/router'
3
+ import { readErrorById } from './error-provider'
4
4
 
5
5
  export default new Action({
6
6
  name: 'Error Show',
@@ -13,12 +13,12 @@ export default new Action({
13
13
  if (id === null)
14
14
  return response.json({ error: 'A numeric error id is required' }, 422)
15
15
 
16
- const result = await errors.fetchById(id)
16
+ const result = await readErrorById(id)
17
17
 
18
- if (!result) {
18
+ if (!result.data) {
19
19
  return response.json({ error: 'Error not found' }, 404)
20
20
  }
21
21
 
22
- return response.json({ data: result })
22
+ return response.json(result)
23
23
  },
24
24
  })
@@ -1,14 +1,12 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { errors } from '@stacksjs/commerce'
3
2
  import { response } from '@stacksjs/router'
3
+ import { readErrorStats } from './error-provider'
4
4
 
5
5
  export default new Action({
6
6
  name: 'Error Stats',
7
7
  description: 'Fetch error statistics',
8
8
  method: 'GET',
9
9
  async handle() {
10
- const stats = await errors.fetchStats()
11
-
12
- return response.json({ data: stats })
10
+ return response.json(await readErrorStats())
13
11
  },
14
12
  })
@@ -1,14 +1,12 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { errors } from '@stacksjs/commerce'
3
2
  import { response } from '@stacksjs/router'
3
+ import { readErrorTimeline } from './error-provider'
4
4
 
5
5
  export default new Action({
6
6
  name: 'Error Timeline',
7
7
  description: 'Fetch error timeline (hourly counts for last 24 hours)',
8
8
  method: 'GET',
9
9
  async handle() {
10
- const timeline = await errors.fetchTimeline()
11
-
12
- return response.json({ data: timeline })
10
+ return response.json(await readErrorTimeline())
13
11
  },
14
12
  })