266 Commits
Author SHA1 Message Date
saltboandClaude Opus 4.8 53ca110218 feat(about): split runtime into runtime engine + deployment platform
Flatten the instance `runtime` object into two fields: `runtime` (the JS engine,
node | workerd) and `platform` (the deployment host). The About page shows each
as its own row with friendly labels (e.g. "workerd" + "Cloudflare Workers",
"Node.js" + "Docker").

- Detect the platform from the entry file (entry === target): each serverless
  entry declares it; entry-node sniffs Cloud Run (K_SERVICE) / Docker
  (ZPAN_RUNTIME, set in the Dockerfile) / bare node. Cloudflare is detected from
  the D1 binding.
- Decouple the cloud payload: zpan-cloud-sdk fixes runtime { provider, target },
  so CloudInstanceInfo keeps that shape and buildCloudInstanceInfo maps to it;
  buildInstanceInfo serves the richer flat shape to the About API.
- Migrate PostHog instance telemetry to the runtime/platform shape and merge the
  duplicate runtimeInfo in licensing-admin into the shared one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:56:29 -04:00
saltboandClaude Opus 4.8 dc936e5eff feat(about): add changelog refresh button that bypasses the cache
The changelog is cached server-side (1h) and by React Query, so stale content
lingered. Add a refresh icon in the drawer header that calls the endpoint with
?refresh=true to skip the server cache and replaces the cached query data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 14:20:02 -04:00
saltboandClaude Opus 4.8 6aa5257f01 feat(about): take latest version from GitHub Releases, keep CHANGELOG drawer
The latest-version indicator now reads the newest published GitHub Release's
tag_name (the source of truth for "what shipped") instead of parsing the
changelog file. The drawer still renders the hand-maintained, product-facing
CHANGELOG.md — the auto-generated release notes are too technical for end users.

- Release lookup is best-effort: a rate-limited/unreachable GitHub API hides the
  version badge but never breaks the drawer (CHANGELOG.md drives the markdown).
- Drop parseLatestVersion; fetch the two sources concurrently behind the cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:57:05 -04:00
saltboandClaude Opus 4.8 e700fd4977 feat(about): add changelog drawer, latest-version check, and commit hash
Maintain a CHANGELOG.md (Keep a Changelog format) at the repo root and surface
it on the admin About page:

- The page now shows the running build's short commit hash next to the version,
  linked to the GitHub commit. Commit is injected at build time via a new
  resolveAppCommit() (ZPAN_APP_COMMIT -> WORKERS_CI_COMMIT_SHA -> git rev-parse),
  wired through vite/tsup defines, the node entry, Docker, and CI.
- A new admin-only GET /api/system/changelog endpoint fetches CHANGELOG.md from
  master on GitHub, caches it, parses the latest released version, and reports
  whether an update is available (semver compare against the running version).
- The About page renders a "latest version" row with an update-available badge
  and a side drawer that displays the changelog markdown.

Tests cover the semver compare, changelog parse/fetch caching, the API wrapper,
and the route (admin-gated, parsed payload).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:38:29 -04:00
Jasper VanandClaude Opus 4.8 5a5583b4ed feat(licensing): regroup comparison table by capability and add social-login/downloader gates (#423)
* feat(licensing): regroup comparison table by capability and add social-login/downloader gates

Decouple the feature comparison table's grouping axis from the pricing tier:
features are now grouped by capability (core/advanced) while edition
availability is expressed purely by the per-edition cells. Coming-soon
features show a badge next to the name with a muted check in the target
edition column, so it's clear which edition they will land in.

Tier reclassification (per product decision):
- Social login & OIDC: free = 1 provider, Pro/Business = unlimited
- Downloaders: free = 1, Pro/Business = unlimited
- Site announcements, Multi-IdP SSO, LDAP/SCIM, Analytics: Business-only

New runtime gates (enforced, mirroring the storages count-gate):
- social_login_unlimited in POST /api/admin/auth-providers (402 on 2nd)
- downloaders_unlimited in POST /api/admin/downloaders (402 on 2nd)
- site_announcements added to BUSINESS_ONLY_FEATURES

Copy cleanup:
- Rename rows that embedded a limit word: "Unlimited Team Workspaces"
  -> "Team Workspaces", "Storage Backends" -> "Storages"
- Clarify "Storage Plans" -> "Sell Storage & Traffic" (the quota_store feature)
- Make the Licensing page intro edition-neutral (Pro + Business) instead of
  the leftover Pro-only copy

Tests: add gate tests for both new limits; seed licenses in the existing
multi-provider/multi-downloader tests; switch announcement tests to a
Business license.

Note: site_announcements moving to Business takes full effect for real
licenses only once zpan-cloud stops listing it in Pro certificates; the
local edition-derived path is already updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(licensing): drop unused per-cert feature override, derive from edition

The optional `features[]` override on the license certificate was added with
the Pro/Business split but was never exercised: real certs carry only
`edition`, and entitlements are derived from it via the feature registry
(PRO_GATE_KEYS minus BUSINESS_ONLY_FEATURES). The override was dead in the
normal flow and duplicated the edition→feature mapping in two places.

Remove it so edition is the single source of truth:
- Drop `LicenseAssertion.features` and `normalizeFeatures` (verify.ts)
- `effectiveFeatures(edition)` no longer takes/honors an override list
- Simplify the test seed helpers (no `features` arg, no test-side
  business-only set) and update licensing tests to assert edition-derived
  entitlements

`BindingState.features` (the resolved list exposed to the client) is kept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(downloader): regenerate OpenAPI spec and Go client for new 402 response

The downloaders create route gained a 402 (feature_not_available) response
for the free-plan limit; regenerate the committed OpenAPI document and Go
client so openapi:downloader:check passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:32:59 -04:00
Jasper VanandClaude Opus 4.8 891e331ca8 feat(admin): add About page with instance info and edition-aware cloud links (#422)
Add an admin About page (/admin/about) showing version, instance metadata
(id/name/url/runtime/server OS/Node version) and edition status, plus two
CTAs: GitHub (star) and ZPan Cloud. When the instance holds a valid license
the cloud CTA deep-links to the certificate detail page.

- server: expose GET /api/system/instance (admin-only); extract shared
  runtimeInfo() helper; move InstanceInfo to shared types
- ribbon: link to the About page, relabel Community -> Free, recolor
  Pro (gold) / Business (indigo) to match the cloud certificate palette
- licensing: BoundStatusCard "Manage" deep-links to the certificate detail
  page when active with a license_id
- useEntitlement: expose licenseId
- tests: api wrapper + system endpoint + ribbon coverage

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 10:40:54 -04:00
saltboandClaude Opus 4.8 b10a1f3d84 fix(quota): chunk batch quota IN queries under D1's 100-param cap
getEffectiveQuotasByOrg passed every org id into a single IN clause. With
more than ~100 orgs the bound-parameter count exceeded Cloudflare D1's
per-query limit (100), so GET /api/admin/quotas threw "Failed query" in
production. Local SQLite has no such cap, so tests and CI never hit it.

Split the org ids into chunks of 90 (leaving headroom for the extra
status/timestamp params on the entitlements query) and run the chunked
queries concurrently, aggregating in memory. Query count stays constant
(2 per chunk) instead of the old 8 per org.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:18:19 -04:00
saltboandClaude Opus 4.8 9c16adbdc0 perf(quota): batch admin quota listing and move monthly reset to cron
The admin quota listing called getEffectiveQuota per org, firing ~8
sequential queries each (N+1). On D1 every query is a network round-trip,
so the endpoint scaled linearly with org count and risked the Workers
subrequest cap.

- Add getEffectiveQuotasByOrg: resolves every org in 2 queries (quota
  rows + active entitlements) and aggregates in memory. Route uses it.
- Remove the inline traffic-period reset writes from the read paths
  (getEffectiveQuota and the listing route). getEffectiveQuota already
  normalizes a stale period in memory, so reads stay correct.
- Add resetExpiredTrafficQuotas and run it on a new monthly cron
  (0 0 1 * *) for CF Workers and a daily idempotent interval for Node.
  The consume write path keeps its atomic reset-and-consume as a
  self-healing fallback if a scheduled run is missed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 03:08:27 -04:00
Jasper VanandClaude Opus 4.8 f922fd4d6f feat(quota): allow admins to edit and revoke granted entitlements (#419)
Admin-granted storage entitlements were insert-only. Add edit + revoke
for admin_grant entitlements (PATCH/DELETE /api/admin/users/:id/entitlements/:eid),
guarded so system-managed sources (free_plan, cloud_order) are untouchable.
Includes RPC wrappers, edit/revoke UI on the user detail page, i18n, and
full unit + integration coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 01:40:13 -04:00
Jasper VanandClaude Opus 4.8 af43701daf fix(version): set app version global in node entry (#418)
* fix(version): resolve app version at runtime in node entry

E2E runs the Node server via tsx, which bypasses the tsup build-time
define, leaving __ZPAN_APP_VERSION__ unset so getAppVersion throws and
/api/licensing/pair returns 500. Resolve the version at runtime via
resolveAppVersion (git describe) when the global is unset; in the built
output the define inlines the constant, so the branch is never reached
and git is not invoked in production. Keeps the git-describe version as
the single source of truth across all paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(version): inject app version into docker build

The Docker build excludes .git from its context (.dockerignore), so the
build-time git describe in build:node would throw and break the image
build. Let resolveAppVersion read ZPAN_APP_VERSION, set it from an
APP_VERSION build arg in the builder stage, and pass the release tag from
the release workflow. git describe stays the default everywhere else, so
the version is unified across CF Workers, tsx, and Docker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:29:47 -04:00
saltbo 43e92caaa6 chore(config): consolidate project tooling 2026-06-08 15:17:54 -04:00
saltbo ef122ec317 fix(version): inject app version at build time 2026-06-08 14:58:28 -04:00
saltbo 3b5f6c7fb5 fix(telemetry): persist detected site origin 2026-06-08 14:43:34 -04:00
saltbo 48b82cc804 fix(telemetry): avoid generic disable flag 2026-06-08 14:08:32 -04:00
saltbo 1a6a77435b fix(telemetry): disable reports during e2e 2026-06-08 14:04:57 -04:00
saltbo f52213b202 fix(telemetry): standardize instance reports 2026-06-08 13:47:20 -04:00
saltbo 178e7311e4 fix(telemetry): enable GeoIP enrichment 2026-06-08 13:29:16 -04:00
saltbo 400940204f fix(telemetry): align instance report fields 2026-06-08 13:25:55 -04:00
saltbo 7b5cd90eb6 fix(telemetry): use correct PostHog project token 2026-06-08 13:18:35 -04:00
saltbo 5c3df3c8ec fix(telemetry): use PostHog SDK for instance reports 2026-06-08 13:15:00 -04:00
Jasper Van bd9f047ebb fix(telemetry): report instance after deployment (#417) 2026-06-08 12:40:59 -04:00
Jasper Van 1d7ac9b071 fix(telemetry): use built-in product reporting endpoint (#416)
* fix(telemetry): use built-in product reporting endpoint

* fix(telemetry): send reports through posthog capture
2026-06-08 11:32:15 -04:00
Jasper Van de0213ad5d feat: add PostHog instance telemetry (#414)
* feat: add PostHog instance telemetry

Agent-Profile: https://agent-kanban.dev/agents/7bf89fb1be06098c

* fix: include node os release in telemetry

Agent-Profile: https://agent-kanban.dev/agents/7bf89fb1be06098c
2026-06-08 10:50:54 -04:00
Jasper Van 9d70acfdea feat(licensing): support independent business authorization
Support independent Pro and Business licensing, migrate Cloud store integration through the SDK, gate Business-only credit billing features, and validate the Cloud store E2E flow.
2026-06-08 01:42:23 -04:00
saltbo ecb38df84d fix(downloads): make remote usage billing resilient 2026-06-07 12:47:07 -04:00
saltbo 2e12929290 fix(downloads): improve downloader assignment handling 2026-06-06 20:13:23 -04:00
saltbo 71af982044 fix(downloader): store runtime updates as snapshots 2026-06-06 18:58:36 -04:00
saltbo 4579efda67 fix(downloader): clear eta after transfer completion 2026-06-06 18:45:41 -04:00
saltbo 00f42e762f fix(downloader): ignore stale seed progress after restart 2026-06-06 16:16:18 -04:00
saltbo 619d5dc980 fix(downloader): merge task runtime patch updates 2026-06-06 16:09:07 -04:00
saltbo d1c83ccece refactor(objects): run file operations sequentially 2026-06-06 12:46:18 -04:00
saltbo 1bd905366d fix(trash): list nested trashed roots 2026-06-06 12:16:11 -04:00
saltbo 25ab67208b refactor(quota): centralize storage usage reservations 2026-06-06 10:58:19 -04:00
saltbo 76f9e01f89 fix(traffic): meter webdav downloads 2026-06-06 10:37:14 -04:00
saltbo 4f15956209 fix(quota): reconcile storage usage after purge 2026-06-06 10:31:22 -04:00
saltbo 12604529b1 feat(downloads): report peer geo regions 2026-06-06 09:44:36 -04:00
saltbo 9486360389 feat(cli): promote downloader to zpan command 2026-06-06 01:38:16 -04:00
saltbo 65eb5d9738 fix(downloader): reset runtime on task restart 2026-06-06 00:54:26 -04:00
saltbo 6aa711bfb6 refactor(downloads): reshape download task status model 2026-06-06 00:07:21 -04:00
saltbo e113c6c2cf fix(downloader): clean recovered torrent runtime 2026-06-05 22:34:32 -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 9dd3ca0105 fix(downloader): separate interrupted task recovery 2026-06-05 21:34:57 -04:00
saltbo 6ba1f16282 fix(downloader): preserve retry recovery checkpoints 2026-06-05 20:52:38 -04:00
saltbo 3f7a859e73 refactor(downloader): consolidate api contract generation 2026-06-05 15:24:56 -04:00
saltbo 0b3bc2da09 fix(api-keys): return 429 for rate-limited keys 2026-06-05 13:19:34 -04:00
saltbo 3c9e1bf1ca fix(webdav): stabilize propfind directory listings 2026-06-05 12:39:56 -04:00
saltbo b05bf4f07e fix(downloads): normalize target folder paths 2026-06-05 10:10:44 -04:00
saltbo b5154e76d9 fix(storage): presign multipart complete without body 2026-06-05 01:42:41 -04:00
saltbo 83d26e9dd6 fix(storage): surface multipart complete failures 2026-06-05 01:31:50 -04:00