Commit Graph
61 Commits
Author SHA1 Message Date
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 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 7fe16f3ed7 refactor(site): replace options with structured config APIs 2026-07-20 15:17:48 -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
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
saltbo b540956fb8 fix(admin): keep stats queries read-only 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 3695c80c2e feat(downloads): add task event timeline 2026-06-29 21:07:00 -04:00
saltbo 3fdc7b4ae0 refactor(downloader): reorganize cmd downloader runtime 2026-06-29 17:42:24 -04:00
saltbo 9acbc011ed feat(storage): add provider presets and request preview 2026-06-24 17:47:34 -04:00
saltbo e55dae3d2f refactor(admin): standardize management forms 2026-06-24 14:50:08 -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
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
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
saltbo 65eb5d9738 fix(downloader): reset runtime on task restart 2026-06-06 00:54:26 -04:00
saltbo 647fcb6d99 fix(downloads): normalize migrated task runtime 2026-06-06 00:17:11 -04:00
saltbo 6aa711bfb6 refactor(downloads): reshape download task status model 2026-06-06 00:07:21 -04:00
saltbo 031c568c40 fix(downloader): keep task upload tokens stable 2026-06-05 22:02:19 -04:00
saltbo 8f272b8a41 refactor(downloader): clarify task states and restart action 2026-06-05 21:51:32 -04:00
saltbo 4dcbab55b1 feat(downloads): improve task controls and metadata 2026-06-04 14:57:44 -04:00
saltbo 94dada75ba feat(downloads): add task actions and classification 2026-06-04 14:08:39 -04:00
saltbo cf3324b4a2 feat(downloads): preserve directory uploads 2026-06-03 12:05:55 -04:00
saltbo f698e5a8a9 feat(downloads): show normalized task details 2026-06-03 11:12:56 -04:00
saltbo 53076d7873 feat(downloads): add remote download workers 2026-06-03 02:21:50 -04:00
saltbo 1ff6d13998 feat(billing): meter storage egress with credits 2026-06-02 22:22:40 -04:00
saltbo 7f85da222f feat(quota): consolidate storage entitlements 2026-06-02 20:12:59 -04:00
Jasper Van e41ea3f016 feat(webdav): add RFC 4918 Class 2 support (#398)
* feat(webdav): add RFC 4918 class 2 support

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* test(webdav): cover RFC failure paths

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix(webdav): harden RFC lock and state semantics

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix(webdav): cover rejected RFC edge cases

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* test(webdav): cover precondition rejection paths

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix(webdav): close RFC lock compliance gaps

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix(webdav): close lock refresh scope gaps

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-12 11:38:29 -04:00
Jasper Van 2053b84c1a feat: add background job foundation (#388)
* feat: add background job foundation

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix: normalize cloud gift card create responses

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-11 19:24:54 -04:00
Jasper Van ec5d08b233 Report traffic egress to Cloud metering (#384)
* feat(api): report traffic egress to cloud metering

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

* test(api): cover cloud traffic redirect reporting

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

* test(api): cover cloud traffic failure branches

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

* test(api): cover final traffic metering branches

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

* test(api): cover cloud metering rollback paths

Agent-Profile: https://agent-kanban.dev/agents/a318237412dd8b98
2026-05-08 20:31:48 -04:00
Jasper Van bf8a4f5877 feat: add quota entitlements (#383)
Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-08 19:21:34 -04:00
Jasper Van 0b65e2dc15 [v2.6] Integrate zpan with new cloud order flow and complete migration cleanup
fix(store): guard cloud order actions by org
2026-05-08 15:42:05 -04:00
Jasper Van 11ab430493 refactor(quota-store): unify orders and gift cards
* refactor(quota-store): unify orders and gift cards

* test(quota-store): cover helper paths

* test(quota-store): cover webhook idempotency

* test(quota-store): cover cloud response normalization
2026-05-07 09:03:55 -04:00
Jasper VanandCopilot be58015ab2 feat(quota-store): support store reversal delivery
Remove the unique constraint on cloud_order_id in quota_delivery_events so
that a same-order reversal event (decrease) can be delivered after the original
purchase event (increase).  A non-unique index replaces the dropped constraint
to preserve query performance.

The resumeDeliveryEvent lookup is tightened to match only by eventId,
cloudRedemptionId, or code — never by cloudOrderId — so different events
for the same order are processed independently.

Add integration tests covering:
- same cloudOrderId increase → decrease both succeed and net to zero
- replaying the same decrease event is idempotent (no double-deduct)
- audit records decrease with action quota_storage_decrease and full metadata
- traffic decrease from same cloudOrderId processes independently

Update the in-memory test schema to match the new index definition.

Closes #v78t1unzz94w


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

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 19:01:24 -04:00
Jasper Van 9908953252 fix: drop local quota store package table
Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-06 17:50:34 -04:00
Jasper Van a4f9e3ff6a refactor: proxy quota store through cloud
Agent-Profile: https://agent-kanban.dev/agents/a318237412dd8b98
2026-05-06 16:16:20 -04:00
Jasper Van 3a88c17d6e feat: add monthly traffic quotas
Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-06 13:41:20 -04:00
Jasper Van 5632cbbc83 fix: use pro binding auth for quota store (#367)
* fix: use pro binding auth for quota store

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix: align quota store event token audience

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix: match cloud delivery event audience contract

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-06 00:29:48 -04:00
Jasper Van 8f6e4c2ff3 Fix quota store Cloud binding contract (#366)
* fix(quota-store): use Pro binding for Cloud store auth

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix(quota-store): align Cloud store binding contract

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-05 22:32:40 -04:00
Jasper Van 41877d384f feat: add quota store backend (#361)
* feat: add quota store backend

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* test: cover quota store cloud responses

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* test: cover quota store edge cases

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5

* fix: align quota store cloud contract

Agent-Profile: https://agent-kanban.dev/agents/1dc839c09b5ee5e5
2026-05-05 12:01:29 -04:00
saltbo 6314717795 feat(announcements): add site announcement management 2026-05-02 22:17:46 -04:00
saltbo f0e7af2798 feat(licensing): redesign Pro license binding 2026-04-29 20:20:18 -04:00
saltbo c4fc8c9f84 feat(admin): add site invitation signup flow 2026-04-27 19:49:19 -04:00
saltboandCopilot fa943ca8b3 feat: replace license_binding table with system_options keys
- Add license-state.ts helper for reading/writing license state as
  system_options key-value pairs instead of a dedicated singleton table
- Rewrite refresh.ts, has-feature.ts, entitlement.ts, licensing-admin.ts,
  licensing-refresh-runner.ts to use license-state helpers
- Generate migration 0014 to drop license_binding table
- Update all 10 test files to use setLicenseOptions instead of
  db.insert(licenseBinding)
- All 2809 tests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 22:58:13 -04:00
Jasper VanandBob 85554512c4 feat: add licenseBinding table, migration 0013_licensing, and shared licensing types (#339)
- Add `licenseBinding` singleton table (id=1) to server/db/schema.ts
- Correct `0012_image-hosting` journal idx from 11→12 (matches its prefix;
  was left wrong by previous rename-only fix in ef1fab8), then auto-generate
  migration 0013_licensing.sql via drizzle-kit
- New 0013_snapshot.json created; 0012_snapshot.json unchanged
- Add ProFeatures enum to shared/constants.ts
- Add shared/types/licensing.ts with LicenseEntitlement, ProFeature, BindingState
- Export new types from shared/types/index.ts

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

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-23 22:39:55 -04:00
2dca9d9ffb feat(db): image hosting schema, apiKey plugin, migration 0011 (v2.4.0 T1) (#315)
* feat(db): image hosting schema, apiKey plugin, migration 0011

Adds the DB infrastructure for the v2.4 image hosting feature:

- server/db/schema.ts: new `image_hosting_configs` (per-org singleton,
  custom domain + referer allowlist) and `image_hostings` tables with
  all required indexes and cascade-delete FKs
- server/db/auth-schema.ts: `apikey` table for @better-auth/api-key
  plugin (referenceId-indexed, rate-limiting fields, permissions column)
- server/auth.ts: enable apiKey plugin (references='organization',
  image-hosting:upload permission declared)
- shared/types/index.ts: ImageHostingConfig, ImageHosting, ImageHostingStatus
- migrations/0011_image-hosting.sql: generated by drizzle-kit (NOT
  hand-authored); creates all three new tables + indexes
- migrations/meta/0011_snapshot.json: baseline snapshot for future
  migration generation (repo was missing snapshots 0002–0010)
- package.json: add @better-auth/api-key ^1.6.2 dep, bump
  better-auth to ^1.6.2

Note: migration is tagged 0011_image-hosting (idx=11 in journal) rather
than 0012 because the existing 0011_notifications was registered as idx=10
(pre-existing numbering offset from a skipped 0006_ slot).

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

* fix: resolve coverage gap and migration filename collision

- Add getTableConfig FK reference tests to cover deferred .references()
  callbacks in schema.ts (fixes Codecov patch coverage < 94.98%)
- Rename 0011_image-hosting → 0012_image-hosting to avoid filename
  collision with pre-existing 0011_notifications migration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 03:18:52 -04:00
saltbo b310145752 fix(migrations): correct 0010/0011 timestamps; add CLAUDE.md rules
The manually-authored journal entries for 0010_shares and 0011_notifications
used when=1745xxxxxxxxx (April 2025), one year before the last applied
migration 0009 (when=1776200000000, March 2026). drizzle-kit migrator
orders by when, so dev databases already at 0009 silently skipped the new
migrations. CI passed because it starts from an empty DB.

- Fix the two timestamps to 1776300000000 / 1776400000000 so drizzle
  sees them as new.
- Add CLAUDE.md rules: never hand-author migrations; always add tests
  for src/lib/api.ts wrappers in the same PR.
2026-04-20 13:28:31 -04:00
2c8e2cc837 feat: v2.3.0 T1 — 站内信系统 (in-app notifications) (#307)
* feat: add in-app notification system (站内信) — schema, service, API, Bell UI

- Add `notifications` table to DB schema with userId/type/title/body/refType/refId/metadata/readAt/createdAt fields; two indexes for list & unread queries
- Migration `0010_notifications.sql` created manually (drizzle-kit requires TTY)
- Service layer: createNotification, listNotifications (paginated + unreadOnly filter), markAsRead (idempotent, owner-only), markAllAsRead, unreadCount
- REST API at `/api/notifications`: list + unreadCount, GET unread-count, POST :id/read (204), POST read-all
- Shared `Notification` type, `listNotificationsQuerySchema`, RPC client export
- NotificationBell (badge, 30s polling), NotificationDropdown, NotificationItem components injected into AppSidebar footer
- Bell badge capped at "9+"; unread items bold; click marks read + navigates via refType/refId
- i18n: en + zh translations for all notification keys
- 26 Node integration tests + 5 CF smoke tests; all 1884 + 26 tests pass

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

* test: add notification API wrapper tests and component logic tests; fix dead condition

- Add tests for listNotifications, getUnreadCount, markNotificationRead, markAllNotificationsRead in api.test.ts
- Add notification-bell.test.ts: badge label logic (0/5/"9+" cap) and polling interval
- Add notification-dropdown.test.ts: mark-all-read visibility, empty state, query key
- Add notification-item.test.ts: resolveHref (share token nav, malformed JSON), diffMinutes, isUnread, title style
- Fix dead condition in markNotificationRead: simplify `!res.ok && res.status !== 204` → `!res.ok`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: trigger CI check run for test coverage fixes

---------

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 01:23:54 -04:00
Jasper VanandBob de4938e75c feat: add shares schema, service layer, and lifecycle integration (#308)
- Add `shares` and `share_recipients` tables to Drizzle schema with indices
- Add migration 0010_shares.sql for shares/share_recipients tables
- Add `server/lib/password.ts` extracting scrypt hash/verify from auth.ts
  to eliminate duplicate crypto params across services
- Add `server/services/share.ts` implementing full CRUD + atomic counters:
  createShare, getShareByToken, incrementViews, incrementDownloadsAtomic
  (atomic SQL UPDATE), listSharesByCreator, revokeShare, cascadeDeleteByMatter
- Add `shared/schemas/share.ts` Zod validation schemas
- Export Share, ShareKind, ShareRecipient from shared/types
- Extend `purge.ts` to cascade-delete shares on matter hard-delete
- Add 38 integration tests and CF Workers atomic counter race tests

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

Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
2026-04-19 23:23:48 -04:00
saltboandClaude Opus 4.7 50c92d63ac fix(files): enforce unique names + Finder-style conflict resolver
Duplicate folder/file names silently created duplicates under the same
parent. Add DB-level partial unique index on (org_id, parent, LOWER(name))
for active rows, plus a centralized plan/commit helper threaded through
create, rename, move, copy, upload-confirm, and restore. 409 responses
open a Keep Both / Replace / Cancel dialog with sticky "apply to all"
for batch operations; case-insensitive match matches OS conventions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:28:40 -04:00