Two follow-ups:
- aria2 peers now report progress (0..1) derived from the peer's piece bitfield
(seeder => 1.0), matching qBittorrent. Previously the Peers list showed '—'
for aria2 because the field was never populated.
- The server builder copied the whole repo via 'COPY . .', so a cmd/-only
(downloader) change busted the layer and forced a full vite/tsup rebuild. Copy
only the build inputs + final-stage sources (src, server, shared, public,
index.html, vite.config.ts, tsconfig.json, migrations, scripts) so
downloader-only pushes reuse the cached server image. Verified the builder
stage still builds and produces dist/dist-server/migrations/entrypoint.
The docker-dev build took ~14 min, dominated by a ~12-min server-image build —
its arm64 half runs under QEMU emulation (native better-sqlite3/sharp).
- Dev images (:dev, :dev-cli) now build linux/amd64 only, skipping arm64 QEMU
entirely. Releases stay multi-arch. (The primary dev consumer runs amd64.)
- The Go CLI builder now runs on $BUILDPLATFORM and cross-compiles via
GOOS/GOARCH (CGO off), so the release arm64 CLI builds natively instead of
under QEMU — arm64 'go build' verified at ~5s vs emulated minutes. Adds
go build/mod cache mounts for fast incremental rebuilds.
Three related changes hardening the remote downloader, bundled because the
module-path rename touches every file — splitting would leave a mid-history
commit that doesn't build.
Engine supervision: a managed engine subprocess (aria2c / qbittorrent-nox)
that exits unexpectedly now crashes the worker. watchEngineProcess waits on
the child, logs the exit at error level, and cancels the run context so
`downloader up` returns non-zero; the container (restart: unless-stopped)
then restarts the whole stack. Previously the exit error was discarded and
the worker kept heartbeating as healthy while every task failed against the
dead RPC. A deliberate shutdown kill is told apart from a crash via a
stopping flag.
aria2 GID recovery: waitAria2 re-discovers the live GID via findTask when
aria2 reports 'GID ... is not found' mid-download (e.g. after an aria2
restart) instead of failing the task. Adds isAria2GIDNotFound, which matches
tellStatus's message format that isAria2DownloadNotFound missed.
CLI install: move the entrypoint to the module root (cmd/main.go) and rename
the module github.com/saltbo/zpan/cmd -> github.com/saltbo/zpan so
`go install .` from cmd/ yields a zpan binary. Internal imports shorten to
github.com/saltbo/zpan/internal/...; the Dockerfile builds the module root.
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>
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>
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>
* 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>
Docker build ran vite with the cloudflare plugin, which put SPA output
under dist/client/, while entry-node.ts serves from ./dist — causing
404 on every request in the container.
- Add build:node (vite --mode node) so the SPA lands in dist/, and
fold build:server into it as a single command
- Move better-sqlite3 to dependencies (Node runtime needs it; CF
Workers build tree-shakes it out anyway)
- Collapse Dockerfile from 4 stages to 2: one npm ci with a BuildKit
cache mount, then npm prune --omit=dev in place. Drops the
duplicate install and the cross-stage better-sqlite3 copy hack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add --external better-sqlite3 to tsup build to fix ESM runtime error
- Add docker-entrypoint.sh to auto-generate BETTER_AUTH_SECRET if not set
- Persist generated secret to /data/.auth_secret across restarts
- Move image-based compose files to deploy/ directory
- Add deploy/docker-compose.rustfs.yml for ZPan + RustFS setup
- Keep build-from-source docker-compose.yml at project root
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
npm prune --omit=dev hits peer dependency conflicts. Instead, install
prod deps with --ignore-scripts and copy the pre-built better-sqlite3
native binary from the full deps stage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
node:24-slim lacks python3/make/g++ needed by node-gyp to compile
better-sqlite3. Install them in deps stage, then use npm prune
to get prod-only node_modules with pre-built native binaries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The deps-prod stage runs npm ci --omit=dev which excludes husky,
but the prepare script tries to run it. Use --ignore-scripts since
prod deps don't need git hooks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split the build into a dedicated deps-prod stage (`npm ci --omit=dev`)
and copy only those node_modules into the runtime layer. Previously
the runtime image inherited the full builder node_modules including
biome, vitest, playwright, wrangler, drizzle-kit, tsup, and tsx —
hundreds of MB of devDependencies shipped to production for no reason.
Also COPY the migrations/ directory so the Node platform's programmatic
drizzle migrator can find SQL files at boot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace pnpm with npm in GitHub Actions workflow
- Upgrade Node.js from 20 to 24 in CI, Dockerfile, and engines
- Pin Node 24.14.1 via volta
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add Dockerfile and docker-compose for self-hosted Node.js deployment
Multi-stage build using node:20-slim. Builder runs pnpm build:pages and
the server bundle; runtime image carries the bundled entry, native deps
(better-sqlite3) and the static frontend. A symlink keeps entry-node.ts's
relative `../../dist` static root resolving correctly when running from
packages/server/dist. Compose mounts a named volume at /data so the
SQLite database persists across restarts.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
* fix: harden Dockerfile with proper layer caching, non-root user, and secret handling
Split build into deps/builder/runtime stages so pnpm install is cached
until lockfile changes. Run the Node process as a non-root `zpan` user.
Replace hardcoded BETTER_AUTH_SECRET with a required env var that fails
loudly when missing.
Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f
---------
Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
Remove the Go v1 codebase (preserved in the v1 branch) and establish
the foundation for v2 — a complete rewrite in TypeScript (server) and
Rust (CLI/desktop). This commit includes the product roadmap (v2.0–v2.9),
technical architecture document, and updated project documentation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>