@stacksjs/defaults 0.75.266 → 0.75.268

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.
@@ -508,6 +508,38 @@ History is chronological, with `has_more` and a `before` message-id cursor.
508
508
  Receipts acknowledge only messages through the id actually displayed.
509
509
  `MessagingError.status` provides safe HTTP statuses for application handlers.
510
510
 
511
+ ### Attachments
512
+
513
+ `send(id, body, retryKey, attachments?)` records files the application has
514
+ already stored. The trait keeps a reference, never bytes: `file` is the
515
+ application's own name for the upload (a disk path, a bucket key, an id), and
516
+ `type`, `size`, `width` and `height` are optional. Resolving and authorizing
517
+ the file stays with the application, so a disk, a bucket and a CDN look the
518
+ same from here.
519
+
520
+ ```ts
521
+ import type { DirectMessageAttachment } from '@stacksjs/chat'
522
+
523
+ const photo: DirectMessageAttachment = { file: upload.key, type: 'image/jpeg', size: upload.size, width: 1200, height: 900 }
524
+ await messenger.send(thread.id, '', retryKey, [photo])
525
+ ```
526
+
527
+ - A photo with no caption is a message: `body` may be empty when there is at
528
+ least one attachment, and only then.
529
+ - `message.attachments` is always an array, `[]` for text. Rows written before
530
+ the column existed read as `[]`.
531
+ - A retry key covers the attachments too. The same key with a different file
532
+ is a different message and returns 409, rather than silently keeping the
533
+ first photo.
534
+ - At most 10 attachments, 4000 characters once serialised. Keys the trait does
535
+ not know are dropped, so the column cannot become a second message body.
536
+ - The column is `chat_messages.attachments`, added by `buddy migrate` to a
537
+ database created before it. A database that has not migrated yet still sends
538
+ text.
539
+ - Notification previews are the application's to word: `onMessageSent`
540
+ receives the message with its attachments, so a photo with no caption can
541
+ read as "Sent a photo" rather than as an empty line.
542
+
511
543
  For the UI, STX ships `ChatInbox`, `ChatMessage`, and `ChatUnreadBadge` in
512
544
  `@stacksjs/components`. Browser stores import `createChat` from
513
545
  `@stacksjs/components/chat` and inject their authenticated HTTP transport.
@@ -18,7 +18,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
18
18
  | `@stacksjs/api` | [`stacks-api`](../stacks-api/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/api/src/index.ts) | 9 |
19
19
  | `@stacksjs/arrays` | [`stacks-arrays`](../stacks-arrays/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/arrays/src/index.ts) | 6 |
20
20
  | `@stacksjs/audio` | [`stacks-audio`](../stacks-audio/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/audio/src/index.ts) | 2 |
21
- | `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 119 |
21
+ | `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 120 |
22
22
  | `@stacksjs/browser` | [`stacks-browser`](../stacks-browser/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser/src/index.ts) | 8 |
23
23
  | `@stacksjs/browser-extension` | [`stacks-browser-extension`](../stacks-browser-extension/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser-extension/src/index.ts) | 7 |
24
24
  | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 164 |
@@ -65,7 +65,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
65
65
  | `@stacksjs/newsletter` | [`stacks-newsletter`](../stacks-newsletter/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/newsletter/src/index.ts) | 7 |
66
66
  | `@stacksjs/notifications` | [`stacks-notifications`](../stacks-notifications/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/index.ts) | 16 |
67
67
  | `@stacksjs/objects` | [`stacks-objects`](../stacks-objects/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/objects/src/index.ts) | 4 |
68
- | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 71 |
68
+ | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 72 |
69
69
  | `@stacksjs/pagination` | [`stacks-pagination`](../stacks-pagination/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/pagination/src/index.ts) | 0 |
70
70
  | `@stacksjs/path` | [`stacks-path`](../stacks-path/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/src/index.ts) | 13 |
71
71
  | `@stacksjs/payments` | [`stacks-payments`](../stacks-payments/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/index.ts) | 28 |
@@ -185,7 +185,7 @@ Declared runtime exports: `DEFAULT_ROLE_PACKS`, `createBqbRbacStore`, `generateT
185
185
 
186
186
  Local source modules: [src/authentication.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authentication.ts), [src/authenticator.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authenticator.ts), [src/authorizable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/authorizable.ts), [src/browser-session.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/browser-session.ts), [src/client.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/client.ts), [src/cookie-auth.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/cookie-auth.ts), [src/credential-user.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/credential-user.ts), [src/email-verification.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/email-verification.ts), [src/gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/gate.ts), [src/magic-link.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/magic-link.ts), [src/middleware.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/middleware.ts), [src/oauth-authorization-codes.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-codes.ts), [src/oauth-authorization-endpoint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-endpoint.ts), [src/oauth-authorization-requests.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-requests.ts), [src/oauth-authorization-response.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization-response.ts), [src/oauth-authorization.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-authorization.ts), [src/oauth-browser-session.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-browser-session.ts), [src/oauth-client-registration.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-client-registration.ts), [src/oauth-consent.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-consent.ts), [src/oauth-delegated-access.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-delegated-access.ts), [src/oauth-dynamic-registration.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-dynamic-registration.ts), [src/oauth-grants.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-grants.ts), [src/oauth-introspection.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-introspection.ts), [src/oauth-metadata.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-metadata.ts), [src/oauth-pkce.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-pkce.ts), [src/oauth-protected-resource.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-protected-resource.ts), [src/oauth-provider.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-provider.ts), [src/oauth-pruning.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-pruning.ts), [src/oauth-revocation.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-revocation.ts), [src/oauth-server.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-server.ts), [src/oauth-token-endpoint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-endpoint.ts), [src/oauth-token-exchange.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-exchange.ts), [src/oauth-token-request.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-request.ts), [src/oauth-token-response.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/oauth-token-response.ts), [src/page-gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/page-gate.ts), [src/passkey.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/passkey.ts), [src/passkeys.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/passkeys.ts), [src/password/reset.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/password/reset.ts), [src/permissions.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/permissions.ts), [src/policy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/policy.ts), [src/rate-limiter.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rate-limiter.ts), [src/rbac-seed.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac-seed.ts), [src/rbac-store-bqb.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac-store-bqb.ts), [src/rbac.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/rbac.ts), [src/referrals.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/referrals.ts), [src/register.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/register.ts), [src/request-token.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/request-token.ts), [src/session-auth.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/session-auth.ts), [src/session-handoff.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/session-handoff.ts), [src/socials.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/socials.ts), [src/team.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/team.ts), [src/tokens.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/tokens.ts), [src/two-factor.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/two-factor.ts), [src/user.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/user.ts).
187
187
 
188
- Evidence directory: [119 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
188
+ Evidence directory: [120 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
189
189
 
190
190
  ### @stacksjs/browser
191
191
 
@@ -633,7 +633,7 @@ Declared runtime exports: `Activity`, `AnalyticsEvent`, `Auction`, `AuctionItem`
633
633
 
634
634
  Local source modules: [src/auto-crud.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/auto-crud.ts), [src/batch-loader.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/batch-loader.ts), [src/db.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/db.ts), [src/define-model.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/define-model.ts), [src/extend-model.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/extend-model.ts), [src/gdpr/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/gdpr/index.ts), [src/generate-database-schema.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/generate-database-schema.ts), [src/locked-row.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/locked-row.ts), [src/messaging.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/messaging.ts), [src/model-types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/model-types.ts), [src/ownership.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/ownership.ts), [src/paginator-request.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/paginator-request.ts), [src/row-token.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/row-token.ts), [src/subquery.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/subquery.ts), [src/traits/audit.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/audit.ts), [src/traits/billable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/billable.ts), [src/traits/categorizable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/categorizable.ts), [src/traits/commentable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/commentable.ts), [src/traits/likeable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/likeable.ts), [src/traits/taggable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/taggable.ts), [src/transaction.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/transaction.ts), [src/types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/types.ts), [src/utils/encrypted.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/utils/encrypted.ts), [src/utils/prunable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/utils/prunable.ts).
635
635
 
636
- Evidence directory: [71 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
636
+ Evidence directory: [72 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
637
637
 
638
638
  ### @stacksjs/pagination
639
639
 
@@ -162,9 +162,17 @@ export default defineModel({
162
162
  */
163
163
 
164
164
  /** Legacy owner column, written as a copy of `tokenable_id`. */
165
+ /**
166
+ * The legacy owner column, a copy of `tokenableId` kept for installs that
167
+ * predate the pair. `ensureAuthTables()` gives it no constraint, since a
168
+ * token can belong to an author as well as a user, so it is not a foreign
169
+ * key. Without saying so, convention inferred `users.id` from the name and
170
+ * a fresh app's first `buddy doctor` failed on it.
171
+ */
165
172
  userId: {
166
173
  order: 10,
167
174
  guarded: true,
175
+ foreignKey: false,
168
176
  validation: {
169
177
  rule: schema.number(),
170
178
  },
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+
3
+ /**
4
+ * Run `parseDashboardDate` in a process pinned to `zone`, since a calendar
5
+ * date only goes wrong west of UTC and a test process's zone is fixed at
6
+ * start-up.
7
+ */
8
+ async function inZone(zone: string, expression: string): Promise<string> {
9
+ const child = Bun.spawn([process.execPath, '--no-env-file', '-e', `
10
+ const { parseDashboardDate } = await import(${JSON.stringify(`${import.meta.dir}/dashboard-dates.ts`)})
11
+ console.log(${expression})
12
+ `], { env: { ...process.env, TZ: zone }, stdout: 'pipe', stderr: 'pipe' })
13
+ const [code, out, err] = await Promise.all([child.exited, new Response(child.stdout).text(), new Response(child.stderr).text()])
14
+ expect(code, err).toBe(0)
15
+ return out.trim()
16
+ }
17
+
18
+ const calendarDay = `[parseDashboardDate('2026-10-01')].map(d => [d.getFullYear(), d.getMonth() + 1, d.getDate()].join('-'))[0]`
19
+
20
+ describe('parseDashboardDate', () => {
21
+ for (const zone of ['America/Los_Angeles', 'UTC', 'Asia/Manila', 'Pacific/Kiritimati']) {
22
+ it(`reads a calendar date as that day in ${zone}`, async () => {
23
+ expect(await inZone(zone, calendarDay)).toBe('2026-10-1')
24
+ })
25
+ }
26
+
27
+ it('reads an offsetless database timestamp as UTC, wherever it runs', async () => {
28
+ for (const zone of ['America/Los_Angeles', 'Asia/Manila'])
29
+ expect(await inZone(zone, `parseDashboardDate('2026-10-01 14:30:00').toISOString()`)).toBe('2026-10-01T14:30:00.000Z')
30
+ })
31
+
32
+ it('leaves an ISO string with an offset, and a Date, as they are', async () => {
33
+ expect(await inZone('America/Los_Angeles', `parseDashboardDate('2026-10-01T14:30:00+08:00').toISOString()`)).toBe('2026-10-01T06:30:00.000Z')
34
+ const at = new Date('2026-10-01T00:00:00Z')
35
+ const { parseDashboardDate } = await import('./dashboard-dates')
36
+ expect(parseDashboardDate(at)).toBe(at)
37
+ })
38
+
39
+ it('the reading it replaces is the bug: a calendar date lands on the day before west of UTC', async () => {
40
+ // The control. Without it, every assertion above would pass in a zone
41
+ // where the two readings agree.
42
+ expect(await inZone('America/Los_Angeles', `[new Date('2026-10-01')].map(d => d.getDate())[0]`)).toBe('30')
43
+ })
44
+ })
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Read a date value from a dashboard row.
3
+ *
4
+ * A calendar date (`2026-10-01`) is a day, not an instant, so it is read as
5
+ * local midnight. `new Date('2026-10-01')` reads it as UTC midnight instead,
6
+ * which every zone west of UTC renders as the day before: a coupon starting
7
+ * on October 1st showed September 30th in Los Angeles.
8
+ *
9
+ * A database timestamp written without an offset (`2026-10-01 14:30:00`) is
10
+ * UTC, which is how the framework writes it, so it gains the `Z` it lacks.
11
+ * Anything else, ISO strings with an offset among them, is left to `Date`.
12
+ */
13
+ export function parseDashboardDate(value: unknown): Date {
14
+ if (value instanceof Date)
15
+ return value
16
+ const text = String(value)
17
+ if (/^\d{4}-\d{2}-\d{2}$/.test(text))
18
+ return new Date(`${text}T00:00:00`)
19
+ if (/^\d{4}-\d{2}-\d{2} \d/.test(text))
20
+ return new Date(`${text.replace(' ', 'T')}Z`)
21
+ return new Date(text)
22
+ }
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.266",
5
+ "version": "0.75.268",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.266",
5
+ "version": "0.75.268",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -45,9 +45,9 @@
45
45
  "dependencies": {
46
46
  "@iconify-json/f7": "^1.2.2",
47
47
  "@iconify-json/hugeicons": "^1.2.35",
48
- "@stacksjs/mobile": "0.75.266",
49
- "@stacksjs/sanitizer": "0.2.663",
50
- "@stacksjs/stx": "0.2.663",
48
+ "@stacksjs/mobile": "0.75.268",
49
+ "@stacksjs/sanitizer": "0.2.667",
50
+ "@stacksjs/stx": "0.2.667",
51
51
  "@xterm/addon-fit": "^0.11.0",
52
52
  "@xterm/xterm": "^6.0.0",
53
53
  "ts-qr-codes": "^0.1.8"
@@ -1,5 +1,6 @@
1
1
  <script client>
2
2
  import { formatCurrency } from '@stacksjs/commerce/money'
3
+ import { parseDashboardDate } from '../../../../functions/dashboard-dates'
3
4
  /**
4
5
  * The data table.
5
6
  *
@@ -276,14 +277,14 @@ function displayValue(row: DashboardTableRow, column: DashboardTableColumn): str
276
277
  return Number.isFinite(amount) ? amount.toLocaleString() : String(value)
277
278
  }
278
279
  if (column.type === 'date') {
279
- const at = toDate(value)
280
+ const at = parseDashboardDate(value)
280
281
  return Number.isNaN(at.getTime()) ? String(value) : at.toLocaleDateString()
281
282
  }
282
283
  // An event or log row is ordered by when it happened, and to the minute, so
283
284
  // the date alone is not enough to read it. Separate from `date` rather than
284
285
  // a flag on it because the two render differently everywhere they appear.
285
286
  if (column.type === 'datetime') {
286
- const at = toDate(value)
287
+ const at = parseDashboardDate(value)
287
288
  return Number.isNaN(at.getTime())
288
289
  ? String(value)
289
290
  : at.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })
@@ -346,20 +347,6 @@ function hasBadge(row: DashboardTableRow, column: DashboardTableColumn): boolean
346
347
  return badgeLabel(row, column) !== ''
347
348
  }
348
349
 
349
- /**
350
- * Parse a stored timestamp.
351
- *
352
- * SQLite hands back `2026-01-01 12:00:00`, with a space and no zone. `new
353
- * Date()` reads that as local time where it reads it at all, so every row
354
- * shifted by the viewer's offset - a date near midnight landing on the wrong
355
- * day. The hand-rolled tables each normalised it themselves; this is the same
356
- * knowledge in the one place that needs it.
357
- */
358
- function toDate(value: unknown): Date {
359
- const text = String(value)
360
- return new Date(/^\d{4}-\d{2}-\d{2} \d/.test(text) ? `${text.replace(' ', 'T')}Z` : text)
361
- }
362
-
363
350
  /** The pill's text: the mapped label, else the value with its underscores read as spaces. */
364
351
  function badgeLabel(row: DashboardTableRow, column: DashboardTableColumn): string {
365
352
  const value = rawValue(row, column.key)
@@ -397,7 +384,7 @@ function compare(a: any, b: any, type: DashboardTableColumn['type']): number {
397
384
  // comparison below, which is only accidentally right for ISO timestamps and
398
385
  // wrong for every other format.
399
386
  if (type === 'date' || type === 'datetime')
400
- return toDate(a).getTime() - toDate(b).getTime()
387
+ return parseDashboardDate(a).getTime() - parseDashboardDate(b).getTime()
401
388
  if (type === 'boolean')
402
389
  return (a ? 1 : 0) - (b ? 1 : 0)
403
390