feat: deploy zopu single-node production
This commit is contained in:
@@ -140,6 +140,11 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
|
|||||||
onOpenUnit={(selectedWorkUnitId) =>
|
onOpenUnit={(selectedWorkUnitId) =>
|
||||||
navigate(`/chat/${selectedWorkUnitId}`)
|
navigate(`/chat/${selectedWorkUnitId}`)
|
||||||
}
|
}
|
||||||
|
onProjectSelect={(projectId) =>
|
||||||
|
projectWorkspace.projectWorkspace.setSelectedProjectId(
|
||||||
|
projectId as Id<"projects">
|
||||||
|
)
|
||||||
|
}
|
||||||
onRestoreChecked={handleRestoreChecked}
|
onRestoreChecked={handleRestoreChecked}
|
||||||
onSettingsClose={() => workspace.setSettingsOpen(false)}
|
onSettingsClose={() => workspace.setSettingsOpen(false)}
|
||||||
onSettingsOpen={() => workspace.setSettingsOpen(true)}
|
onSettingsOpen={() => workspace.setSettingsOpen(true)}
|
||||||
|
|||||||
@@ -8,11 +8,20 @@
|
|||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 1. Convex (control plane) — REQUIRED
|
# 1. Convex (control plane) — REQUIRED
|
||||||
# Convex is already deployed; provide the production deployment URL.
|
# Public URLs for the self-hosted Convex deployment.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
CONVEX_URL=https://your-deployment.convex.cloud
|
CONVEX_URL=https://your-deployment.convex.cloud
|
||||||
CONVEX_SITE_URL=https://your-deployment.convex.site
|
CONVEX_SITE_URL=https://your-deployment.convex.site
|
||||||
SITE_URL=http://localhost:5173
|
SITE_URL=http://localhost:5173
|
||||||
|
VITE_CONVEX_URL=https://your-deployment.convex.cloud
|
||||||
|
VITE_CONVEX_SITE_URL=https://your-deployment.convex.site
|
||||||
|
VITE_FLUE_URL=http://localhost:3583
|
||||||
|
|
||||||
|
# Self-hosted Convex origins used by convex/docker-compose.yml
|
||||||
|
CONVEX_CLOUD_ORIGIN=https://your-deployment.convex.cloud
|
||||||
|
CONVEX_SITE_ORIGIN=https://your-deployment.convex.site
|
||||||
|
CONVEX_INSTANCE_NAME=zopu-production
|
||||||
|
CONVEX_INSTANCE_SECRET=
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2. Self-hosted Git / Gitea — REQUIRED for issue lifecycle
|
# 2. Self-hosted Git / Gitea — REQUIRED for issue lifecycle
|
||||||
@@ -46,16 +55,15 @@ AGENT_MODEL_MAX_TOKENS=131072
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 5. Zopu agent service (Flue)
|
# 5. Zopu agent service (Flue)
|
||||||
# FLUE_DB_TOKEN authenticates the Flue persistence adapter.
|
# FLUE_DB_TOKEN authenticates the Flue persistence adapter.
|
||||||
# PORT is the Flue Node HTTP server listen port.
|
# zopu-agent.service pins the Flue Node server to port 3583.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
FLUE_DB_TOKEN=replace-with-long-random-token
|
FLUE_DB_TOKEN=replace-with-long-random-token
|
||||||
PORT=3583
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 6. Daemon identity
|
# 6. Daemon identity
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
DAEMON_ID=zopu-dedicated
|
DAEMON_ID=zopu-dedicated
|
||||||
DAEMON_NAME=Zopu Dedicated Server
|
DAEMON_NAME=Zopu-Dedicated-Server
|
||||||
DAEMON_VERSION=0.0.0
|
DAEMON_VERSION=0.0.0
|
||||||
DAEMON_HEARTBEAT_MS=15000
|
DAEMON_HEARTBEAT_MS=15000
|
||||||
DAEMON_COMMAND_LEASE_MS=60000
|
DAEMON_COMMAND_LEASE_MS=60000
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
# Zopu Single-Node Runtime Deployment
|
# Zopu Single-Node Runtime Deployment
|
||||||
|
|
||||||
Deployment artifacts for the Zopu execution plane on a single Debian dedicated
|
Deployment artifacts for the complete Zopu stack on a single Debian dedicated server: the React Router web app, a persistent self-hosted Convex control plane, the daemon (Bun/Effect + AgentOS/RivetKit), the Flue agent service, Docker Engine, and supporting infrastructure.
|
||||||
server. Convex is already deployed as the control plane; this lane deploys only
|
|
||||||
the execution plane: the daemon (Bun/Effect + AgentOS/RivetKit), the Flue agent
|
|
||||||
service, Docker Engine for future Orb sandboxes, and supporting infrastructure.
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -13,23 +10,17 @@ service, Docker Engine for future Orb sandboxes, and supporting infrastructure.
|
|||||||
│ ~12 CPU cores · ~40 GB RAM · single-node │
|
│ ~12 CPU cores · ~40 GB RAM · single-node │
|
||||||
│ │
|
│ │
|
||||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||||
│ │ zopu-daemon │ │ zopu-agent │ │ Docker │ │
|
│ │ zopu-web │ │ zopu-agent │ │ Docker │ │
|
||||||
│ │ (systemd) │ │ (systemd) │ │ Engine │ │
|
│ │ (systemd) │ │ (systemd) │ │ Engine │ │
|
||||||
│ │ │ │ │ │ │ │
|
│ │ │ │ │ │ │ │
|
||||||
│ │ Effect daemon│ │ Flue Node │ │ Orb sandboxes│ │
|
│ │ React Router │ │ Flue Node 22 │ │ Convex │ │
|
||||||
│ │ + RivetKit │ │ server.mjs │ │ (future) │ │
|
│ │ :13100 │ │ server.mjs │ │ backend │ │
|
||||||
│ │ in-process │ │ :3583 │ │ │ │
|
│ │ │ │ :3583 │ │ :3210/:3211 │ │
|
||||||
│ │ engine │ │ │ │ │ │
|
|
||||||
│ │ + native │ │ │ │ │ │
|
|
||||||
│ │ sidecar │ │ │ │ │ │
|
|
||||||
│ │ :6420 │ │ │ │ │ │
|
|
||||||
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
|
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ └────────┬───────────┘ │
|
│ ┌──────▼───────┐ │
|
||||||
│ │ │
|
│ │ zopu-daemon │ Bun + Effect + RivetKit :6420 │
|
||||||
│ ┌────────▼─────────┐ │
|
│ └──────────────┘ │
|
||||||
│ │ Convex (cloud) │ ← control plane (pre-deployed)│
|
|
||||||
│ └──────────────────┘ │
|
|
||||||
│ │
|
│ │
|
||||||
│ systemd timers: health-check (60s), docker-cleanup (daily) │
|
│ systemd timers: health-check (60s), docker-cleanup (daily) │
|
||||||
│ cron: disk-monitor (daily 06:00) │
|
│ cron: disk-monitor (daily 06:00) │
|
||||||
@@ -40,31 +31,15 @@ service, Docker Engine for future Orb sandboxes, and supporting infrastructure.
|
|||||||
|
|
||||||
### Single-node RivetKit topology
|
### Single-node RivetKit topology
|
||||||
|
|
||||||
The daemon calls `registry.start()` from `rivetkit`, which boots an
|
The daemon calls `registry.start()` from `rivetkit`, which boots an **in-process RivetKit engine** (envoy mode) backed by a **native Rust sidecar** binary (`@rivet-dev/agentos-sidecar`, platform-resolved). The engine listens on `RIVET_ENDPOINT` (default `http://localhost:6420`). The daemon then calls `createClient(RIVET_ENDPOINT)` to connect back to its own in-process engine for actor dispatch.
|
||||||
**in-process RivetKit engine** (envoy mode) backed by a **native Rust
|
|
||||||
sidecar** binary (`@rivet-dev/agentos-sidecar`, platform-resolved). The engine
|
|
||||||
listens on `RIVET_ENDPOINT` (default `http://localhost:6420`). The daemon then
|
|
||||||
calls `createClient(RIVET_ENDPOINT)` to connect back to its own in-process
|
|
||||||
engine for actor dispatch.
|
|
||||||
|
|
||||||
Evidence: RivetKit source `chunk-YDUQHING.js` line 4751 —
|
Evidence: RivetKit source `chunk-YDUQHING.js` line 4751 — `DEFAULT_ENDPOINT = "http://localhost:6420"`. The `Registry.start()` method calls `#startEnvoy()` → `runtime.serveRegistry()` for serverful mode (Mode A). The `createClient()` function reads `RIVET_ENDPOINT` env or defaults to the same `http://localhost:6420`.
|
||||||
`DEFAULT_ENDPOINT = "http://localhost:6420"`. The `Registry.start()` method
|
|
||||||
calls `#startEnvoy()` → `runtime.serveRegistry()` for serverful mode (Mode A).
|
|
||||||
The `createClient()` function reads `RIVET_ENDPOINT` env or defaults to the
|
|
||||||
same `http://localhost:6420`.
|
|
||||||
|
|
||||||
**No separate Rivet Engine process is required.** The engine, actor envoy, and
|
**No separate Rivet Engine process is required.** The engine, actor envoy, and sidecar all run inside the daemon process. A future multi-node deployment would externalize the engine, but that is out of scope.
|
||||||
sidecar all run inside the daemon process. A future multi-node deployment
|
|
||||||
would externalize the engine, but that is out of scope.
|
|
||||||
|
|
||||||
### Docker / Orb boundary
|
### Docker / Orb boundary
|
||||||
|
|
||||||
Docker Engine is installed and the `zopu` service user is in the `docker`
|
Docker Engine is installed and the `zopu` service user is in the `docker` group. The daemon's systemd unit includes `SupplementaryGroups=docker`. However, **no Docker-backed sandbox code is currently wired**. The Orb sandbox lane contract has not landed; Docker access is provisioned now so the boundary is ready. The current agent uses the in-process AgentOS VM (Wasm/V8) sandbox, not Docker.
|
||||||
group. The daemon's systemd unit includes `SupplementaryGroups=docker`.
|
|
||||||
However, **no Docker-backed sandbox code is currently wired**. The Orb
|
|
||||||
sandbox lane contract has not landed; Docker access is provisioned now so the
|
|
||||||
boundary is ready. The current agent uses the in-process AgentOS VM (Wasm/V8)
|
|
||||||
sandbox, not Docker.
|
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
@@ -76,6 +51,7 @@ deploy/zopu-runtime/
|
|||||||
├── systemd/
|
├── systemd/
|
||||||
│ ├── zopu-daemon.service # Daemon systemd unit
|
│ ├── zopu-daemon.service # Daemon systemd unit
|
||||||
│ ├── zopu-agent.service # Agent systemd unit
|
│ ├── zopu-agent.service # Agent systemd unit
|
||||||
|
│ ├── zopu-web.service # React Router web systemd unit
|
||||||
│ ├── zopu-health.service # Health check oneshot
|
│ ├── zopu-health.service # Health check oneshot
|
||||||
│ ├── zopu-health.timer # Health check every 60s
|
│ ├── zopu-health.timer # Health check every 60s
|
||||||
│ ├── zopu-docker-cleanup.service
|
│ ├── zopu-docker-cleanup.service
|
||||||
@@ -108,7 +84,7 @@ bash bootstrap.sh
|
|||||||
nano /opt/zopu/.env
|
nano /opt/zopu/.env
|
||||||
|
|
||||||
# 5. Start services:
|
# 5. Start services:
|
||||||
systemctl start zopu-daemon
|
systemctl start zopu-web zopu-daemon
|
||||||
sleep 3
|
sleep 3
|
||||||
systemctl start zopu-agent
|
systemctl start zopu-agent
|
||||||
|
|
||||||
@@ -122,20 +98,20 @@ systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer
|
|||||||
## Start / stop / restart
|
## Start / stop / restart
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start both services
|
# Start all services
|
||||||
systemctl start zopu-daemon zopu-agent
|
systemctl start zopu-web zopu-daemon zopu-agent
|
||||||
|
|
||||||
# Stop both services
|
# Stop all services
|
||||||
systemctl stop zopu-agent zopu-daemon
|
systemctl stop zopu-agent zopu-daemon zopu-web
|
||||||
|
|
||||||
# Restart (daemon first — it owns the RivetKit engine)
|
# Restart (daemon first — it owns the RivetKit engine)
|
||||||
systemctl restart zopu-daemon && sleep 3 && systemctl restart zopu-agent
|
systemctl restart zopu-web zopu-daemon && sleep 3 && systemctl restart zopu-agent
|
||||||
|
|
||||||
# Enable on boot
|
# Enable on boot
|
||||||
systemctl enable zopu-daemon zopu-agent
|
systemctl enable zopu-web zopu-daemon zopu-agent
|
||||||
|
|
||||||
# Disable on boot
|
# Disable on boot
|
||||||
systemctl disable zopu-daemon zopu-agent
|
systemctl disable zopu-web zopu-daemon zopu-agent
|
||||||
```
|
```
|
||||||
|
|
||||||
## Log inspection
|
## Log inspection
|
||||||
@@ -183,16 +159,14 @@ systemctl list-timers zopu-health.timer
|
|||||||
```
|
```
|
||||||
|
|
||||||
The health check probes:
|
The health check probes:
|
||||||
|
|
||||||
1. `zopu-daemon` systemd unit is active
|
1. `zopu-daemon` systemd unit is active
|
||||||
2. `zopu-agent` systemd unit is active
|
2. `zopu-agent` systemd unit is active
|
||||||
3. RivetKit engine port (default 6420) accepts TCP connections
|
3. RivetKit engine port (default 6420) accepts TCP connections
|
||||||
4. Flue agent port (default 3583) accepts TCP connections
|
4. Flue agent port (default 3583) accepts TCP connections
|
||||||
5. Docker daemon responds to `docker info`
|
5. Docker daemon responds to `docker info`
|
||||||
|
|
||||||
No HTTP health endpoints are assumed. Flue does not expose one by design
|
No HTTP health endpoints are assumed. Flue does not expose one by design (per Flue docs: "Flue does not add a health endpoint"). RivetKit's health route is internal to the registry runtime and not documented as publicly addressable on the engine endpoint.
|
||||||
(per Flue docs: "Flue does not add a health endpoint"). RivetKit's health
|
|
||||||
route is internal to the registry runtime and not documented as
|
|
||||||
publicly addressable on the engine endpoint.
|
|
||||||
|
|
||||||
## Update to commit
|
## Update to commit
|
||||||
|
|
||||||
@@ -208,6 +182,7 @@ publicly addressable on the engine endpoint.
|
|||||||
```
|
```
|
||||||
|
|
||||||
The update script:
|
The update script:
|
||||||
|
|
||||||
1. Records current HEAD to `.last-deployed-sha`
|
1. Records current HEAD to `.last-deployed-sha`
|
||||||
2. Fetches, resolves branch-or-commit, checks out
|
2. Fetches, resolves branch-or-commit, checks out
|
||||||
3. `bun install`, builds daemon and agent
|
3. `bun install`, builds daemon and agent
|
||||||
@@ -224,9 +199,7 @@ The update script:
|
|||||||
/opt/zopu/deploy/zopu-runtime/scripts/rollback.sh abc123def456
|
/opt/zopu/deploy/zopu-runtime/scripts/rollback.sh abc123def456
|
||||||
```
|
```
|
||||||
|
|
||||||
Rollback reads `.last-deployed-sha` (written by `update.sh`), checks out that
|
Rollback reads `.last-deployed-sha` (written by `update.sh`), checks out that commit, rebuilds, and restarts services. The pre-rollback SHA is saved to `.pre-rollback-sha` for re-rollback if needed.
|
||||||
commit, rebuilds, and restarts services. The pre-rollback SHA is saved to
|
|
||||||
`.pre-rollback-sha` for re-rollback if needed.
|
|
||||||
|
|
||||||
## Docker cleanup
|
## Docker cleanup
|
||||||
|
|
||||||
@@ -242,11 +215,11 @@ systemctl list-timers zopu-docker-cleanup.timer
|
|||||||
```
|
```
|
||||||
|
|
||||||
Cleanup prunes:
|
Cleanup prunes:
|
||||||
|
|
||||||
- Stopped containers older than 24 hours
|
- Stopped containers older than 24 hours
|
||||||
- Dangling (untagged) images
|
- Dangling (untagged) images
|
||||||
- Unused networks
|
- Unused networks
|
||||||
|
|
||||||
|
|
||||||
Named volumes and running containers are never removed.
|
Named volumes and running containers are never removed.
|
||||||
|
|
||||||
## Disk-space monitoring
|
## Disk-space monitoring
|
||||||
@@ -259,69 +232,71 @@ Named volumes and running containers are never removed.
|
|||||||
/opt/zopu/deploy/zopu-runtime/scripts/disk-monitor.sh --warn-percent 90
|
/opt/zopu/deploy/zopu-runtime/scripts/disk-monitor.sh --warn-percent 90
|
||||||
```
|
```
|
||||||
|
|
||||||
A cron job runs at 06:00 daily and writes to `/var/log/zopu/disk-monitor.log`.
|
A cron job runs at 06:00 daily and writes to `/var/log/zopu/disk-monitor.log`. Default alert threshold is 80%.
|
||||||
Default alert threshold is 80%.
|
|
||||||
|
|
||||||
## Firewall and private networking
|
## Firewall and private networking
|
||||||
|
|
||||||
The firewall (`ufw`) is deny-by-default:
|
The firewall (`ufw`) is deny-by-default:
|
||||||
|
|
||||||
- SSH (port 22) is allowed on all interfaces
|
- SSH (port 22) is allowed on all interfaces
|
||||||
- All traffic on `tailscale0` is allowed (Tailscale private overlay)
|
- All traffic on `tailscale0` is allowed (Tailscale private overlay)
|
||||||
- All other incoming traffic is denied
|
- All other incoming traffic is denied
|
||||||
|
|
||||||
The RivetKit engine (`:6420`) and Flue agent (`:3583`) ports are **not**
|
The RivetKit engine (`:6420`) and Flue agent (`:3583`) ports are **not** exposed on public interfaces. Reachability options:
|
||||||
exposed on public interfaces. Reachability options:
|
|
||||||
|
|
||||||
1. **Tailscale** (recommended): bootstrap runs `ufw allow in on tailscale0`
|
1. **Tailscale** (recommended): bootstrap runs `ufw allow in on tailscale0` so all ports are reachable over the private overlay. Set `TAILSCALE_AUTHKEY` before running bootstrap to configure automatically. Other Tailscale-connected machines can reach the agent at `http://zopu-runtime:3583` and the engine at `http://zopu-runtime:6420`.
|
||||||
so all ports are reachable over the private overlay. Set
|
2. **Custom private interface**: if you have a non-Tailscale private network (e.g. a VLAN or wireguard interface), add an explicit rule:
|
||||||
`TAILSCALE_AUTHKEY` before running bootstrap to configure automatically.
|
|
||||||
Other Tailscale-connected machines can reach the agent at
|
|
||||||
`http://zopu-runtime:3583` and the engine at `http://zopu-runtime:6420`.
|
|
||||||
2. **Custom private interface**: if you have a non-Tailscale private network
|
|
||||||
(e.g. a VLAN or wireguard interface), add an explicit rule:
|
|
||||||
```bash
|
```bash
|
||||||
ufw allow in on eth1 # or your private interface name
|
ufw allow in on eth1 # or your private interface name
|
||||||
```
|
```
|
||||||
Do NOT assume direct private IP access works by default — the deny-incoming
|
Do NOT assume direct private IP access works by default — the deny-incoming policy blocks it until an interface-specific rule is added.
|
||||||
policy blocks it until an interface-specific rule is added.
|
3. **Caddy** (optional): install Caddy and use the annotated Caddyfile in `caddy/` if you need TLS termination or a public ingress point.
|
||||||
3. **Caddy** (optional): install Caddy and use the annotated Caddyfile in
|
|
||||||
`caddy/` if you need TLS termination or a public ingress point.
|
|
||||||
|
|
||||||
## Environment groups
|
## Environment groups
|
||||||
|
|
||||||
See [`.env.template`](./.env.template) for the full annotated template. The
|
See [`.env.template`](./.env.template) for the full annotated template. The eight required groups:
|
||||||
eight required groups:
|
|
||||||
|
|
||||||
| Group | Variables |
|
| Group | Variables |
|
||||||
|-------|-----------|
|
| --- | --- |
|
||||||
| Convex | `CONVEX_URL`, `CONVEX_SITE_URL`, `SITE_URL` |
|
| Convex | `CONVEX_URL`, `CONVEX_SITE_URL`, `SITE_URL` |
|
||||||
| Gitea | `GITEA_URL`, `GITEA_TOKEN` |
|
| Gitea | `GITEA_URL`, `GITEA_TOKEN` |
|
||||||
| Model gateway | `AGENT_MODEL_*` |
|
| Model gateway | `AGENT_MODEL_*` |
|
||||||
| AgentOS/RivetKit | `RIVET_ENDPOINT` (optional) |
|
| AgentOS/RivetKit | `RIVET_ENDPOINT` (optional) |
|
||||||
| Zopu agent | `FLUE_DB_TOKEN`, `PORT` |
|
| Zopu agent | `FLUE_DB_TOKEN` (`zopu-agent.service` sets port 3583) |
|
||||||
| Daemon | `DAEMON_ID`, `DAEMON_NAME`, `DAEMON_VERSION`, `DAEMON_HEARTBEAT_MS`, `DAEMON_COMMAND_LEASE_MS` |
|
| Daemon | `DAEMON_ID`, `DAEMON_NAME`, `DAEMON_VERSION`, `DAEMON_HEARTBEAT_MS`, `DAEMON_COMMAND_LEASE_MS` |
|
||||||
| Docker sandbox | group membership (no env vars) |
|
| Docker sandbox | group membership (no env vars) |
|
||||||
| Service auth | `AUTH_SECRET` (if needed) |
|
| Service auth | `AUTH_SECRET` (if needed) |
|
||||||
|
|
||||||
|
## Public single-node routes
|
||||||
|
|
||||||
|
The checked-in Caddy example assumes Cloudflare Tunnel terminates TLS and sends the four Zopu hosts to Caddy on loopback:
|
||||||
|
|
||||||
|
- `zopu.sai-onchain.me` → React Router web app on `127.0.0.1:13100`
|
||||||
|
- `zopu-api.sai-onchain.me` → Convex API on `127.0.0.1:3210`
|
||||||
|
- `zopu-site.sai-onchain.me` → Convex HTTP actions on `127.0.0.1:3211`
|
||||||
|
- `zopu-agent.sai-onchain.me` → Flue on `127.0.0.1:3583`
|
||||||
|
|
||||||
|
Self-hosted Convex state lives in the `zopu-convex-data` Docker volume. Generate the CLI admin key after the backend is healthy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose \
|
||||||
|
--env-file /opt/zopu/.env \
|
||||||
|
-f /opt/zopu/deploy/zopu-runtime/convex/docker-compose.yml \
|
||||||
|
exec backend ./generate_admin_key.sh
|
||||||
|
```
|
||||||
|
|
||||||
## What is NOT deployed
|
## What is NOT deployed
|
||||||
|
|
||||||
- **Web app**: the web frontend is not deployed in this lane.
|
|
||||||
- **Kubernetes**: no container orchestration.
|
- **Kubernetes**: no container orchestration.
|
||||||
- **PostgreSQL for Rivet**: the in-process RivetKit engine uses its own
|
- **PostgreSQL for Rivet**: the in-process RivetKit engine uses its own storage; no external PostgreSQL is required.
|
||||||
storage; no external PostgreSQL is required.
|
|
||||||
- **Multi-node coordination**: single-node only.
|
- **Multi-node coordination**: single-node only.
|
||||||
- **Public administration endpoints**: no admin HTTP surface.
|
- **Public administration endpoints**: no admin HTTP surface.
|
||||||
- **Secrets in source**: `.env` is never committed; `.env.template` contains
|
- **Secrets in source**: `.env` is never committed; `.env.template` contains only placeholder values.
|
||||||
only placeholder values.
|
- **Docker-backed Orb sandboxes**: Docker is installed and access is provisioned, but no Orb sandbox code is wired. This is a boundary prepared for the Orb lane, not a working feature.
|
||||||
- **Production generated-app hosting**: only the execution plane runs here.
|
|
||||||
- **Docker-backed Orb sandboxes**: Docker is installed and access is
|
|
||||||
provisioned, but no Orb sandbox code is wired. This is a boundary
|
|
||||||
prepared for the Orb lane, not a working feature.
|
|
||||||
|
|
||||||
## Reproducibility
|
## Reproducibility
|
||||||
|
|
||||||
The deployment does not require the developer's MacBook to remain online.
|
The deployment does not require the developer's MacBook to remain online. Once bootstrap completes and `.env` is filled in:
|
||||||
Once bootstrap completes and `.env` is filled in:
|
|
||||||
|
|
||||||
1. Services run under systemd with `Restart=always`.
|
1. Services run under systemd with `Restart=always`.
|
||||||
2. Logs persist in journald.
|
2. Logs persist in journald.
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
# TAILSCALE_AUTHKEY — if set, configure Tailscale
|
# TAILSCALE_AUTHKEY — if set, configure Tailscale
|
||||||
# TAILSCALE_HOSTNAME — Tailscale hostname (default: zopu-runtime)
|
# TAILSCALE_HOSTNAME — Tailscale hostname (default: zopu-runtime)
|
||||||
#
|
#
|
||||||
# Installs: Docker Engine, Bun, clones the repo, runs bun install, builds the
|
# Installs: Docker Engine, Node.js 22, Bun, clones the repo, runs bun install, builds the
|
||||||
# daemon and agent, creates a non-root service user, installs systemd units,
|
# web app, daemon, and agent, creates a non-root service user, installs systemd
|
||||||
# and configures firewall/Tailscale defaults.
|
# units, and configures firewall/Tailscale defaults.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -105,18 +105,31 @@ fi
|
|||||||
systemctl enable --now docker
|
systemctl enable --now docker
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 3. Bun
|
# 3. Node.js 22 (required by the Flue Node target)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
NODE_MAJOR=$(node --version 2>/dev/null | sed -n 's/^v\([0-9][0-9]*\).*/\1/p')
|
||||||
|
if [[ -z "$NODE_MAJOR" || "$NODE_MAJOR" -lt 22 ]]; then
|
||||||
|
log "Installing Node.js 22..."
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh
|
||||||
|
bash /tmp/nodesource_setup.sh
|
||||||
|
apt-get install -y nodejs
|
||||||
|
else
|
||||||
|
log "Node.js already installed: $(node --version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. Bun
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if ! command -v bun &>/dev/null; then
|
if ! command -v bun &>/dev/null; then
|
||||||
log "Installing Bun..."
|
log "Installing Bun..."
|
||||||
curl -fsSL https://bun.sh/install | bash
|
curl -fsSL https://bun.sh/install | bash
|
||||||
ln -sf /root/.bun/bin/bun /usr/local/bin/bun
|
install -m 0755 /root/.bun/bin/bun /usr/local/bin/bun
|
||||||
else
|
else
|
||||||
log "Bun already installed: $(bun --version)"
|
log "Bun already installed: $(bun --version)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 4. Service user
|
# 5. Service user
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if ! id "$SERVICE_USER" &>/dev/null; then
|
if ! id "$SERVICE_USER" &>/dev/null; then
|
||||||
log "Creating service user: $SERVICE_USER"
|
log "Creating service user: $SERVICE_USER"
|
||||||
@@ -129,7 +142,7 @@ if ! id -nG "$SERVICE_USER" | grep -qw docker; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 5. Clone or update repository
|
# 6. Clone or update repository
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if [[ -d "$INSTALL_DIR/.git" ]]; then
|
if [[ -d "$INSTALL_DIR/.git" ]]; then
|
||||||
log "Repository exists at $INSTALL_DIR, fetching latest..."
|
log "Repository exists at $INSTALL_DIR, fetching latest..."
|
||||||
@@ -144,25 +157,28 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 5b. Hand ownership of the checkout to the service user
|
# 6b. Hand ownership of the checkout to the service user
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
log "Setting ownership of $INSTALL_DIR to $SERVICE_USER..."
|
log "Setting ownership of $INSTALL_DIR to $SERVICE_USER..."
|
||||||
chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR"
|
chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 6. Install dependencies and build
|
# 7. Install dependencies and build
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
log "Running bun install..."
|
log "Running bun install..."
|
||||||
run_as_service bun install
|
run_as_service bun install
|
||||||
|
|
||||||
log "Building daemon binary..."
|
log "Building web app..."
|
||||||
|
run_as_service bun run --cwd apps/web build
|
||||||
|
|
||||||
|
log "Validating daemon production build..."
|
||||||
run_as_service bun run build:daemon
|
run_as_service bun run build:daemon
|
||||||
|
|
||||||
log "Building agent service..."
|
log "Building agent service..."
|
||||||
run_as_service bun run build:agents
|
run_as_service bun run build:agents
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 7. Environment file
|
# 8. Environment file
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
ENV_FILE="$INSTALL_DIR/.env"
|
ENV_FILE="$INSTALL_DIR/.env"
|
||||||
if [[ ! -f "$ENV_FILE" ]]; then
|
if [[ ! -f "$ENV_FILE" ]]; then
|
||||||
@@ -179,17 +195,17 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 8. Persistent log directory
|
# 9. Persistent log directory
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
LOG_DIR="/var/log/zopu"
|
LOG_DIR="/var/log/zopu"
|
||||||
mkdir -p "$LOG_DIR"
|
mkdir -p "$LOG_DIR"
|
||||||
chown "$SERVICE_USER":"$SERVICE_USER" "$LOG_DIR"
|
chown "$SERVICE_USER":"$SERVICE_USER" "$LOG_DIR"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 9. Install systemd units (substitute placeholders)
|
# 10. Install systemd units (substitute placeholders)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
log "Installing systemd units..."
|
log "Installing systemd units..."
|
||||||
for unit in zopu-daemon.service zopu-agent.service \
|
for unit in zopu-web.service zopu-daemon.service zopu-agent.service \
|
||||||
zopu-health.timer zopu-health.service \
|
zopu-health.timer zopu-health.service \
|
||||||
zopu-docker-cleanup.timer zopu-docker-cleanup.service; do
|
zopu-docker-cleanup.timer zopu-docker-cleanup.service; do
|
||||||
SRC="$DEPLOY_DIR/systemd/$unit"
|
SRC="$DEPLOY_DIR/systemd/$unit"
|
||||||
@@ -205,7 +221,7 @@ done
|
|||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 10. Firewall (deny-by-default, explicit allow for Tailscale)
|
# 11. Firewall (deny-by-default, explicit allow for Tailscale)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
log "Configuring firewall..."
|
log "Configuring firewall..."
|
||||||
if ! ufw status 2>/dev/null | grep -q "Status: active"; then
|
if ! ufw status 2>/dev/null | grep -q "Status: active"; then
|
||||||
@@ -227,7 +243,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 11. Tailscale (optional)
|
# 12. Tailscale (optional)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if [[ -n "${TAILSCALE_AUTHKEY:-}" ]]; then
|
if [[ -n "${TAILSCALE_AUTHKEY:-}" ]]; then
|
||||||
log "Installing and configuring Tailscale..."
|
log "Installing and configuring Tailscale..."
|
||||||
@@ -249,7 +265,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 12. Disk-space monitoring cron
|
# 13. Disk-space monitoring cron
|
||||||
# /etc/cron.d format REQUIRES a username field.
|
# /etc/cron.d format REQUIRES a username field.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
log "Installing disk-space monitor (daily at 06:00)..."
|
log "Installing disk-space monitor (daily at 06:00)..."
|
||||||
@@ -265,7 +281,7 @@ echo ""
|
|||||||
echo "Next steps:"
|
echo "Next steps:"
|
||||||
echo " 1. Edit $ENV_FILE with real values"
|
echo " 1. Edit $ENV_FILE with real values"
|
||||||
echo " 2. Start services:"
|
echo " 2. Start services:"
|
||||||
echo " systemctl start zopu-daemon zopu-agent"
|
echo " systemctl start zopu-web zopu-daemon zopu-agent"
|
||||||
echo " 3. Enable health monitoring:"
|
echo " 3. Enable health monitoring:"
|
||||||
echo " systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer"
|
echo " systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer"
|
||||||
echo " 4. Verify health:"
|
echo " 4. Verify health:"
|
||||||
|
|||||||
@@ -17,10 +17,28 @@
|
|||||||
# reverse_proxy localhost:3583
|
# reverse_proxy localhost:3583
|
||||||
# }
|
# }
|
||||||
#
|
#
|
||||||
# --- Public deployment with automatic TLS (only if needed) ---
|
# --- Public deployment through Cloudflare Tunnel ---
|
||||||
#
|
#
|
||||||
# agent.example.invalid {
|
# Cloudflare terminates TLS and forwards these hosts to Caddy over loopback.
|
||||||
# reverse_proxy localhost:3583
|
#
|
||||||
|
# http://zopu.sai-onchain.me {
|
||||||
|
# bind 127.0.0.1
|
||||||
|
# reverse_proxy 127.0.0.1:13100
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# http://zopu-api.sai-onchain.me {
|
||||||
|
# bind 127.0.0.1
|
||||||
|
# reverse_proxy 127.0.0.1:3210
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# http://zopu-site.sai-onchain.me {
|
||||||
|
# bind 127.0.0.1
|
||||||
|
# reverse_proxy 127.0.0.1:3211
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# http://zopu-agent.sai-onchain.me {
|
||||||
|
# bind 127.0.0.1
|
||||||
|
# reverse_proxy 127.0.0.1:3583
|
||||||
# }
|
# }
|
||||||
|
|
||||||
# Default: do not ship an active Caddyfile. The file is documentation.
|
# Default: do not ship an active Caddyfile. The file is documentation.
|
||||||
|
|||||||
25
deploy/zopu-runtime/caddy/Caddyfile.production
Normal file
25
deploy/zopu-runtime/caddy/Caddyfile.production
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
admin 127.0.0.1:2019
|
||||||
|
auto_https off
|
||||||
|
http_port 8080
|
||||||
|
}
|
||||||
|
|
||||||
|
http://zopu.sai-onchain.me {
|
||||||
|
bind 127.0.0.1
|
||||||
|
reverse_proxy 127.0.0.1:13100
|
||||||
|
}
|
||||||
|
|
||||||
|
http://zopu-api.sai-onchain.me {
|
||||||
|
bind 127.0.0.1
|
||||||
|
reverse_proxy 127.0.0.1:3210
|
||||||
|
}
|
||||||
|
|
||||||
|
http://zopu-site.sai-onchain.me {
|
||||||
|
bind 127.0.0.1
|
||||||
|
reverse_proxy 127.0.0.1:3211
|
||||||
|
}
|
||||||
|
|
||||||
|
http://zopu-agent.sai-onchain.me {
|
||||||
|
bind 127.0.0.1
|
||||||
|
reverse_proxy 127.0.0.1:3583
|
||||||
|
}
|
||||||
13
deploy/zopu-runtime/cloudflared/config.production.yml
Normal file
13
deploy/zopu-runtime/cloudflared/config.production.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
tunnel: 9e54ac9c-c1a1-4583-be08-3b5f1acc7b65
|
||||||
|
credentials-file: /root/.cloudflared/9e54ac9c-c1a1-4583-be08-3b5f1acc7b65.json
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
- hostname: zopu.sai-onchain.me
|
||||||
|
service: http://127.0.0.1:8080
|
||||||
|
- hostname: zopu-api.sai-onchain.me
|
||||||
|
service: http://127.0.0.1:8080
|
||||||
|
- hostname: zopu-site.sai-onchain.me
|
||||||
|
service: http://127.0.0.1:8080
|
||||||
|
- hostname: zopu-agent.sai-onchain.me
|
||||||
|
service: http://127.0.0.1:8080
|
||||||
|
- service: http_status:404
|
||||||
32
deploy/zopu-runtime/convex/docker-compose.yml
Normal file
32
deploy/zopu-runtime/convex/docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
image: ghcr.io/get-convex/convex-backend:latest
|
||||||
|
container_name: zopu-convex
|
||||||
|
restart: unless-stopped
|
||||||
|
stop_grace_period: 10s
|
||||||
|
stop_signal: SIGINT
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3210:3210"
|
||||||
|
- "127.0.0.1:3211:3211"
|
||||||
|
volumes:
|
||||||
|
- zopu-convex-data:/convex/data
|
||||||
|
environment:
|
||||||
|
CONVEX_CLOUD_ORIGIN: ${CONVEX_CLOUD_ORIGIN}
|
||||||
|
CONVEX_SITE_ORIGIN: ${CONVEX_SITE_ORIGIN}
|
||||||
|
DISABLE_BEACON: "true"
|
||||||
|
DISABLE_METRICS_ENDPOINT: "true"
|
||||||
|
DOCUMENT_RETENTION_DELAY: "172800"
|
||||||
|
INSTANCE_NAME: ${CONVEX_INSTANCE_NAME:-zopu-production}
|
||||||
|
INSTANCE_SECRET: ${CONVEX_INSTANCE_SECRET:-}
|
||||||
|
REDACT_LOGS_TO_CLIENT: "true"
|
||||||
|
RUST_LOG: ${CONVEX_RUST_LOG:-info}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3210/version"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 12
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
zopu-convex-data:
|
||||||
|
name: zopu-convex-data
|
||||||
@@ -72,7 +72,10 @@ fi
|
|||||||
log "Running bun install..."
|
log "Running bun install..."
|
||||||
run_as_service bun install
|
run_as_service bun install
|
||||||
|
|
||||||
log "Building daemon..."
|
log "Building web app..."
|
||||||
|
run_as_service bun run --cwd apps/web build
|
||||||
|
|
||||||
|
log "Validating daemon production build..."
|
||||||
run_as_service bun run build:daemon
|
run_as_service bun run build:daemon
|
||||||
|
|
||||||
log "Building agent service..."
|
log "Building agent service..."
|
||||||
@@ -82,6 +85,7 @@ log "Restarting services..."
|
|||||||
systemctl restart zopu-daemon
|
systemctl restart zopu-daemon
|
||||||
sleep 3
|
sleep 3
|
||||||
systemctl restart zopu-agent
|
systemctl restart zopu-agent
|
||||||
|
systemctl restart zopu-web
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
@@ -94,6 +98,7 @@ if [[ -x "$HEALTH_SCRIPT" ]]; then
|
|||||||
err "Health check failed after rollback!"
|
err "Health check failed after rollback!"
|
||||||
err " journalctl -u zopu-daemon -n 50"
|
err " journalctl -u zopu-daemon -n 50"
|
||||||
err " journalctl -u zopu-agent -n 50"
|
err " journalctl -u zopu-agent -n 50"
|
||||||
|
err " journalctl -u zopu-web -n 50"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -77,17 +77,21 @@ fi
|
|||||||
log "Running bun install..."
|
log "Running bun install..."
|
||||||
run_as_service bun install
|
run_as_service bun install
|
||||||
|
|
||||||
log "Building daemon binary..."
|
log "Building web app..."
|
||||||
|
run_as_service bun run --cwd apps/web build
|
||||||
|
|
||||||
|
log "Validating daemon production build..."
|
||||||
run_as_service bun run build:daemon
|
run_as_service bun run build:daemon
|
||||||
|
|
||||||
log "Building agent service..."
|
log "Building agent service..."
|
||||||
run_as_service bun run build:agents
|
run_as_service bun run build:agents
|
||||||
|
|
||||||
# Graceful restart: daemon first (owns RivetKit engine), then agent
|
# Graceful restart: daemon first (owns RivetKit engine), then agent and web
|
||||||
log "Restarting services..."
|
log "Restarting services..."
|
||||||
systemctl restart zopu-daemon
|
systemctl restart zopu-daemon
|
||||||
sleep 3
|
sleep 3
|
||||||
systemctl restart zopu-agent
|
systemctl restart zopu-agent
|
||||||
|
systemctl restart zopu-web
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
@@ -100,6 +104,7 @@ if [[ -x "$HEALTH_SCRIPT" ]]; then
|
|||||||
err "Health check failed after update!"
|
err "Health check failed after update!"
|
||||||
err " journalctl -u zopu-daemon -n 50"
|
err " journalctl -u zopu-daemon -n 50"
|
||||||
err " journalctl -u zopu-agent -n 50"
|
err " journalctl -u zopu-agent -n 50"
|
||||||
|
err " journalctl -u zopu-web -n 50"
|
||||||
err "To rollback: ${INSTALL_DIR}/deploy/zopu-runtime/scripts/rollback.sh"
|
err "To rollback: ${INSTALL_DIR}/deploy/zopu-runtime/scripts/rollback.sh"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ Group=__SERVICE_USER__
|
|||||||
WorkingDirectory=__INSTALL_DIR__/packages/agents
|
WorkingDirectory=__INSTALL_DIR__/packages/agents
|
||||||
|
|
||||||
EnvironmentFile=__INSTALL_DIR__/.env
|
EnvironmentFile=__INSTALL_DIR__/.env
|
||||||
|
Environment=PORT=3583
|
||||||
|
|
||||||
# Flue build output: packages/agents/dist/server.mjs
|
# Flue's Node target requires Node built-ins such as node:sqlite.
|
||||||
# Listens on PORT (default 3583 per .env.template).
|
# Listens on the service-pinned port 3583.
|
||||||
ExecStart=/usr/local/bin/bun dist/server.mjs
|
ExecStart=/usr/bin/node dist/server.mjs
|
||||||
|
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ Group=__SERVICE_USER__
|
|||||||
WorkingDirectory=__INSTALL_DIR__
|
WorkingDirectory=__INSTALL_DIR__
|
||||||
|
|
||||||
EnvironmentFile=__INSTALL_DIR__/.env
|
EnvironmentFile=__INSTALL_DIR__/.env
|
||||||
|
Environment=HOME=/var/lib/zopu
|
||||||
|
StateDirectory=zopu
|
||||||
|
|
||||||
# RivetKit in-process engine: envoy mode (serverful).
|
# RivetKit in-process engine: envoy mode (serverful).
|
||||||
# registry.start() boots the native Rust sidecar and actor envoy.
|
# Run the Bun source entrypoint so AgentOS software assets retain their real
|
||||||
# RIVET_ENDPOINT defaults to http://localhost:6420 inside the daemon process.
|
# node_modules paths; Bun compiled executables do not embed .aospkg files.
|
||||||
ExecStart=__INSTALL_DIR__/apps/daemon/dist/code-daemon
|
ExecStart=/usr/local/bin/bun --env-file=__INSTALL_DIR__/.env apps/daemon/src/index.ts
|
||||||
|
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
|
|||||||
26
deploy/zopu-runtime/systemd/zopu-web.service
Normal file
26
deploy/zopu-runtime/systemd/zopu-web.service
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Zopu Web
|
||||||
|
After=network-online.target docker.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=__SERVICE_USER__
|
||||||
|
Group=__SERVICE_USER__
|
||||||
|
WorkingDirectory=__INSTALL_DIR__/apps/web
|
||||||
|
EnvironmentFile=__INSTALL_DIR__/.env
|
||||||
|
Environment=HOST=127.0.0.1
|
||||||
|
Environment=PORT=13100
|
||||||
|
ExecStart=/usr/local/bin/bun run start
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=zopu-web
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=full
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { cn } from "@code/ui/lib/utils";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
|
import type { MobileProjectView } from "./models";
|
||||||
|
|
||||||
|
export interface MobileProjectSelectProps {
|
||||||
|
readonly activeCount: number;
|
||||||
|
readonly className?: string;
|
||||||
|
readonly onProjectSelect: (projectId: string) => void;
|
||||||
|
readonly projects: readonly MobileProjectView[];
|
||||||
|
readonly selectedProjectId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MobileProjectSelect = ({
|
||||||
|
activeCount,
|
||||||
|
className,
|
||||||
|
onProjectSelect,
|
||||||
|
projects,
|
||||||
|
selectedProjectId,
|
||||||
|
}: MobileProjectSelectProps) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mt-0.5 flex min-w-0 items-center gap-1.5 text-[12px] leading-4 text-[#7c7772]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="size-1.5 shrink-0 rounded-full bg-[#64ad1f]" />
|
||||||
|
<label className="relative min-w-0">
|
||||||
|
<span className="sr-only">Connected project</span>
|
||||||
|
<select
|
||||||
|
aria-label="Connected project"
|
||||||
|
className="max-w-[132px] cursor-pointer appearance-none truncate bg-transparent pr-4 font-medium text-[#5d5955] outline-none focus-visible:ring-2 focus-visible:ring-[#315dc0]"
|
||||||
|
onChange={(event) => onProjectSelect(event.currentTarget.value)}
|
||||||
|
value={selectedProjectId ?? ""}
|
||||||
|
>
|
||||||
|
{projects.length === 0 ? (
|
||||||
|
<option value="">No connected project</option>
|
||||||
|
) : null}
|
||||||
|
{projects.map((project) => (
|
||||||
|
<option key={project.id} value={project.id}>
|
||||||
|
{project.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<ChevronDown
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute top-1/2 right-0 size-3 -translate-y-1/2"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<span aria-hidden="true">·</span>
|
||||||
|
<span className="shrink-0 tabular-nums">{activeCount} active</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -10,8 +10,10 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { FormEvent, MouseEvent } from "react";
|
import type { FormEvent, MouseEvent } from "react";
|
||||||
|
|
||||||
|
import { MobileProjectSelect } from "./mobile-project-select";
|
||||||
import { MobileSettingsPanel } from "./mobile-settings-panel";
|
import { MobileSettingsPanel } from "./mobile-settings-panel";
|
||||||
import type {
|
import type {
|
||||||
|
MobileProjectView,
|
||||||
MobileWorkspaceView,
|
MobileWorkspaceView,
|
||||||
MobileWorkUnitTone,
|
MobileWorkUnitTone,
|
||||||
MobileWorkUnitView,
|
MobileWorkUnitView,
|
||||||
@@ -41,12 +43,18 @@ const dateLabel = new Intl.DateTimeFormat("en", {
|
|||||||
interface WorkStackHeaderProps {
|
interface WorkStackHeaderProps {
|
||||||
readonly activeCount: number;
|
readonly activeCount: number;
|
||||||
readonly onOpenSettings?: () => void;
|
readonly onOpenSettings?: () => void;
|
||||||
|
readonly onProjectSelect: (projectId: string) => void;
|
||||||
|
readonly projects: readonly MobileProjectView[];
|
||||||
|
readonly selectedProjectId?: string;
|
||||||
readonly userInitials: string;
|
readonly userInitials: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WorkStackHeader = ({
|
const WorkStackHeader = ({
|
||||||
activeCount,
|
activeCount,
|
||||||
onOpenSettings,
|
onOpenSettings,
|
||||||
|
onProjectSelect,
|
||||||
|
projects,
|
||||||
|
selectedProjectId,
|
||||||
userInitials,
|
userInitials,
|
||||||
}: WorkStackHeaderProps) => (
|
}: WorkStackHeaderProps) => (
|
||||||
<header className="flex h-[96px] shrink-0 items-center bg-white px-4">
|
<header className="flex h-[96px] shrink-0 items-center bg-white px-4">
|
||||||
@@ -57,10 +65,12 @@ const WorkStackHeader = ({
|
|||||||
<h1 className="text-[20px] leading-6 font-semibold tracking-[-0.03em] text-[#0b0b0a]">
|
<h1 className="text-[20px] leading-6 font-semibold tracking-[-0.03em] text-[#0b0b0a]">
|
||||||
Zopu
|
Zopu
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-0.5 flex items-center gap-2 text-[13px] leading-4 text-[#7c7772]">
|
<MobileProjectSelect
|
||||||
<span className="size-1.5 rounded-full bg-[#64ad1f]" />
|
activeCount={activeCount}
|
||||||
{activeCount} active work units
|
onProjectSelect={onProjectSelect}
|
||||||
</p>
|
projects={projects}
|
||||||
|
selectedProjectId={selectedProjectId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
aria-label="Open settings"
|
aria-label="Open settings"
|
||||||
@@ -291,6 +301,7 @@ export interface MobileWorkStackScreenProps {
|
|||||||
readonly onComposerChange: (value: string) => void;
|
readonly onComposerChange: (value: string) => void;
|
||||||
readonly onComposerSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
readonly onComposerSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||||
readonly onOpenUnit: (workUnitId: string) => void;
|
readonly onOpenUnit: (workUnitId: string) => void;
|
||||||
|
readonly onProjectSelect: (projectId: string) => void;
|
||||||
readonly onRestoreChecked: () => void;
|
readonly onRestoreChecked: () => void;
|
||||||
readonly onSettingsClose: () => void;
|
readonly onSettingsClose: () => void;
|
||||||
readonly onSettingsOpen: () => void;
|
readonly onSettingsOpen: () => void;
|
||||||
@@ -314,6 +325,7 @@ export const MobileWorkStackScreen = ({
|
|||||||
onComposerChange,
|
onComposerChange,
|
||||||
onComposerSubmit,
|
onComposerSubmit,
|
||||||
onOpenUnit,
|
onOpenUnit,
|
||||||
|
onProjectSelect,
|
||||||
onRestoreChecked,
|
onRestoreChecked,
|
||||||
onSettingsClose,
|
onSettingsClose,
|
||||||
onSettingsOpen,
|
onSettingsOpen,
|
||||||
@@ -339,6 +351,9 @@ export const MobileWorkStackScreen = ({
|
|||||||
<WorkStackHeader
|
<WorkStackHeader
|
||||||
activeCount={data.activeCount}
|
activeCount={data.activeCount}
|
||||||
onOpenSettings={onSettingsOpen}
|
onOpenSettings={onSettingsOpen}
|
||||||
|
onProjectSelect={onProjectSelect}
|
||||||
|
projects={data.projects}
|
||||||
|
selectedProjectId={data.selectedProjectId}
|
||||||
userInitials={userInitials}
|
userInitials={userInitials}
|
||||||
/>
|
/>
|
||||||
<main className="relative min-h-0 flex-1 overflow-hidden bg-[#f7f8f5] px-5 pt-[26px]">
|
<main className="relative min-h-0 flex-1 overflow-hidden bg-[#f7f8f5] px-5 pt-[26px]">
|
||||||
|
|||||||
Reference in New Issue
Block a user