Commit Graph
60 Commits
Author SHA1 Message Date
saltbo 387c731b3b feat(api-keys): unify user ownership and workspace scopes 2026-07-23 01:41:41 -04:00
saltbo eea3dafefa fix(upload): sync stored content type after upload 2026-07-23 00:43:49 -04:00
saltbo 7c3738f950 fix(downloads): preserve owner uid for api key uploads 2026-07-23 00:32:34 -04:00
saltbo 23f679d18c feat(webdav): add admin service controls 2026-07-22 23:25:54 -04:00
saltbo df0c5f3fbd feat(auth): add configurable email verification 2026-07-22 10:15:29 -04:00
saltbo ff78a33464 fix(audit): enforce HTTP usecase boundary
Move transfer audit persistence and redirect audit target resolution behind usecase functions so HTTP handlers pass dependencies whole and satisfy the architectural boundary.
2026-07-21 23:09:39 -04:00
saltbo e55ee53496 refactor(stats): unify audit and fact pipelines
Centralize request audit recording, preserve immutable download-task history, and derive hourly statistics and backfills from the same authoritative sources. Add durable user registration facts so admin deletion no longer destroys signup history.
2026-07-21 23:02:52 -04:00
saltbo 14abbf2648 fix(admin): use live overview metrics 2026-07-21 12:39:12 -04:00
saltbo 40c5f10d48 feat(admin): show storage change trend 2026-07-21 12:04:42 -04:00
saltbo b6cbc614b8 fix(admin): enforce exact analytics data sources 2026-07-21 02:38:54 -04:00
saltbo 3abf4f231e fix(storage): preserve purge history and track usage deltas
Retain purged matter and image metadata while hiding tombstones from live reads. Record exact storage deltas and build hourly trends only after the opening balance.
2026-07-21 00:29:27 -04:00
saltbo d9da5094bb feat(admin): add live dashboard overview 2026-07-20 22:35:12 -04:00
saltbo e761b6aec7 feat(webdav): verify derived domains before publishing 2026-07-20 18:54:12 -04:00
saltbo 7fe16f3ed7 refactor(site): replace options with structured config APIs 2026-07-20 15:17:48 -04:00
saltbo 75d7b50538 feat(webdav): support optional custom domains
Serve canonical root-relative DAV URLs on a configured hostname while preserving the existing /dav mount. Reconcile Cloudflare rewrite rules and Worker custom domains during fork deployments, and expose the effective URL through site options.
2026-07-20 12:59:58 -04:00
Jasper Van 816f584e48 fix(stats): reject ranges without closed buckets (#512) 2026-07-20 11:15:42 -04:00
Jasper Van c85e60f200 fix(analytics): enforce trustworthy offline statistics (#510)
* fix(analytics): enforce trustworthy offline statistics

Separate immutable counters from point-in-time snapshots, expose incomplete coverage instead of synthetic zeroes, and keep browser analytics result-only.

Restore finite Free quota baselines, fail closed for invalid storage quota, reconcile traffic reports fairly, and add production-safe backfill and data-quality diagnostics.

* fix(analytics): preserve global backfill totals

Group generated hourly backfill rows by their projected values so SQLite cannot resolve output aliases to source organization columns and overwrite cross-organization totals.
2026-07-20 10:23:22 -04:00
agent-kanban[bot]andRavi Shah f0292cc257 fix(events): allow org API keys to stream download tasks (#509)
* fix(events): authorize org API key task streams

Agent-Profile: https://agent-kanban.dev/agents/e0a1ce35687e48ef

* docs(events): complete verification checklist

Agent-Profile: https://agent-kanban.dev/agents/e0a1ce35687e48ef

---------

Co-authored-by: Ravi Shah <ravi-shah@mails.agent-kanban.dev>
2026-07-19 01:09:12 -04:00
saltbo 60f8f64481 refactor(analytics): enforce offline result boundaries
Build versioned UTC hourly rollups, validate analytics facts, and keep dashboard requests result-only with explicit coverage semantics.

Move derived metrics to the server and simplify the admin dashboard around one reconcilable UTC range without export.
2026-07-18 01:58:34 -04:00
Jasper Van 882f38e631 fix(downloads): harden active target folders (#507) 2026-07-17 23:44:17 -04:00
Jasper Van 15189c4107 fix(downloads): protect remote download target folders (#506)
* fix(downloads): protect remote download target folders

* test(downloads): cover target folder races
2026-07-17 23:18:22 -04:00
Jasper Van 8bd1e55b5c refactor(analytics): replace daily stats with hourly rollups (#505)
* refactor(analytics): replace daily stats with hourly rollups

* fix(analytics): merge hourly inventory sources

* test(analytics): cover rollup failure boundaries

* test(analytics): verify growth dashboard metrics

* test(analytics): cover historical rollup reads

* test(analytics): verify hourly dashboard dimensions

* test(analytics): cover remaining metric boundaries

* test(analytics): cover backfill SQL parsing
2026-07-10 13:26:27 -04:00
Jasper Van 8a8fa3e9e1 fix: repair admin statistics integrity (#502)
* fix: repair admin statistics integrity

* test: add PR 502 preview evidence

* chore: remove preview evidence artifacts
2026-07-10 01:28:23 -04:00
saltbo b540956fb8 fix(admin): keep stats queries read-only 2026-07-09 23:34:03 -04:00
saltbo ab81f138bd fix(admin): harden dashboard stats ranges 2026-07-09 23:34:03 -04:00
saltbo b7f7fa7ecd feat(admin): implement operations dashboard stats 2026-07-09 23:34:03 -04:00
saltbo 554c231536 feat(admin): redesign dashboard with pro analytics 2026-07-09 23:34:03 -04:00
saltbo 77aa770ed5 fix(workspaces): standardize workspace names and slugs
Use metadata-aware personal workspace detection across org repositories and UI, generate personal/team slugs with the new default random rules, and remove user-managed team slug fields.

Encode WebDAV path segments consistently so Finder follows workspace names such as Ambor's Space via %27 instead of XML-escaped apostrophes.
2026-07-09 01:00:03 -04:00
agent-kanban[bot]andNoah Reed 7ccaba2f8b feat: refine admin audit filtering (#495)
* feat: refine admin audit filtering

Agent-Profile: https://agent-kanban.dev/agents/b0abe6cd7aeba133

* test: add audit filter spec scenarios

Agent-Profile: https://agent-kanban.dev/agents/b0abe6cd7aeba133

* chore: refresh openapi client

Agent-Profile: https://agent-kanban.dev/agents/b0abe6cd7aeba133

---------

Co-authored-by: Noah Reed <noah-reed@mails.agent-kanban.dev>
2026-07-04 10:21:45 -04:00
saltbo 3695c80c2e feat(downloads): add task event timeline 2026-06-29 21:07:00 -04:00
saltbo cf7b1de112 fix(downloader): consolidate polling into heartbeat 2026-06-27 01:16:12 -04:00
saltbo e55dae3d2f refactor(admin): standardize management forms 2026-06-24 14:50:08 -04:00
saltbo fb2281f1b3 fix(downloader): clean local data only on task delete 2026-06-24 11:12:31 -04:00
agent-kanban-local[bot]andJordan Park f41ed27bba [codex] separate billing configuration (#479)
* feat(admin): separate billing configuration

Add dedicated storage egress and downloader credit billing contracts, usecases, RPC wrappers, drawers, generated client updates, and coverage.

Agent-Profile: https://agent-kanban.dev/agents/2673e70e0085f4e0

* fix(billing): preserve not found ordering

Check storage and downloader existence before quota_store gating in dedicated billing usecases, and cover enabled missing-resource requests at usecase and route levels.

Agent-Profile: https://agent-kanban.dev/agents/2673e70e0085f4e0

---------

Co-authored-by: Jordan Park <jordan-park@mails.agent-kanban.dev>
2026-06-24 05:57:10 -04:00
agent-kanban-local[bot]andJordan Park 82c5452782 feat(auth): move OAuth provider editor to drawer (#480)
Agent-Profile: https://agent-kanban.dev/agents/57ed5bcf43079e29

Co-authored-by: Jordan Park <jordan-park@mails.agent-kanban.dev>
2026-06-24 05:55:26 -04:00
agent-kanban-local[bot]andJordan Park c7f3d11793 [codex] Add admin storage connection testing (#475)
* feat(storage): add admin connection testing

Agent-Profile: https://agent-kanban.dev/agents/2673e70e0085f4e0

* fix: correct storage CORS guidance

Agent-Profile: https://agent-kanban.dev/agents/2673e70e0085f4e0

---------

Co-authored-by: Jordan Park <jordan-park@mails.agent-kanban.dev>
2026-06-24 00:37:59 -04:00
f4b65e4987 feat: make forcePathStyle configurable per storage (#474)
* feat: make forcePathStyle configurable per storage

Previously hardcoded to true, which breaks S3-compatible backends that require
virtual-hosted-style addressing (e.g. Alibaba Cloud OSS). Now configurable via
admin storage settings with a toggle switch, defaulting to true for backwards
compatibility.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test: cover storage force path style

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: saltbo <saltbo@foxmail.com>
2026-06-23 20:46:33 -04:00
saltboandClaude Opus 4.8 407e4ac803 refactor(s3): drop dead getPublicUrl gateway method
No callers remain after select('public') was removed in #456 — the
public-bucket flow is gone, so the gateway method, its port declaration
and its tests are dead code.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 10:53:30 -04:00
agent-kanban-local[bot]andAlex Chen 00f48cf355 feat(avatars): host avatars + team logos on Cloud via SDK 2.4.0; remove public-bucket mode (#467)
* feat(avatars): host avatars + team logos on Cloud via SDK 2.4.0; remove public-bucket mode

Host user avatars and org logos on the ZPan Cloud avatar service
(zpan-cloud-sdk ^2.4.0) instead of a public S3/R2 bucket, then remove the
now-dead storages.mode / public-bucket concept entirely (#456 parts 2-3).

- image-upload gateway: upload/delete via SDK uploadAvatar/deleteAvatar against
  a bound Cloud client; validate mime (AVATAR_CONTENT_TYPES) + size
  (MAX_AVATAR_BYTES) before the call; map cloud error codes to 400/403/413/500;
  unbound instance returns 503 cloud_required (delete is a best-effort no-op).
- licensing-cloud: createAvatarUploadClient builds the client with a plain-object
  bearer header so both the image content-type and Authorization survive hono's
  per-request header merge (a Headers instance would be dropped).
- drop storages.mode (migration via drizzle-kit), StorageRepo.select() no longer
  takes a mode, remove StorageMode / Storage.mode / mode schema+audit+UI+i18n and
  the PUBLIC_IMAGES bucket + PUBLIC_IMAGES_URL wiring.

Agent-Profile: https://agent-kanban.dev/agents/f759c704c282d88a

* ci(deploy): drop dead PUBLIC_IMAGES R2 provisioning from CF deploy

The Cloud avatar migration removed the PUBLIC_IMAGES binding from
wrangler.toml, so the deploy workflow's R2 public-images steps are dead and
must go too — otherwise every CF deploy keeps re-provisioning a public-read
zpan-public-images bucket (the footgun #456 eliminates) and sets an unused
PUBLIC_IMAGES_URL secret. Removes the bucket-create, managed-public-URL, and
secret steps (steps.r2 was only consumed by the secret step). Also drops a
stale storage-modes line from the v2.0 roadmap.

Agent-Profile: https://agent-kanban.dev/agents/f759c704c282d88a

---------

Co-authored-by: Alex Chen <alex-chen@mails.agent-kanban.dev>
2026-06-20 00:16:07 -04:00
saltbo 6e2cb47b72 fix(downloads): preserve transfer progress when a runtime report omits it
Download/upload totals were getting wiped on completed tasks. Two causes, both
mine:

1. nextTaskRuntime treated an incoming runtime as a full snapshot and REPLACED
   the stored one. reportSeedingStopped sends only {phase, seeding} (no
   progress), so the cumulative download/upload progress was erased — a completed
   task ended up as {phase: completed, seeding: {active: false}} with no
   transfer record. Progress is cumulative, not a per-report snapshot: carry it
   forward when a runtime report omits it (progress patches still apply on top).
2. clearStaleSeedingRuntime nulled the entire runtime to drop the seeding phase,
   which also erased progress. Now it surgically edits the JSON (phase ->
   completed, remove the seeding object) and keeps progress + file list.

Confirmed on prod: affected tasks had runtime collapsed to just engine/phase/
seeding with progress gone. Tests updated to assert a phase-only report
preserves progress.
2026-06-19 17:37:08 -04:00
saltbo 9afa8f1bb6 fix(downloads): make a blocked remote-download unit retryable so credit top-ups recover
reportRemoteDownloadUnit short-circuited on a locally-cached 'blocked' usage
record and threw without re-contacting the cloud. Once a unit was blocked, the
task was wedged in 'suspended' permanently — recharging credits had no effect,
because the next charge attempt never re-asked the cloud (confirmed on prod: the
stuck task's unit 1 was status='blocked').

Drop the short-circuit so a previously-blocked unit re-syncs with the cloud on
the next attempt; if credits are now available it's accepted and the task leaves
suspended. A 'reported' (already-charged) unit still short-circuits, so no
double-charge.

Test: a unit blocked on first attempt is accepted on retry after a top-up, and
the task transitions back to downloading.
2026-06-19 14:40:05 -04:00
saltbo 8278b9bb71 fix(downloads): clear stale seeding by live-downloader set (covers deleted owners)
The first cut keyed off listUnreachableIds (existing downloaders with a stale
heartbeat), which misses a task whose downloader was DELETED — its id is gone
from the table, so it never appears in any stale list. Confirmed on prod: the
lone stuck task was owned by a downloader id absent from the downloaders table.

clearStaleSeedingRuntime now drops the stale seeding runtime on any completed
task NOT owned by a live (recently-heartbeating) downloader — offline, deleted,
or unassigned — and runs every sweep regardless of new staleness. A live
downloader's genuine seed is preserved.

Test now asserts both: a deleted owner's seed is cleared, a live owner's is kept.
2026-06-19 14:17:18 -04:00
saltboandClaude Opus 4.8 1b37bb8dd0 fix(downloads): clear stale seeding runtime on unreachable downloaders
A completed task keeps runtime.phase='seeding' while its downloader seeds. If
that downloader goes offline without reporting the seed stopped, the task shows
'Phase: Seeding' forever — the clearStaleSeedingReports one-shot only covers the
current downloader's own (assignedTo=me) tasks, not another offline downloader's.

Stale recovery now also drops the stale seeding runtime on completed tasks of
unreachable downloaders (alongside the canceling/pausing settle), so they stop
showing as seeding. Runs every sweep, idempotent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:07:31 -04:00
saltboandClaude Opus 4.8 e6709f5728 fix(downloads): pre-authorize remote-download credits, gate before download, surface suspend reason
Billing was charge-in-arrears: a credit unit was only charged once the
downloader had already reported downloading into it, and the first unit only
after the first progress report — so a no-credit task still pulled bytes before
being blocked, then suspended with no explanation.

- Pre-authorize one unit ahead of the bytes pulled: targetUnits =
  min(ceil(downloaded/unit) + 1, ceil(total/unit)). The downloader never fetches
  bytes it hasn't paid for, and the cap keeps the lifetime charge at exactly
  ceil(total/unit) — same total as before, only billed earlier.
- Charge the first unit on the transition into 'downloading' (zero bytes), so a
  task that can't afford a unit is suspended at the gate and pulls nothing. The
  worker reads the authoritative status from that transition response and does
  not start downloading when it comes back suspended (progress reports stay pure
  telemetry; control still flows through the poll).
- On suspend, set runtime.message so the UI shows why (insufficient credits).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 13:33:38 -04:00
Jasper VanandClaude Opus 4.8 a020cf74f0 refactor(branding): store logo + favicon as base64 data URIs (drop public bucket) (#466)
Branding's logo/favicon are now encoded as `data:${mime};base64,…` URIs and
stored directly in the `branding_logo_url` / `branding_favicon_url` system
options instead of being uploaded to a `mode='public'` S3 bucket. This removes
branding's dependency on public storage entirely (#456 Part 1).

- uploadBrandingImage encodes the raw file bytes and drops select('public') /
  s3.putObject / s3.getPublicUrl; s3 + storages removed from BrandingDeps.
- Per-field raw-byte caps replace the single 2 MiB limit: logo ≤ 256 KB,
  favicon ≤ 64 KB; over-cap returns 413 naming the field's limit.
- The 503 "no public storage" path is gone: uploads succeed with no public
  storage configured, and the updateBranding route no longer advertises 503
  (operationId unchanged; Go OpenAPI client regenerated).
- GET shape unchanged; legacy absolute-URL values keep rendering as-is (no
  migration, no backfill, old _system/branding objects untouched).

Out of scope (#456 Parts 2-3): avatar/team-logo upload, storages.mode,
StorageRepo.select.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 13:15:20 -04:00
saltbo 75761b3764 fix(downloads): settle control tasks for already-offline downloaders too
The previous fix gated control-task settling on listStaleIds, which only
returns downloaders that are still status='online' with a stale heartbeat — the
online→offline transition. A downloader marked offline by an earlier sweep is
never returned again, so a canceling/pausing task it held stayed stuck forever
(exactly the observed case). The early 'no new stale downloaders' return made it
worse.

Add listUnreachableIds (heartbeat past the lease, any status) and settle
canceling→canceled / pausing→paused for those every sweep (idempotent),
independent of the requeue+markOffline transition path.

Regression test now flips the downloader to status='offline' before recovery —
the case the prior fix missed.
2026-06-19 12:39:59 -04:00
Jasper VanandClaude Opus 4.8 875452032a fix(downloads): settle stale downloaders' canceling/pausing tasks (#465)
When a downloader went offline holding a 'canceling' (or 'pausing') task, the
stale-lease recovery only requeued [assigned,downloading,uploading,interrupted]
— so the control task was never resolved and sat in 'canceling' forever, since
no live downloader would ever ack it.

recoverStaleDownloaderAssignments now also settles control states for stale
downloaders: canceling→canceled (terminal, assignment cleared) and
pausing→paused (resumable). Adds the resolveControlAssignedToMany repo method.

Integration test: a stale downloader's canceling task settles to canceled once
recovery runs.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 12:01:29 -04:00
Jasper VanandClaude Opus 4.8 63d5b45e0e refactor(api)!: collapse polymorphic GET responses to one monomorphic schema (#449) (#455)
Every endpoint now exposes one monomorphic schema: role/state changes field
values (mask / null / filter), never the shape.

image-hosting/config: drop the `full config | { enabled: false }` union. GET
always returns the full ImageHostingConfig shape carrying `enabled`; not-configured
→ `enabled: false` with every other field null (`createdAt` is now nullable).
`buildResponse` is made total over `row | null` so it is the single producer of
the shape, and `getIhostConfig` no longer returns `| null`.

auth-providers: collapse the admin-config vs public-display union into one
AuthProvider schema (providerId, type, enabled, name, icon, clientId, discoveryUrl,
scopes, clientSecret). Same endpoint, no path split — role changes one value:
admin gets a masked clientSecret, front-of-house gets `clientSecret: null` and the
enabled-only list. The two list usecases collapse into listAuthProviders(deps,
{ isAdmin }); the PUT response and the merged frontend wrapper adopt the same
schema, deleting AuthProviderConfig/PublicAuthProvider entirely.

Regenerated the Go client (union types removed) and updated frontend types/consumers.

Closes #449.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:14:23 -04:00
Jasper VanandClaude Opus 4.8 7b8c8c915e refactor(api)!: unify object upload + rework delete/trash lifecycle (#448) (#454)
Resolve #448 — one upload entry point and an AIP-164 soft delete.

Upload: POST /objects now returns size-decided upload instructions
{ sessionId, partSize, urls }; the server picks single PutObject (<=5 GiB)
vs 5 GiB-part multipart (>5 GiB) and rejects >5 TiB. The client PUTs each
slice, reads its ETag, then POSTs them to
POST /objects/{id}/uploads/{sid}/completions (returns the live object).
DELETE /objects/{id}/uploads/{sid} aborts and discards the draft.

Trash: matters.status drops 'trashed' (enum is {draft,active}); trash is
tracked by the existing trashedAt timestamp. DELETE /objects/{id} now
soft-deletes; the recycle bin lives under /trash/objects (list roots, get,
restorations, purge). Empty-trash is a frontend loop over roots.

BREAKING CHANGE:
- removes PUT /objects/{id}/status and POST /objects/{id}/uploads
- PUT .../uploads/{sid}/status -> POST .../uploads/{sid}/completions {parts}
- DELETE /objects/{id} flips hard-purge -> soft-delete; permanent purge
  moves to DELETE /trash/objects/{id}
- DELETE /trash removed; restore is POST /trash/objects/{id}/restorations
- matters.status enum loses 'trashed' (migration backfills to trashedAt)

The migration swaps the matters_active_name_uniq partial index to exclude
trashed rows (WHERE status='active' AND trashed_at IS NULL). The single-PUT
presign is header-free so the uniform slice uploader's raw PUT matches the
S3 signature. Go downloader client + agent reworked to the unified flow.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:21:43 -04:00
Jasper VanandClaude Opus 4.8 78e2550e67 refactor(api)!: unify revoke/cancel on PUT /{resource}/{id}/status (#452) (#453)
* refactor(api)!: unify revoke/cancel on PUT /{resource}/{id}/status (#452)

Retire the misleading DELETE /shares/{token} and PATCH /store/orders/{orderId}
shapes in favor of the existing status-subresource convention already used by
background-jobs and download-tasks.

- Shares: PUT /api/shares/{token}/status {status:'revoked'} -> 200 + the updated
  creator ShareView. revokeShare now resolves the share before the UPDATE (the
  record is unresolvable once revoked) and builds the view via a composeShareView
  helper shared with viewShare; concurrently-revoked tokens now return 404.
  Removed the now-dead getCreatorByToken repo port/adapter method.
- Store: PUT /api/store/orders/{orderId}/status {status:'canceled'} -> 200. Only
  the local route shape changed; the upstream cloud SDK $patch call is untouched.
- Frontend: deleteShare -> revokeShare and cancelCloudOrder now use .status.$put
  via the Hono RPC client; updated the shares route component.
- Regenerated the Go OpenAPI client.

Note: revoking a share whose matter is trashed-but-not-purged now returns 404
(was 204), a consequence of reusing viewShare's resolution path.

Agent-Profile: https://agent-kanban.dev/agents/f759c704c282d88a
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(spec): rename share delete scenarios to revoke status-subresource

Align spec/shares.feature scenario tags (@shares/revoke,
@shares/revoke-non-creator) with the renamed [spec:] breadcrumbs so
lint:spec traceability passes.

Agent-Profile: https://agent-kanban.dev/agents/f759c704c282d88a
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(shares): keep revoke working for a trashed-but-not-purged matter

revokeShare switched to resolveByToken, which returned matter_trashed for a
soft-deleted (not purged) matter and was short-circuited to 404. Because
trashing a matter does not cascade to its shares, the share stayed active and
still appeared in the owner's list — so the owner could no longer revoke it
(privacy footgun: restoring the file re-exposed a share they believed revoked).

ShareResolution now carries the share/matter/recipient records on the
matter_trashed variant (and splits not_found/revoked into single-literal members
so control-flow narrowing works). Viewer-facing callers still branch on status,
so trashed -> 410 for viewers is unchanged. revokeShare treats matter_trashed as
revocable (ownership check, revokeByToken, revoked creator view), while not_found
and already-revoked still map to 404.

Adds unit coverage (trashed-matter revoke succeeds; non-creator still 403) and a
backend integration test (share a landing matter, trash it, PUT status revoked ->
200 + status:'revoked', DB flips).

Agent-Profile: https://agent-kanban.dev/agents/f759c704c282d88a
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 02:11:08 -04:00