@stacksjs/defaults 0.72.5 → 0.72.7
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/app/Actions/Auth/GenerateAuthenticationAction.ts +6 -6
- package/app/Actions/Auth/GenerateRegistrationAction.ts +10 -5
- package/app/Actions/Auth/RegisterAction.ts +5 -2
- package/app/Actions/Auth/VerifyAuthenticationAction.ts +5 -3
- package/app/Actions/Auth/VerifyRegistrationAction.ts +7 -3
- package/app/Actions/Billable/CheckoutAction.ts +14 -1
- package/app/Actions/Cms/AuthorDestroyAction.ts +1 -1
- package/app/Actions/Cms/AuthorShowAction.ts +1 -1
- package/app/Actions/Cms/AuthorUpdateAction.ts +1 -1
- package/app/Actions/Cms/CategorizableDestroyAction.ts +1 -1
- package/app/Actions/Cms/CategorizableShowAction.ts +1 -1
- package/app/Actions/Cms/CategorizableUpdateAction.ts +2 -3
- package/app/Actions/Cms/CommentDestroyAction.ts +1 -1
- package/app/Actions/Cms/CommentShowAction.ts +1 -1
- package/app/Actions/Cms/CommentUpdateAction.ts +1 -1
- package/app/Actions/Cms/PageDestroyAction.ts +1 -1
- package/app/Actions/Cms/PageStoreAction.ts +13 -13
- package/app/Actions/Cms/PageUpdateAction.ts +11 -8
- package/app/Actions/Cms/PostDestroyAction.ts +1 -1
- package/app/Actions/Cms/PostShowAction.ts +1 -1
- package/app/Actions/Cms/PostUpdateAction.ts +1 -1
- package/app/Actions/Cms/TaggableDestroyAction.ts +1 -1
- package/app/Actions/Cms/TaggableShowAction.ts +1 -1
- package/app/Actions/Cms/TaggableUpdateAction.ts +2 -2
- package/app/Actions/Commerce/GiftCardUpdateBalanceAction.ts +1 -1
- package/app/Actions/Commerce/WaitlistProductAnalyticsAction.ts +6 -2
- package/app/Actions/Commerce/WaitlistProductQuantityDistributionAction.ts +6 -2
- package/app/Actions/Commerce/WaitlistProductStatusAction.ts +8 -2
- package/app/Actions/Commerce/WaitlistProductTimeSeriesAction.ts +6 -2
- package/app/Actions/Dashboard/Analytics/campaign-analytics.ts +6 -1
- package/app/Actions/Dashboard/Analytics/request-analytics.ts +3 -3
- package/app/Actions/Dashboard/Buddy/buddy-chat.ts +8 -0
- package/app/Actions/Dashboard/Ci/runner-pressure-monitor.ts +2 -1
- package/app/Actions/Dashboard/Cloud/cloud-overview.ts +3 -1
- package/app/Actions/Dashboard/Commerce/CommerceDashboardAction.ts +12 -4
- package/app/Actions/Dashboard/Commerce/CommerceOrdersAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +2 -2
- package/app/Actions/Dashboard/Commerce/CommercePrintLogsAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +2 -2
- package/app/Actions/Dashboard/Commerce/CommerceProductsAction.ts +5 -3
- package/app/Actions/Dashboard/Commerce/DeliveryRouteIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/LicenseKeyIndexAction.ts +3 -3
- package/app/Actions/Dashboard/Commerce/PaymentRefundAction.ts +4 -4
- package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/ShippingMethodIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/ShippingRateIndexAction.ts +2 -2
- package/app/Actions/Dashboard/Commerce/ShippingZoneIndexAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +1 -1
- package/app/Actions/Dashboard/Commerce/commerce-dashboard.ts +20 -9
- package/app/Actions/Dashboard/Content/file-manager.ts +3 -2
- package/app/Actions/Dashboard/DashboardHomeAction.ts +1 -1
- package/app/Actions/Dashboard/DashboardStatsAction.ts +7 -4
- package/app/Actions/Dashboard/Deployments/CreateDeploymentRollback.ts +2 -2
- package/app/Actions/Dashboard/Deployments/deployment-input.ts +3 -3
- package/app/Actions/Dashboard/Deployments/deployment-rollback.ts +12 -0
- package/app/Actions/Dashboard/Infrastructure/InsightsAction.ts +3 -3
- package/app/Actions/Dashboard/Jobs/job-records.ts +3 -3
- package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +5 -3
- package/app/Actions/Dashboard/Kanban/CardStoreAction.ts +5 -3
- package/app/Actions/Dashboard/Kanban/CardsReorderAction.ts +1 -1
- package/app/Actions/Dashboard/Kanban/kanban-model.ts +3 -3
- package/app/Actions/Dashboard/Kanban/kanban-response.ts +2 -1
- package/app/Actions/Dashboard/Library/DependencyIndexAction.ts +1 -0
- package/app/Actions/Dashboard/Library/GetReleases.ts +8 -3
- package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +3 -3
- package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +7 -0
- package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +5 -1
- package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +12 -9
- package/app/Actions/Dashboard/Operations/control-plane.ts +16 -4
- package/app/Actions/Dashboard/Queries/QueryDashboardAction.ts +1 -1
- package/app/Actions/Dashboard/Queue/QueueStatsAction.ts +23 -14
- package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +1 -1
- package/app/Actions/Dashboard/Settings/mail-settings.ts +12 -4
- package/app/Actions/Dashboard/Source/source-inventory.ts +6 -2
- package/app/Actions/Dashboard/dashboard-response.ts +2 -1
- package/app/Actions/HealthAction.ts +11 -6
- package/app/Actions/Locale/SetLocaleAction.ts +3 -1
- package/app/Actions/Monitoring/ErrorShowAction.ts +5 -0
- package/app/Actions/Payment/CreatePaymentIntentAction.ts +3 -1
- package/app/Actions/SendWelcomeEmail.ts +3 -1
- package/app/Actions/Teams/team-response.ts +2 -1
- package/app/Actions/TestErrorAction.ts +1 -1
- package/app/Jobs/PruneModelsJob.ts +1 -1
- package/app/Jobs/PruneQueryLogsJob.ts +3 -1
- package/app/Mail/OrderConfirmation.ts +18 -2
- package/app/Middleware/Abilities.ts +3 -1
- package/app/Middleware/Can.ts +3 -3
- package/app/Middleware/Compress.ts +3 -1
- package/app/Middleware/Csrf.ts +10 -4
- package/app/Middleware/EnsureEmailIsVerified.ts +2 -1
- package/app/Middleware/Permission.ts +2 -1
- package/app/Middleware/Role.ts +2 -1
- package/app/Middleware/Team.ts +2 -1
- package/app/Middleware/Throttle.ts +1 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/functions/dashboard/data.ts +2 -2
|
@@ -24,12 +24,12 @@ export default new Action({
|
|
|
24
24
|
async handle(request: RequestInstance) {
|
|
25
25
|
const id = Number(request.getParam('id'))
|
|
26
26
|
if (!Number.isSafeInteger(id) || id <= 0)
|
|
27
|
-
return response.notFound(
|
|
27
|
+
return response.notFound('Product not found')
|
|
28
28
|
|
|
29
29
|
try {
|
|
30
30
|
const product = await Product.find(id)
|
|
31
31
|
if (!product)
|
|
32
|
-
return response.notFound(
|
|
32
|
+
return response.notFound('Product not found')
|
|
33
33
|
|
|
34
34
|
const [categories, manufacturers, variants, units, reviews] = await Promise.all([
|
|
35
35
|
Category.orderBy('name', 'asc').limit(500).get(),
|
|
@@ -22,12 +22,14 @@ export default new Action({
|
|
|
22
22
|
try {
|
|
23
23
|
const products = await Product.orderByDesc('id').limit(500).get()
|
|
24
24
|
const productIds = products.map(product => commerceRecordIdentifier(product, 'Product'))
|
|
25
|
+
// Identifiers normalize to strings; `product_id` is numeric.
|
|
26
|
+
const numericProductIds = productIds.map(Number).filter(Number.isSafeInteger)
|
|
25
27
|
const [categories, manufacturers, variants, units, reviews] = await Promise.all([
|
|
26
28
|
Category.orderBy('name', 'asc').limit(500).get(),
|
|
27
29
|
Manufacturer.orderBy('manufacturer', 'asc').limit(500).get(),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
numericProductIds.length > 0 ? ProductVariant.whereIn('product_id', numericProductIds).get() : [],
|
|
31
|
+
numericProductIds.length > 0 ? ProductUnit.whereIn('product_id', numericProductIds).get() : [],
|
|
32
|
+
numericProductIds.length > 0 ? Review.whereIn('product_id', numericProductIds).get() : [],
|
|
31
33
|
])
|
|
32
34
|
const categoryOptions = categories.map(normalizeProductOption)
|
|
33
35
|
const manufacturerOptions = manufacturers.map(normalizeManufacturerOption)
|
|
@@ -16,7 +16,7 @@ export default new Action({
|
|
|
16
16
|
try {
|
|
17
17
|
const routes = await DeliveryRoute.orderByDesc('last_active').limit(500).get()
|
|
18
18
|
const driverIds = [...new Set(routes.map(route => route.get('driver_id')).filter(Boolean))]
|
|
19
|
-
const drivers = driverIds.length ? await Driver.
|
|
19
|
+
const drivers = driverIds.length ? await Driver.whereIn('id', driverIds).get() : []
|
|
20
20
|
const driversById = indexDeliveryRouteDrivers(drivers)
|
|
21
21
|
return routes.map(route => normalizeDeliveryRouteRecord(route, driversById))
|
|
22
22
|
}
|
|
@@ -13,7 +13,7 @@ export default new Action({
|
|
|
13
13
|
try {
|
|
14
14
|
const drivers = await Driver.orderBy('name', 'asc').limit(500).get()
|
|
15
15
|
const userIds = [...new Set(drivers.map(driver => driver.get('user_id')).filter(Boolean))]
|
|
16
|
-
const users = userIds.length ? await User.
|
|
16
|
+
const users = userIds.length ? await User.whereIn('id', userIds).get() : []
|
|
17
17
|
const usersById = indexDriverUsers(users)
|
|
18
18
|
return drivers.map(driver => normalizeDriverRecord(driver, usersById))
|
|
19
19
|
}
|
|
@@ -21,9 +21,9 @@ export default new Action({
|
|
|
21
21
|
const productIds = [...new Set(keys.map(key => key.get('product_id')).filter(Boolean))]
|
|
22
22
|
const orderIds = [...new Set(keys.map(key => key.get('order_id')).filter(Boolean))]
|
|
23
23
|
const [customers, products, orders] = await Promise.all([
|
|
24
|
-
customerIds.length ? Customer.
|
|
25
|
-
productIds.length ? Product.
|
|
26
|
-
orderIds.length ? Order.
|
|
24
|
+
customerIds.length ? Customer.whereIn('id', customerIds).get() : [],
|
|
25
|
+
productIds.length ? Product.whereIn('id', productIds).get() : [],
|
|
26
|
+
orderIds.length ? Order.whereIn('id', orderIds).get() : [],
|
|
27
27
|
])
|
|
28
28
|
const customerMap = indexLicenseKeyOptions(customers.map(normalizeLicenseKeyCustomer))
|
|
29
29
|
const productMap = indexLicenseKeyOptions(products.map(normalizeLicenseKeyProduct))
|
|
@@ -15,9 +15,9 @@ export default new Action({
|
|
|
15
15
|
const id = Number(request.getParam('id'))
|
|
16
16
|
const amount = request.integer('amount')
|
|
17
17
|
if (!Number.isSafeInteger(id) || id <= 0)
|
|
18
|
-
return response.notFound(
|
|
18
|
+
return response.notFound('Payment not found')
|
|
19
19
|
if (!Number.isSafeInteger(amount) || amount <= 0)
|
|
20
|
-
return response.badRequest(
|
|
20
|
+
return response.badRequest('Refund amount must be a positive integer in minor units')
|
|
21
21
|
|
|
22
22
|
try {
|
|
23
23
|
const payment = await payments.recordRefund(id, amount)
|
|
@@ -26,9 +26,9 @@ export default new Action({
|
|
|
26
26
|
catch (error) {
|
|
27
27
|
const message = error instanceof Error ? error.message : String(error)
|
|
28
28
|
if (message.includes('not found'))
|
|
29
|
-
return response.notFound(
|
|
29
|
+
return response.notFound(message)
|
|
30
30
|
if (message.includes('cannot be refunded') || message.includes('exceeds the remaining'))
|
|
31
|
-
return response.badRequest(
|
|
31
|
+
return response.badRequest(message)
|
|
32
32
|
|
|
33
33
|
log.error('[dashboard:payments] Failed to record refund', { id, error })
|
|
34
34
|
return response.error('Unable to record the refund.', 500)
|
|
@@ -26,7 +26,7 @@ export default new Action({
|
|
|
26
26
|
return dashboardOperationalError(error, 'Product unit default could not be updated.', 'ProductUnitDefaultAction', 500)
|
|
27
27
|
}
|
|
28
28
|
if (!updated)
|
|
29
|
-
return response.notFound(
|
|
29
|
+
return response.notFound('Product unit not found')
|
|
30
30
|
|
|
31
31
|
return { success: true, id: String(id), isDefault }
|
|
32
32
|
},
|
|
@@ -18,7 +18,7 @@ export default new Action({
|
|
|
18
18
|
const methods = await ShippingMethod.orderBy('name', 'asc').limit(500).get()
|
|
19
19
|
const methodIds = shippingMethodIds(methods)
|
|
20
20
|
const zones = methodIds.size
|
|
21
|
-
? await ShippingZone.
|
|
21
|
+
? await ShippingZone.whereIn('shipping_method_id', [...methodIds]).orderBy('name', 'asc').get()
|
|
22
22
|
: []
|
|
23
23
|
const zonesByMethodId = groupShippingMethodZones(zones, methodIds)
|
|
24
24
|
return methods.map(method => normalizeShippingMethodRecord(method, zonesByMethodId))
|
|
@@ -19,8 +19,8 @@ export default new Action({
|
|
|
19
19
|
const methodIds = [...new Set(rates.map(rate => rate.get('shipping_method_id')).filter(Boolean))]
|
|
20
20
|
const zoneIds = [...new Set(rates.map(rate => rate.get('shipping_zone_id')).filter(Boolean))]
|
|
21
21
|
const [methods, zones] = await Promise.all([
|
|
22
|
-
methodIds.length ? ShippingMethod.
|
|
23
|
-
zoneIds.length ? ShippingZone.
|
|
22
|
+
methodIds.length ? ShippingMethod.whereIn('id', methodIds).get() : [],
|
|
23
|
+
zoneIds.length ? ShippingZone.whereIn('id', zoneIds).get() : [],
|
|
24
24
|
])
|
|
25
25
|
const methodsById = indexShippingRateMethods(methods)
|
|
26
26
|
const zonesById = indexShippingRateZones(zones)
|
|
@@ -17,7 +17,7 @@ export default new Action({
|
|
|
17
17
|
const zones = await ShippingZone.orderBy('name', 'asc').limit(500).get()
|
|
18
18
|
const methodIds = [...new Set(zones.map(zone => zone.get('shipping_method_id')).filter(Boolean))]
|
|
19
19
|
const methods = methodIds.length
|
|
20
|
-
? await ShippingMethod.
|
|
20
|
+
? await ShippingMethod.whereIn('id', methodIds).get()
|
|
21
21
|
: []
|
|
22
22
|
const methodsById = indexShippingZoneMethods(methods)
|
|
23
23
|
return zones.map(zone => normalizeShippingZoneRecord(zone, methodsById))
|
|
@@ -26,7 +26,7 @@ export default new Action({
|
|
|
26
26
|
return dashboardOperationalError(error, 'Tax rate default could not be updated.', 'TaxRateDefaultAction', 500)
|
|
27
27
|
}
|
|
28
28
|
if (!updated)
|
|
29
|
-
return response.notFound(
|
|
29
|
+
return response.notFound('Tax rate not found')
|
|
30
30
|
|
|
31
31
|
return { success: true, id: String(id), isDefault }
|
|
32
32
|
},
|
|
@@ -198,7 +198,9 @@ function rangeWindow(range: CommerceDashboardRange, now: Date, rows: CommerceDas
|
|
|
198
198
|
.map(row => timestamp(row.createdAt))
|
|
199
199
|
.filter(Number.isFinite)
|
|
200
200
|
.sort((left, right) => left - right)[0]
|
|
201
|
-
|
|
201
|
+
// `earliest` is `number | undefined` (the array may be empty), and
|
|
202
|
+
// `Number.isFinite` does not narrow it for `new Date(...)`.
|
|
203
|
+
const start = earliest !== undefined && Number.isFinite(earliest) ? new Date(earliest) : startOfUtcMonth(now)
|
|
202
204
|
const elapsedDays = Math.max(0, (now.getTime() - start.getTime()) / DAY_MS)
|
|
203
205
|
return {
|
|
204
206
|
start,
|
|
@@ -255,8 +257,11 @@ function formatCurrencyTotals(totals: Map<string, number>): { value: string, det
|
|
|
255
257
|
const entries = [...totals.entries()].sort((left, right) => right[1] - left[1])
|
|
256
258
|
if (entries.length === 0)
|
|
257
259
|
return { value: formatMoney(0, 'USD'), detail: 'No recorded revenue' }
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
// Destructured rather than indexed: a `length === 1` check does not narrow
|
|
261
|
+
// `entries[0]` for the compiler, and every index below would need a guard.
|
|
262
|
+
const [only] = entries
|
|
263
|
+
if (entries.length === 1 && only)
|
|
264
|
+
return { value: formatMoney(only[1], only[0]), detail: only[0] }
|
|
260
265
|
return {
|
|
261
266
|
value: `${entries.length} currencies`,
|
|
262
267
|
detail: entries.map(([code, amount]) => formatMoney(amount, code)).join(' | '),
|
|
@@ -269,10 +274,11 @@ function formatCurrencyAverages(totals: Map<string, { amount: number, orders: nu
|
|
|
269
274
|
.sort((left, right) => right[1].amount - left[1].amount)
|
|
270
275
|
if (entries.length === 0)
|
|
271
276
|
return { value: formatMoney(0, 'USD'), detail: 'No recorded orders' }
|
|
272
|
-
|
|
277
|
+
const [only] = entries
|
|
278
|
+
if (entries.length === 1 && only)
|
|
273
279
|
return {
|
|
274
|
-
value: formatMoney(
|
|
275
|
-
detail:
|
|
280
|
+
value: formatMoney(only[1].amount / only[1].orders, only[0]),
|
|
281
|
+
detail: only[0],
|
|
276
282
|
}
|
|
277
283
|
return {
|
|
278
284
|
value: 'Mixed currencies',
|
|
@@ -329,7 +335,10 @@ function buildBuckets(start: Date, end: Date, bucket: RangeWindow['bucket']): Bu
|
|
|
329
335
|
function singleCurrencyChange(current: Map<string, number>, previous: Map<string, number>): string {
|
|
330
336
|
if (current.size !== 1)
|
|
331
337
|
return ''
|
|
332
|
-
const [
|
|
338
|
+
const [entry] = [...current.entries()]
|
|
339
|
+
if (!entry)
|
|
340
|
+
return ''
|
|
341
|
+
const [code, amount] = entry
|
|
333
342
|
return percentChange(amount, previous.get(code) || 0)
|
|
334
343
|
}
|
|
335
344
|
|
|
@@ -409,12 +418,14 @@ export function buildCommerceDashboard(
|
|
|
409
418
|
const index = bucketIndexes.get(bucketKey(createdAt, window.bucket))
|
|
410
419
|
if (index === undefined)
|
|
411
420
|
continue
|
|
412
|
-
|
|
421
|
+
// `index` came out of a Map lookup, so the compiler treats these slots as
|
|
422
|
+
// possibly missing even though the arrays are pre-filled to bucket length.
|
|
423
|
+
orderSeries[index] = (orderSeries[index] ?? 0) + 1
|
|
413
424
|
if (isCancelled(order.status))
|
|
414
425
|
continue
|
|
415
426
|
const code = currency(order.currency)
|
|
416
427
|
const values = revenueSeries.get(code) || Array.from({ length: buckets.length }, () => 0)
|
|
417
|
-
values[index]
|
|
428
|
+
values[index] = (values[index] ?? 0) + order.totalAmount
|
|
418
429
|
revenueSeries.set(code, values)
|
|
419
430
|
}
|
|
420
431
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResponseStatus } from '@stacksjs/bun-router'
|
|
1
2
|
import { statfs } from 'node:fs/promises'
|
|
2
3
|
import { posix } from 'node:path'
|
|
3
4
|
import type { StorageAdapter, StorageManager, UploadedFileLike } from '@stacksjs/storage'
|
|
@@ -52,10 +53,10 @@ export interface DashboardFileSnapshot {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export class DashboardFileError extends Error {
|
|
55
|
-
readonly status:
|
|
56
|
+
readonly status: ResponseStatus
|
|
56
57
|
readonly fields?: Record<string, string>
|
|
57
58
|
|
|
58
|
-
constructor(message: string, status = 422, fields?: Record<string, string>) {
|
|
59
|
+
constructor(message: string, status: ResponseStatus = 422, fields?: Record<string, string>) {
|
|
59
60
|
super(message)
|
|
60
61
|
this.name = 'DashboardFileError'
|
|
61
62
|
this.status = status
|
|
@@ -142,7 +142,7 @@ export default new Action({
|
|
|
142
142
|
|
|
143
143
|
const sources = ['Users', 'Products', 'Orders', 'Posts', 'Revenue', 'Recent orders', 'Recent users', 'Request count', 'Recent requests']
|
|
144
144
|
const issues = modelResults
|
|
145
|
-
.map((result, index) => issue(sources[index], result))
|
|
145
|
+
.map((result, index) => issue(sources[index] ?? 'Dashboard', result))
|
|
146
146
|
.filter((entry): entry is { source: string, message: string } => entry !== null)
|
|
147
147
|
if (health.status === 'rejected') {
|
|
148
148
|
issues.push({
|
|
@@ -57,15 +57,18 @@ export default new Action({
|
|
|
57
57
|
const results = await Promise.allSettled(definitions.map(definition => definition.count()))
|
|
58
58
|
const stats = definitions.map((definition, index) => {
|
|
59
59
|
const { count: _count, ...metadata } = definition
|
|
60
|
-
|
|
60
|
+
// allSettled returns one result per input, but indexed access is typed
|
|
61
|
+
// as possibly missing; a rejected placeholder keeps the shape honest.
|
|
62
|
+
const result = results[index] ?? { status: 'rejected' as const, reason: new Error('missing result') }
|
|
63
|
+
return formatDashboardStat(metadata, result)
|
|
61
64
|
})
|
|
62
65
|
const issues = results.flatMap((result, index) => result.status === 'rejected'
|
|
63
66
|
? [{
|
|
64
|
-
source: definitions[index]
|
|
67
|
+
source: definitions[index]?.title ?? 'Unknown',
|
|
65
68
|
message: dashboardOperationalIssue(
|
|
66
69
|
result.reason,
|
|
67
|
-
`${definitions[index]
|
|
68
|
-
`DashboardStatsAction.${definitions[index]
|
|
70
|
+
`${definitions[index]?.title ?? 'Dashboard'} data could not be loaded.`,
|
|
71
|
+
`DashboardStatsAction.${(definitions[index]?.title ?? 'unknown').toLowerCase().replaceAll(' ', '-')}`,
|
|
69
72
|
),
|
|
70
73
|
}]
|
|
71
74
|
: [])
|
|
@@ -3,7 +3,7 @@ import { Action } from '@stacksjs/actions'
|
|
|
3
3
|
import { response } from '@stacksjs/router'
|
|
4
4
|
import { trackOperatorOperation } from '../Operations/control-plane'
|
|
5
5
|
import { stringValue } from '../Operations/recovery-input'
|
|
6
|
-
import {
|
|
6
|
+
import { DeploymentRollbackError, deploymentRollbackInput, executeDeploymentRollback, rollbackAuditPayload } from './deployment-rollback'
|
|
7
7
|
|
|
8
8
|
export default new Action({
|
|
9
9
|
name: 'CreateDeploymentRollback',
|
|
@@ -14,7 +14,7 @@ export default new Action({
|
|
|
14
14
|
const body = request.all() as Record<string, unknown>
|
|
15
15
|
try {
|
|
16
16
|
const input = deploymentRollbackInput(body)
|
|
17
|
-
const tracked = await trackOperatorOperation(request, 'dashboard.deployments.rollback', input, () => executeDeploymentRollback(input, stringValue(body.revision), stringValue(body.confirmation)))
|
|
17
|
+
const tracked = await trackOperatorOperation(request, 'dashboard.deployments.rollback', rollbackAuditPayload(input), () => executeDeploymentRollback(input, stringValue(body.revision), stringValue(body.confirmation)))
|
|
18
18
|
return { success: true, ...tracked.result, operation: tracked.operation }
|
|
19
19
|
}
|
|
20
20
|
catch (error) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import type { ReadableRecord } from '@stacksjs/orm'
|
|
1
2
|
export interface DeploymentCommandInput {
|
|
2
3
|
environment?: unknown
|
|
3
4
|
domain?: unknown
|
|
4
5
|
}
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
7
|
+
/** The shared shape, kept under this name for the helpers below. */
|
|
8
|
+
export type DeploymentRecordLike = ReadableRecord
|
|
9
9
|
|
|
10
10
|
export function booleanValue(value: unknown): boolean {
|
|
11
11
|
return value === true || value === 1 || value === '1' || value === 'true' || value === 'on'
|
|
@@ -6,6 +6,18 @@ export interface DeploymentRollbackInput {
|
|
|
6
6
|
release?: string
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* The same input as a value the operations log can store.
|
|
11
|
+
*
|
|
12
|
+
* `JsonValue` has no room for `undefined`, so the optional fields are dropped
|
|
13
|
+
* rather than recorded as absent-but-present keys.
|
|
14
|
+
*/
|
|
15
|
+
export function rollbackAuditPayload(input: DeploymentRollbackInput): Record<string, string> {
|
|
16
|
+
return Object.fromEntries(
|
|
17
|
+
Object.entries(input).filter((entry): entry is [string, string] => typeof entry[1] === 'string'),
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
9
21
|
export class DeploymentRollbackError extends Error {
|
|
10
22
|
constructor(message: string) {
|
|
11
23
|
super(message)
|
|
@@ -181,7 +181,7 @@ export default new Action({
|
|
|
181
181
|
averageDurationMs: finiteNumber(requestSummary.value?.average),
|
|
182
182
|
maximumDurationMs: finiteNumber(requestSummary.value?.maximum),
|
|
183
183
|
latestAt: requestSummary.value?.latest || '',
|
|
184
|
-
slowest: slowRequests.value.map(row => ({
|
|
184
|
+
slowest: slowRequests.value.map((row: Record<string, unknown>) => ({
|
|
185
185
|
id: countValue(row.id),
|
|
186
186
|
method: String(row.method || 'GET'),
|
|
187
187
|
path: safeRequestPath(row.path),
|
|
@@ -200,7 +200,7 @@ export default new Action({
|
|
|
200
200
|
averageDurationMs: finiteNumber(querySummary.value?.average),
|
|
201
201
|
maximumDurationMs: finiteNumber(querySummary.value?.maximum),
|
|
202
202
|
latestAt: querySummary.value?.latest || '',
|
|
203
|
-
slowest: slowQueries.value.map(row => ({
|
|
203
|
+
slowest: slowQueries.value.map((row: Record<string, unknown>) => ({
|
|
204
204
|
id: countValue(row.id),
|
|
205
205
|
query: compactSql(row.query),
|
|
206
206
|
durationMs: finiteNumber(row.duration),
|
|
@@ -240,7 +240,7 @@ export default new Action({
|
|
|
240
240
|
errors: {
|
|
241
241
|
total: countValue(errorSummary.value?.total),
|
|
242
242
|
latestAt: errorSummary.value?.latest || '',
|
|
243
|
-
recent: recentErrors.value.map(row => ({
|
|
243
|
+
recent: recentErrors.value.map((row: Record<string, unknown>) => ({
|
|
244
244
|
id: countValue(row.id),
|
|
245
245
|
type: String(row.type || 'Error'),
|
|
246
246
|
message: String(row.message || ''),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import type { ReadableRecord } from '@stacksjs/orm'
|
|
1
2
|
export type JobRecordSource = 'job' | 'failed'
|
|
2
3
|
export type DashboardJobStatus = 'queued' | 'processing' | 'failed'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
5
|
+
/** The shared shape, kept under this name for the helpers below. */
|
|
6
|
+
export type ModelRecord = ReadableRecord
|
|
7
7
|
|
|
8
8
|
export interface NormalizedJob {
|
|
9
9
|
id: string
|
|
@@ -36,7 +36,9 @@ export default new Action({
|
|
|
36
36
|
if (!name || name.length > 120) {
|
|
37
37
|
return kanbanError('Name is required and must be 1-120 characters.', 400)
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
// `undefined` rather than `null`: the model treats the column as optional,
|
|
40
|
+
// and "no description was sent" is not the same as "set it to null".
|
|
41
|
+
const description = typeof body.description === 'string' ? body.description.trim() : undefined
|
|
40
42
|
const icon = typeof body.icon === 'string' && body.icon ? body.icon : 'rectangle.stack.fill'
|
|
41
43
|
const color = typeof body.color === 'string' && body.color ? body.color : 'violet'
|
|
42
44
|
|
|
@@ -71,8 +73,8 @@ export default new Action({
|
|
|
71
73
|
position: nextPosition,
|
|
72
74
|
archived: false,
|
|
73
75
|
cardCount: 0,
|
|
74
|
-
createdAt: board.get('
|
|
75
|
-
updatedAt: board.get('
|
|
76
|
+
createdAt: board.get('created_at') ?? null,
|
|
77
|
+
updatedAt: board.get('updated_at') ?? null,
|
|
76
78
|
},
|
|
77
79
|
}
|
|
78
80
|
}
|
|
@@ -40,7 +40,9 @@ export default new Action({
|
|
|
40
40
|
if (!title || title.length > 300) {
|
|
41
41
|
return kanbanError('`title` is required and must be 1-300 characters.', 400)
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
// `undefined` rather than `null`: the model treats the column as optional,
|
|
44
|
+
// and "no description was sent" is not the same as "set it to null".
|
|
45
|
+
const description = typeof body.description === 'string' ? body.description.trim() : undefined
|
|
44
46
|
const dueDate = typeof body.dueDate === 'string' && body.dueDate ? body.dueDate : null
|
|
45
47
|
|
|
46
48
|
try {
|
|
@@ -84,8 +86,8 @@ export default new Action({
|
|
|
84
86
|
createdByUserId,
|
|
85
87
|
dueDate,
|
|
86
88
|
archived: false,
|
|
87
|
-
createdAt: card.get('
|
|
88
|
-
updatedAt: card.get('
|
|
89
|
+
createdAt: card.get('created_at') ?? null,
|
|
90
|
+
updatedAt: card.get('updated_at') ?? null,
|
|
89
91
|
},
|
|
90
92
|
}
|
|
91
93
|
}
|
|
@@ -99,7 +99,7 @@ export default new Action({
|
|
|
99
99
|
if (boardIds.size > 1) {
|
|
100
100
|
return kanbanError('All columns in a reorder request must belong to the same board.', 400)
|
|
101
101
|
}
|
|
102
|
-
const boardId = colRows[0]
|
|
102
|
+
const boardId = colRows[0]?.board_id
|
|
103
103
|
|
|
104
104
|
// Verify all cards belong to that board (prevents a malformed
|
|
105
105
|
// page from moving cards in from a sibling board).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { ReadableRecord } from '@stacksjs/orm'
|
|
2
|
+
/** The shared shape, kept under this name for the helpers below. */
|
|
3
|
+
export type KanbanModelRecord = ReadableRecord
|
|
4
4
|
|
|
5
5
|
export interface RefreshableKanbanModelRecord extends KanbanModelRecord {
|
|
6
6
|
fresh: () => Promise<KanbanModelRecord | null>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { ResponseStatus } from '@stacksjs/bun-router'
|
|
1
2
|
import { response } from '@stacksjs/router'
|
|
2
3
|
|
|
3
|
-
export function kanbanError(error: string, status:
|
|
4
|
+
export function kanbanError(error: string, status: ResponseStatus): Response {
|
|
4
5
|
return response.json({ error }, status)
|
|
5
6
|
}
|
|
6
7
|
|
|
@@ -44,6 +44,7 @@ function dependencySnapshot() {
|
|
|
44
44
|
|
|
45
45
|
for (const wsKey of Object.keys(workspaces)) {
|
|
46
46
|
const ws = workspaces[wsKey]
|
|
47
|
+
if (!ws) continue
|
|
47
48
|
const allDeps = { ...ws.dependencies, ...ws.devDependencies }
|
|
48
49
|
for (const depName of Object.keys(allDeps)) {
|
|
49
50
|
if (declaredSet[depName]) continue
|
|
@@ -12,13 +12,18 @@ export default new Action({
|
|
|
12
12
|
try {
|
|
13
13
|
const allReleases = await Release.orderByDesc('id').limit(500).get()
|
|
14
14
|
|
|
15
|
+
// Only columns the Release model actually declares. `released_at`,
|
|
16
|
+
// `changes`, `change_count` and `breaking` were read here but have never
|
|
17
|
+
// existed on the model, so they resolved to '' / 0 / false on every row
|
|
18
|
+
// - the same fabricated-metric problem 9882e7ede removed from the rest
|
|
19
|
+
// of this dashboard, left behind in this one action. Nothing consumed
|
|
20
|
+
// them.
|
|
15
21
|
const releases = allReleases.map(r => ({
|
|
16
22
|
version: String(r.get('version') || ''),
|
|
17
|
-
date: String(r.get('created_at') ||
|
|
23
|
+
date: String(r.get('created_at') || ''),
|
|
18
24
|
type: String(r.get('type') || 'patch'),
|
|
19
25
|
status: String(r.get('status') || 'previous'),
|
|
20
|
-
|
|
21
|
-
breaking: Boolean(r.get('breaking') || false),
|
|
26
|
+
notes: String(r.get('notes') || ''),
|
|
22
27
|
}))
|
|
23
28
|
|
|
24
29
|
return { releases }
|
|
@@ -28,9 +28,9 @@ export default new Action({
|
|
|
28
28
|
...data,
|
|
29
29
|
audience_size: 0,
|
|
30
30
|
sent_count: 0,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// A campaign that has not been sent has no rates yet. The columns are
|
|
32
|
+
// nullable in the database but the model types them as optional
|
|
33
|
+
// numbers, so leaving them out says "not measured" in both.
|
|
34
34
|
})
|
|
35
35
|
return response.json({ id: campaign.get('id') }, 201)
|
|
36
36
|
}
|
|
@@ -17,9 +17,16 @@ export default new Action({
|
|
|
17
17
|
if (validationError)
|
|
18
18
|
return response.json({ message: validationError }, 422)
|
|
19
19
|
|
|
20
|
+
// A social post belongs to a User, and the column is NOT NULL - so an
|
|
21
|
+
// author-less payload used to pass validation and then fail at the
|
|
22
|
+
// database as a 500. Say so plainly instead.
|
|
23
|
+
if (data.user_id === null)
|
|
24
|
+
return response.json({ message: 'A social post needs an author.' }, 422)
|
|
25
|
+
|
|
20
26
|
try {
|
|
21
27
|
const post = await SocialPost.create({
|
|
22
28
|
...data,
|
|
29
|
+
user_id: data.user_id,
|
|
23
30
|
platform: data.platform as Exclude<typeof data.platform, ''>,
|
|
24
31
|
likes: 0,
|
|
25
32
|
shares: 0,
|
|
@@ -25,8 +25,12 @@ export default new Action({
|
|
|
25
25
|
if (!post)
|
|
26
26
|
return response.json({ message: 'Social post not found.' }, 404)
|
|
27
27
|
|
|
28
|
+
// Same NOT NULL author column as on create; an update that did not
|
|
29
|
+
// name one leaves the existing author alone rather than clearing it.
|
|
30
|
+
const { user_id: authorId, ...rest } = data
|
|
28
31
|
await post.update({
|
|
29
|
-
...
|
|
32
|
+
...rest,
|
|
33
|
+
...(authorId === null ? {} : { user_id: authorId }),
|
|
30
34
|
platform: data.platform as Exclude<typeof data.platform, ''>,
|
|
31
35
|
})
|
|
32
36
|
return response.json({ id })
|
|
@@ -84,7 +84,7 @@ function walkModels(dir: string, recursive: boolean): Array<{ name: string, file
|
|
|
84
84
|
function categorize(model: { source: 'userland' | 'framework', file: string }): string {
|
|
85
85
|
if (model.source === 'userland') return 'userland'
|
|
86
86
|
const segs = model.file.split('/Models/')[1]?.split('/') ?? []
|
|
87
|
-
if (segs.length > 1) return segs[0].toLowerCase()
|
|
87
|
+
if (segs.length > 1 && segs[0]) return segs[0].toLowerCase()
|
|
88
88
|
return 'framework'
|
|
89
89
|
}
|
|
90
90
|
|
|
@@ -197,19 +197,22 @@ export default new Action({
|
|
|
197
197
|
const userlandCount = enriched.filter(m => m.source === 'userland').length
|
|
198
198
|
const unavailableModels = enriched.filter(m => m.error !== null).length
|
|
199
199
|
|
|
200
|
-
const groupMap
|
|
200
|
+
const groupMap = new Map<string, ModelRow[]>()
|
|
201
201
|
for (const m of enriched) {
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
const bucket = groupMap.get(m.category) ?? []
|
|
203
|
+
bucket.push(m)
|
|
204
|
+
groupMap.set(m.category, bucket)
|
|
204
205
|
}
|
|
205
206
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
// A Map rather than a plain object: every read here is by a key that was
|
|
208
|
+
// just written, and an index signature makes the compiler doubt all four.
|
|
209
|
+
const categoryGroups: ModelGroup[] = [...groupMap.entries()]
|
|
210
|
+
.sort(([a], [b]) => categoryOrder(a) - categoryOrder(b))
|
|
211
|
+
.map(([key, models]) => ({
|
|
209
212
|
key,
|
|
210
213
|
label: categoryLabel(key),
|
|
211
|
-
models
|
|
212
|
-
countLabel: `${
|
|
214
|
+
models,
|
|
215
|
+
countLabel: `${models.length} model${models.length === 1 ? '' : 's'}`,
|
|
213
216
|
}))
|
|
214
217
|
|
|
215
218
|
return {
|
|
@@ -2,9 +2,12 @@ import type { RequestInstance } from '@stacksjs/types'
|
|
|
2
2
|
import type {
|
|
3
3
|
ControlPlaneActor,
|
|
4
4
|
ControlPlaneOperation,
|
|
5
|
-
DashboardControlPlane,
|
|
6
5
|
JsonValue,
|
|
7
6
|
} from '@stacksjs/ts-cloud'
|
|
7
|
+
// Lives on the `/deploy` entry alongside `initializeDashboardControlPlane`,
|
|
8
|
+
// not on the package root - importing it from the root silently produced
|
|
9
|
+
// `any`, and every caller of this helper inherited that.
|
|
10
|
+
import type { DashboardControlPlane } from '@stacksjs/ts-cloud/deploy'
|
|
8
11
|
import process from 'node:process'
|
|
9
12
|
import { setStateDir } from '@stacksjs/ts-cloud'
|
|
10
13
|
import { initializeDashboardControlPlane } from '@stacksjs/ts-cloud/deploy'
|
|
@@ -48,7 +51,16 @@ export function operationsEnvironment(controlPlane = operationsControlPlane()) {
|
|
|
48
51
|
?? controlPlane.environments.values().next().value
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
/**
|
|
55
|
+
* `T` is only required to be serializable, not to BE a `JsonValue`.
|
|
56
|
+
*
|
|
57
|
+
* An interface without an index signature - `Alert`, `ReconcileResult` - never
|
|
58
|
+
* satisfies `{ [key: string]: JsonValue }` structurally, however plainly
|
|
59
|
+
* JSON-shaped its fields are. Constraining the result that way meant the
|
|
60
|
+
* honest callers were the ones that failed to typecheck, so the constraint is
|
|
61
|
+
* dropped here and the value is narrowed once, where it is actually recorded.
|
|
62
|
+
*/
|
|
63
|
+
export async function trackOperatorOperation<T>(
|
|
52
64
|
request: RequestInstance,
|
|
53
65
|
kind: string,
|
|
54
66
|
input: JsonValue,
|
|
@@ -83,7 +95,7 @@ export async function trackOperatorOperation<T extends JsonValue>(
|
|
|
83
95
|
operation = controlPlane.store.transitionOperation(operation.id, {
|
|
84
96
|
to: 'succeeded',
|
|
85
97
|
expectedVersion: operation.version,
|
|
86
|
-
output: result,
|
|
98
|
+
output: result as JsonValue,
|
|
87
99
|
})
|
|
88
100
|
controlPlane.store.appendEvent({
|
|
89
101
|
organizationId: controlPlane.organization.id,
|
|
@@ -92,7 +104,7 @@ export async function trackOperatorOperation<T extends JsonValue>(
|
|
|
92
104
|
actorId: actor.id,
|
|
93
105
|
correlationId: operation.correlationId,
|
|
94
106
|
type: `${kind}.succeeded`,
|
|
95
|
-
payload: result,
|
|
107
|
+
payload: result as JsonValue,
|
|
96
108
|
})
|
|
97
109
|
return { result, operation }
|
|
98
110
|
}
|
|
@@ -23,7 +23,7 @@ export default new Action({
|
|
|
23
23
|
return {
|
|
24
24
|
enabled: queryLogging?.enabled === true,
|
|
25
25
|
slowThreshold: queryLogging?.slowThreshold || 100,
|
|
26
|
-
queries: rows.map(row => mapDashboardQueryLog(row as QueryLogSourceRow)),
|
|
26
|
+
queries: rows.map((row: unknown) => mapDashboardQueryLog(row as QueryLogSourceRow)),
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
catch (error) {
|