Add zh-CN, ja, ko, ru, es, pt-BR translations under docs/i18n/, keeping
the canonical English README/CHANGELOG at the repo root. Each file gets a
language switcher bar with the current language bolded.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- Wider drawer (max-w-xl) with consistent px-6 padding.
- Larger version headings and cleaner changelog markdown styling.
- Show the commit hash as plain text (no GitHub link).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
The drawer reused the announcement markdown styling, which made versions (h2)
and their Features/Fixes groups (h3) look nearly identical with no separation.
Add a changelog-specific renderer: each version is a bordered section with a
larger bold heading, and Features/Fixes render as small muted labels.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The intro and maintainer note showed up verbatim in the About page drawer.
The drawer already has its own title/subtitle, so start the file at the first
version. Maintainer guidance lives in shared/constants.ts comments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regenerate CHANGELOG.md from the GitHub release notes, covering every release
from 2.0.0 to 2.7.2. Entries describe user-facing value (not commit-level
detail) and each links to its GitHub release for the full technical log.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- Show the latest-version status as a clickable badge inside the Version row
instead of a separate row; remove the duplicate header update badge.
- Move the commit hash to its own "Commit" row below the version.
- Drop the standalone Changelog button; the drawer now opens by clicking the
version number or the latest-version badge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
* 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>
Cloudflare Workers Builds deploys master continuously without setting
ZPAN_APP_VERSION, and its checkout has no tags, so git describe --always fell
back to a bare commit hash — the deployed Worker showed a commit instead of a
version. package.json is present in every build environment, so read the
version from it when ZPAN_APP_VERSION is unset. Release/Docker/GH-action
deploys still pass the exact tag via the env var, which keeps priority.
Release process: bump package.json on each release so Workers Builds reports
the right version.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
wrangler deploy --dry-run compiles the Worker and validates wrangler.toml
plus the @cloudflare/vite-plugin deploy config (assets directory, queues,
bindings) without contacting Cloudflare, so it needs no secrets and runs on
fork PRs. Guards the deploy regressions unit tests miss — a dropped build
step (as in #417) or a binding added to wrangler.toml without provisioning,
the two failures behind #415.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v2.7 added the zpan-archive-jobs queue binding but the CF deploy workflow
never created it, and bd9f047 (#417) accidentally dropped the vite build
step. Add a queue-provisioning step (from @wshijiang) and restore an explicit
Build step so @cloudflare/vite-plugin generates the deploy config with the
correct assets directory.
Co-Authored-By: saltbo <rdsaltbo@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Public URL field referenced admin.settings.sitePublicOrigin plus its
Placeholder and Hint variants, but none existed in en.json or zh.json, so
the label, placeholder, and helper text rendered as raw keys. Add all three
to both locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bundled app-version.mjs self-exec block (`import.meta.url ===
file://${process.argv[1]}`) fired at runtime once tsup inlined it into
entry-node.js, since the container runs `node dist-server/entry-node.js`.
It shelled out to `git describe`, which is absent in the image, crashing
the server on boot and leaving the compose stack unhealthy. Drop the
unused CLI entry; resolveAppVersion is still build-time inlined.
Also stop the CLI build from overwriting the `latest` tag: metadata-action
defaults to latest=auto, so cli-meta re-added a bare `latest` that, running
after the server build, clobbered it with the downloader image. Pin
latest=false on cli-meta.
Add a docker-smoke CI job that runs `docker compose up --wait` so an image
that fails to boot turns CI red, and give the GeoIP download --retry to cut
flake on that external fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
* feat(admin): add diagonal license-edition corner ribbon to admin layout
Add a fixed diagonal corner ribbon (top-right) at the admin Layout level that
shows the current license edition with appropriate color and link:
- Community (unbound): gray #64748B, links to GitHub
- Pro (bound, edition=pro): brand blue #1A73E8, links to GitHub
- Business (bound, edition=business): gold #F59E0B, links to cloud dashboard
Changes:
- src/components/billing/LicenseRibbon.tsx: new ribbon component
- src/components/billing/LicenseRibbon.test.tsx: 15 co-located tests
- src/hooks/useEntitlement.ts: expose cloudDashboardUrl from BindingState
- src/routes/_authenticated/admin/route.tsx: render LicenseRibbon at layout level
- src/i18n/locales/en.json + zh.json: add admin.licenseRibbon.* keys
- src/routes/_authenticated/teams/index.test.tsx: add cloudDashboardUrl to mock
Agent-Profile: https://agent-kanban.dev/agents/e41f6168983d6e3f
* fix(admin): address review findings on LicenseRibbon
- Type editionKey parameter as LicenseEdition | null (import from @shared/types)
to stay consistent with the shared type contract
- Increase container from h-20 w-20 to h-24 w-24 for a larger visible and
clickable ribbon area in the corner
- Strengthen aria-label test to assert the exact interpolated key value
rather than just truthiness
Agent-Profile: https://agent-kanban.dev/agents/e41f6168983d6e3f
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>
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>
* 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>
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.