* fix(download): force browser-downloadable files to save
* fix(storage): require signed upload CORS headers
* fix(storage): keep upload metadata out of signatures
* fix(storage): sign bare upload commands
* revert: keep upload metadata in signed commands
Move site configuration under settings, model analytics and licensing as resources, and isolate scheduler runs under the internal API.
BREAKING CHANGE: site email, branding, analytics, licensing, WebDAV verification, and scheduler endpoint paths have changed.
Refs #451
* fix!: unify pagination and realtime change delivery
Replace offset paging on affected unbounded collections with signed keyset tokens and infinite loading. Persist scoped resource changes so one global SSE connection can resume and invalidate query caches safely.
BREAKING CHANGE: migrated list APIs now accept pageToken and return nextPageToken instead of page and total fields.
Refs #450
* fix: keep page tokens at the HTTP boundary
Move signed page-token handling out of the pure domain layer so dependency-cruiser architecture checks pass without changing behavior.
* fix: route background job stats through usecase
Keep the HTTP boundary from reaching directly into repository ports and cover the new usecase wrapper.
* fix: align clients and checks with cursor pagination
* refactor: unify pagination boundaries and infinite loading
Replace opt-in profile listings with an opt-out private flag and a unified privacy endpoint.
BREAKING CHANGE: showOnProfile, listedAt, and the profile-listing endpoints are replaced by private and PUT /api/shares/:token/privacy.
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.
* 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>
* 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>
* 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>
Final piece of the restart→download→upload→seed robustness pass. nextTaskWorkStage
can route a task to uploadExistingResult based on the server checkpoint (upload
progress / runtime phase / download totals), but the engine may not report the
download complete yet — aria2 re-checks on-disk files after a restart (showing
downloaded=0/total=nil transiently), or the download was lost. The old code
panic()'d on that mismatch, which recoverTaskPanic turned into a permanently
failed task (this is what failed Moonfall after it had fully downloaded 7.4GB).
Now uploadExistingResult falls back to downloadThenUpload when the runtime isn't
complete (or has no result). downloadThenUpload re-attaches to the session
download, waits for aria2 to finish re-checking, and uploads on completion — no
data loss, no spurious failure. Test updated to assert resume-not-fail.
isAria2RPCDisconnected called err.Error() unconditionally, so a nil error
panicked (nil pointer deref). findSeed reaches it with a nil error whenever
tellStatus succeeds but the status isn't a seed — which crashed the downloader at
startup while restoring a retained seed from the ledger. Guard nil -> false.
Second restart bug behind the same stuck Moonfall: aria2 auto-seeds a torrent
the instant its download finishes (--seed-time), so a completed-but-not-yet-
uploaded download shows up in ListSeeds. reconcileEngineSeeds runs at startup
BEFORE the task loop marks tasks running, so 'running' was empty and it adopted
that seed as a done seed for managed expiry — skipping the upload entirely. The
task then sat at 'downloading' forever and the file would be deleted when the
seed expired.
Skip seeds whose task is still assigned/unfinished (AssignedTasks: assigned/
downloading/interrupted/uploading) — those belong to the task loop, which will
upload then seed them. Only genuinely orphaned seeds (completed tasks) are
adopted. Confirmed on prod: Moonfall finished (7.4GB on disk) with
result_object_id=null, stuck downloading.
After a downloader restart, the web UI showed a frozen download with 0 speed
even though aria2 was still downloading. Cause: on restart a task comes back as
'interrupted', but shouldAttachExistingAria2Task only attached for
downloading/uploading — so the worker RE-ADDED the magnet. aria2 had already
reloaded that download from its saved session, so the re-add hit error 12
(infohash already registered) and produced a dead duplicate gid. The worker then
polled the dead duplicate (bytes=0, speed=0) instead of the live download;
mergeTaskProgress's max() kept the stale byte count, so the UI froze. The real
download kept going, orphaned and unreported.
Fixes:
- shouldAttachExistingAria2Task also returns true for 'interrupted', so restart
attaches to the session-restored download instead of re-adding.
- findTask skips error/removed entries so it never attaches to a dead duplicate.
Confirmed on prod: Moonfall had two aria2 gids — 29ded179 (live, 6.8GB) and
3203aca6 (error-12 duplicate) — and the server runtime was frozen at the
pre-restart snapshot.