Infra: split dev and production (releases, services, deploy script)

- Production now runs from ~/freezmo-prod/current (release symlink) as a
  standalone server on :3207; releases are exported by scripts/deploy.sh,
  built and e2e-verified on a temp port, switched atomically, health-
  checked via the new /api/healthz (commit + build time) and rolled back
  automatically on failure. Last 5 releases kept; scripts/rollback.sh for
  manual rollback. Shared env + data live in ~/freezmo-prod/shared/ and
  are symlinked into every release (existing sync/push data migrated and
  verified identical).
- Dev runs from ~/freezmo via freezmo-dev.service: next dev on :3217
  (LAN only), its own build folder (.next-dev via NEXT_DIST_DIR), its own
  data (~/freezmo/.freezmo-dev), DEV badge, dev favicon/theme colour,
  "Freezmo Dev" PWA identity and never the production service worker.
- /api/healthz + the Settings Details version row (short commit + build
  time). output: standalone in next.config (next start still works).
- AGENTS.md documents the workflow rules: only dev updates during a task,
  temporary dirs/ports for test builds, and deploy-at-the-end as the only
  production path. README gains an Environments and deploy section.
- Fix found by the rehearsal: lib/dev-mode.ts must not be a client module
  (a server component importing a constant from a "use client" module
  gets a truthy proxy, which baked dev metadata into production builds).
- SW -> freezmo-v34.
This commit is contained in:
Martin Dvořák 2026-10-11 19:25:29 +00:00
commit 3f52ab47c2
22 changed files with 665 additions and 35 deletions

View file

@ -25,6 +25,7 @@ no database, no data leaving your machine except the requests you send to Ollama
- [Checking the connection](#checking-the-connection)
- [Running on your network / from your phone](#running-on-your-network--from-your-phone)
- [Keep it running as a service](#keep-it-running-as-a-service-systemd-user-service)
- [Environments and deploy](#environments-and-deploy)
- [Folders](#folders)
- [Install as an app (PWA)](#install-as-an-app-pwa)
- [Security notes](#security-notes)
@ -494,37 +495,90 @@ Notes for phones:
- Clipboard paste works best on desktop; on mobile use the camera or photo picker.
- Add the page to your home screen for a full-screen, app-like experience.
### Keep it running as a service (systemd user service)
### Keep it running as a service (systemd user services)
`deploy/freezmo.service` runs the production build under **systemd (user)**, so Freezmo comes back by
itself after a reboot and restarts if it ever crashes:
Two units keep Freezmo alive — **production** (`deploy/freezmo.service`, the
release on :3207) and **dev** (`deploy/freezmo-dev.service`, hot reload on
:3217). Both come back after a reboot and restart if they crash:
```bash
npm run build # the service serves the built app
mkdir -p ~/.config/systemd/user
cp deploy/freezmo.service ~/.config/systemd/user/
cp deploy/freezmo.service deploy/freezmo-dev.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now freezmo.service
systemctl --user enable --now freezmo.service freezmo-dev.service
loginctl enable-linger "$USER" # start at boot without logging in
```
| Command | What it does |
| --- | --- |
| `systemctl --user status freezmo` | is it up? |
| `journalctl --user -u freezmo -f` | live logs |
| `systemctl --user restart freezmo` | after a rebuild |
| `systemctl --user stop freezmo` | stop it |
| `systemctl --user status freezmo freezmo-dev` | are they up? |
| `journalctl --user -u freezmo-dev -f` | live dev logs (hot reload included) |
| `journalctl --user -u freezmo -f` | live production logs |
| `systemctl --user restart freezmo-dev` | after changing the dev unit |
It binds `0.0.0.0:3207` exactly like `npm start` and reads `.env.local` from the project directory.
The unit refuses to start without `.next/BUILD_ID`, so run `npm run build` before enabling it — and
again after pulling changes.
Production is **never** rebuilt or restarted by hand — `scripts/deploy.sh`
does that, and only with a pushed, verified commit. The dev instance reads
`~/freezmo/.env.local`, builds into `.next-dev` and keeps its data in
`.freezmo-dev/`, so nothing it does can touch production. See
[Environments and deploy](#environments-and-deploy) and
[AGENTS.md](AGENTS.md) for the rules.
Because the build and the running server share `.next/`, stop the service before a manual rebuild:
---
## Environments and deploy
Freezmo runs as two separate instances so that live work never touches what
you actually use:
| | **Dev** | **Production** |
| --- | --- | --- |
| Path | `~/freezmo` (the working tree) | `~/freezmo-prod/current` → `releases/<timestamp>-<sha>` |
| Service | `freezmo-dev.service` | `freezmo.service` |
| Port | **3217** — LAN only, hot reload | **3207** — the public URL |
| Build | `.next-dev` (dev server) | standalone server inside the release |
| Data | `~/freezmo/.freezmo-dev/` | `~/freezmo-prod/shared/freezmo/` (sync, push) |
| Env | `~/freezmo/.env.local` | `~/freezmo-prod/shared/env.local` |
| Look | DEV badge, dev favicon/theme, installs as **Freezmo Dev** | normal |
On your phone: **dev = `http://<lan-ip>:3217`**, production =
`http://<lan-ip>:3207` (and the public URL). The dev instance is bound to the
LAN only — nothing routes port 3217 through the public proxy — so source maps
and the HMR socket never leave the network. It uses a different origin, which
means separate storage and a separate service worker: the production worker is
never registered there and both PWAs can be installed side by side.
`NEXT_DIST_DIR` keeps the build folders apart (dev uses `.next-dev`, a plain
`npm run build` keeps producing `.next` for the test suites), so no build can
clobber a running server.
### Deploying
```bash
systemctl --user stop freezmo && npm run build && systemctl --user start freezmo
scripts/deploy.sh # deploy the latest origin/main
scripts/deploy.sh <commit> # a specific pushed commit
scripts/deploy.sh --skip-tests # fast path (lint + build only)
scripts/rollback.sh # back to the previous release
```
`deploy.sh` refuses a dirty working tree and commits that aren't on
`origin/main`, exports the commit into a fresh release directory, runs
`npm ci`, lint and the production build there, starts the new release on a
temporary port and runs the **full browser suite against it** (the running
production is untouched), then links the shared env + data, writes
`release.json`, flips the `current` symlink atomically, restarts the service
and polls `/api/healthz` until the new commit answers. Any failure after the
switch rolls back to the previous release automatically. The last 5 releases
are kept. The running version (short commit + build time) is shown in
Settings → Details.
Data that must survive deploys lives in `~/freezmo-prod/shared/`:
`env.local`, and `freezmo/{sync,push}` (symlinked into every release as
`.env.local` and `.freezmo`).
The day-to-day rules — only dev updates during a task, production changes only
through the deploy script at the end — are written down in
[AGENTS.md](AGENTS.md) so every future session follows them.
---
## Folders