diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9175688 --- /dev/null +++ b/.env.example @@ -0,0 +1,12 @@ +# Copy this file to .env and fill in your values, then run `docker compose up`. +# .env is gitignored — never commit your real key. + +# Required: Demeter utxorpc API key for the mainnet endpoint in deploy/tracker.toml. +# Get a free key at https://demeter.run. The tracker fails to authenticate without it. +DMTR_API_KEY= + +# Optional: host port the dashboard is published on (container always listens on 3000). +# PORT=3000 + +# Optional: tracker log level (error | warn | info | debug | trace). +# RUST_LOG=info diff --git a/.github/workflows/docker-dashboard.yml b/.github/workflows/docker-dashboard.yml new file mode 100644 index 0000000..6a50ab1 --- /dev/null +++ b/.github/workflows/docker-dashboard.yml @@ -0,0 +1,37 @@ +name: Publish dashboard image to GHCR + +permissions: + contents: read + packages: write + +on: + workflow_dispatch: {} + +jobs: + build: + name: Build and push multi-arch image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v7 + with: + context: ./frontend + platforms: linux/amd64,linux/arm64 + push: true + tags: ghcr.io/tx3-lang/dashboard-frontend:${{ github.sha }} diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml index bbb7892..1cb44d8 100644 --- a/.github/workflows/frontend-ci.yml +++ b/.github/workflows/frontend-ci.yml @@ -22,15 +22,17 @@ jobs: working-directory: ./frontend/ steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v6 with: - version: 10 + # Version comes from the packageManager field in frontend/package.json. + # (Action inputs don't honor working-directory, so point at the file.) + package_json_file: frontend/package.json - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '24' cache: 'pnpm' @@ -49,4 +51,4 @@ jobs: run: pnpm run check - name: Run tests - run: pnpm test --run \ No newline at end of file + run: pnpm test --run diff --git a/README.md b/README.md index 8501e22..bbafa94 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,21 @@ Rel(dashboard, utxorpc, "Subscribes to tx stream", "gRPC / TLS") Rel(operator, registry, "Pulls TII once at vendor time", "GraphQL") ``` -## Quick start +## Run with Docker -You need a clone of [`tx3-lang/tx3-lift`](https://github.com/tx3-lang/tx3-lift) as a sibling directory and a Demeter `utxorpc` API key. +The fastest way to get the full stack running. You need Docker (with Compose v2) and a Demeter `utxorpc` API key ([free sign-up at demeter.run](https://demeter.run)). + +```bash +cp .env.example .env +# Open .env and set DMTR_API_KEY=dmtr_... +docker compose up +``` + +Open . The tracker streams Cardano mainnet and writes `tracker.db` into a Docker volume; the dashboard reads from it. Mainnet matches for the configured protocols typically appear within a few minutes. + +## Quick start (from source) + +You need a clone of [`tx3-lang/tx3-lift`](https://github.com/tx3-lang/tx3-lift) as a sibling directory, Rust stable, Node 24, and a Demeter `utxorpc` API key. ```bash # Once: clone tx3-lift sibling @@ -44,7 +56,9 @@ Open . See [`docs/running.md`](docs/running.md) for prere ## What the demo shows -The committed configuration tracks the [`buidler-fest/ticketing-2026`](protocols/buidler-fest/ticketing-2026.tii) protocol on Cardano preview. It defines a single transaction name (`buy_ticket`) with three parties (`buyer`, `treasury`, `issuer`) and roughly 80 real on-chain matches. The matches list at `/` shows every `buy_ticket` the tracker has seen, newest first; clicking through to `/txs/` shows the parties and their addresses for that transaction. +The Docker Compose stack (`deploy/tracker.toml`) tracks five DeFi protocols on **Cardano mainnet**: Indigo, VyFi, Bodega Market, Fluid Aquarium, and Strike Staking. Matching uses `mode = "best"`, which keeps only the highest-ranked candidate when a single transaction matches multiple TIIs. The matches list at `/` shows every matched transaction the tracker has seen, newest first; clicking through to `/txs/` shows the lifted parties and their addresses for that transaction. + +The `protocols/buidler-fest/` directory contains the earlier preview demo TII for reference, but the active configuration targets mainnet. ## Documentation @@ -57,14 +71,23 @@ The committed configuration tracks the [`buidler-fest/ticketing-2026`](protocols ``` dashboard/ ├── README.md # this file -├── tracker.toml # config consumed by the external tracker +├── docker-compose.yml # tracker + dashboard via Docker +├── .env.example # DMTR_API_KEY and optional PORT/RUST_LOG +├── tracker.toml # bare-metal tracker config (api_key commented out) +├── deploy/ +│ └── tracker.toml # Docker tracker config (mainnet, absolute paths) ├── docs/ │ ├── architecture.md │ ├── access-patterns.md │ └── running.md ├── protocols/ +│ ├── indigo.tii +│ ├── vyfi.tii +│ ├── bodega_market.tii +│ ├── fluid-aquarium.tii +│ ├── strike-staking.tii │ └── buidler-fest/ -│ └── ticketing-2026.tii # vendored TII for the demo protocol +│ └── ticketing-2026.tii # earlier preview demo (reference only) └── frontend/ # TanStack Start SSR app ├── package.json └── src/ diff --git a/deploy/tracker.toml b/deploy/tracker.toml new file mode 100644 index 0000000..eb9f752 --- /dev/null +++ b/deploy/tracker.toml @@ -0,0 +1,51 @@ +# Tracker configuration for the Docker Compose stack. +# +# Paths are ABSOLUTE on purpose: the tracker resolves tii_path relative to its +# working directory, and this file is consumed inside the tracker container +# where ./protocols and ./tracker.db do not exist. The compose mounts: +# - ./protocols -> /protocols (read-only bind mount, TII files) +# - ./deploy/tracker.toml -> /etc/tracker/tracker.toml (read-only bind mount) +# - tracker-data -> /data (named volume, shared with the dashboard) +# +# No api_key here: the key is supplied via the DMTR_API_KEY environment variable +# (set in .env). The tracker falls back to DMTR_API_KEY when api_key is absent. + +[upstream] +endpoint = "https://cardano-mainnet.utxorpc-m1.demeter.run" +intersect = "tip" + +# No upstream filter — every mainnet tx is forwarded; the fingerprint pre-filter +# plus the structural matcher decide which ones count. + +[storage] +database_path = "/data/tracker.db" + +# Several mainnet protocols are tracked at once, so keep only the highest-ranked +# candidate per transaction to avoid one tx matching multiple unrelated TIIs. +[matching] +mode = "best" + +[[sources]] +name = "indigo-mainnet" +tii_path = "/protocols/indigo.tii" +profile = "mainnet" + +[[sources]] +name = "vyfi-mainnet" +tii_path = "/protocols/vyfi.tii" +profile = "mainnet" + +[[sources]] +name = "bodega-mainnet" +tii_path = "/protocols/bodega_market.tii" +profile = "mainnet" + +[[sources]] +name = "fluid-aquarium-mainnet" +tii_path = "/protocols/fluid-aquarium.tii" +profile = "mainnet" + +[[sources]] +name = "strike-staking-mainnet" +tii_path = "/protocols/strike-staking.tii" +profile = "mainnet" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f593efd --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,72 @@ +# Docker Compose stack: tracker (writer) + dashboard (reader). +# +# Volume strategy: +# - Bind mounts (./protocols, ./deploy/tracker.toml) give the tracker read-only +# access to host-side config and TII files that operators may want to update +# without rebuilding the image. +# - Named volume (tracker-data) is the shared SQLite database. Using a named +# volume instead of a bind mount avoids UID/GID mismatches between the Rust +# and Node containers and lets Docker manage the lifecycle of the data. +# +# Startup ordering: +# The dashboard opens the DB with fileMustExist: true, so it must not start +# until the tracker has written at least one batch and created tracker.db. +# The healthcheck on the tracker service (test -f /data/tracker.db) gates the +# dashboard via depends_on: condition: service_healthy. + +services: + + tracker: + # Images are tagged with the git SHA by the "Publish tracker image to GHCR" + # workflow. Pin a published SHA here and bump it deliberately after each publish. + image: ghcr.io/tx3-lang/tx3-lift-tracker:GIT_SHA + restart: unless-stopped + environment: + # API key for the Demeter utxorpc mainnet endpoint — required. + DMTR_API_KEY: ${DMTR_API_KEY} + # Log level; defaults to info so operators don't have to set it explicitly. + RUST_LOG: ${RUST_LOG:-info} + volumes: + # Read-only bind mounts: config files that live in this repo. + - ./protocols:/protocols:ro + - ./deploy/tracker.toml:/etc/tracker/tracker.toml:ro + # Named volume: SQLite database shared with the dashboard. + - tracker-data:/data + # Pass the config path as the sole argument to the tracker binary (ENTRYPOINT). + command: ["/etc/tracker/tracker.toml"] + healthcheck: + # The tracker creates tracker.db on its first cursor advance, which can take + # up to ~30 s on a cold start before the upstream delivers the first block. + # We wait for the file to exist before considering the service healthy. + test: ["CMD-SHELL", "test -f /data/tracker.db"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 30s + + dashboard: + # Images are tagged with the git SHA by the "Publish dashboard image to GHCR" + # workflow. Pin a published SHA here and bump it deliberately after each publish. + image: ghcr.io/tx3-lang/dashboard-frontend:GIT_SHA + restart: unless-stopped + environment: + # Path inside the container where the dashboard opens the SQLite database. + TRACKER_DB_PATH: /data/tracker.db + # The container always listens on 3000 (set via the image's ENV PORT). + # The host-side port is configurable below via the PORT variable. + volumes: + # Same named volume as the tracker. NOT read-only: SQLite WAL mode requires + # the reader to access -shm/-wal sidecar files, which can fail on a :ro mount. + - tracker-data:/data + ports: + # Publish on the host port set by PORT (default 3000); container port is fixed. + - "${PORT:-3000}:3000" + depends_on: + tracker: + # Do not start the dashboard until the tracker has created tracker.db. + condition: service_healthy + +volumes: + # Named volume that holds tracker.db (and its WAL sidecar files). + # Docker manages its lifecycle; use `docker compose down -v` to remove it. + tracker-data: diff --git a/docs/running.md b/docs/running.md index d19536f..580cbe8 100644 --- a/docs/running.md +++ b/docs/running.md @@ -1,28 +1,66 @@ # Running the dashboard -This guide walks you through running the tx3 Dashboard end-to-end against the committed `buidler-fest/ticketing-2026` demo on Cardano preview. +This guide walks you through running the tx3 Dashboard end-to-end against the mainnet DeFi demo (Indigo, VyFi, Bodega, Fluid Aquarium, and Strike on Cardano mainnet). ## Prerequisites You'll need: -- **Rust stable** (for building and running the tracker from `tx3-lang/tx3-lift`). -- **Node 24** (the version Nitro and TanStack Start are tested against in CI). -- **pnpm 10** — install with `npm install -g pnpm@10` or `corepack enable`. -- **A sibling clone of [`tx3-lang/tx3-lift`](https://github.com/tx3-lang/tx3-lift)** at `../tx3-lift` (relative to this repo). The dashboard does not embed the tracker; you run the tracker binary from that clone as a sidecar. -- **A `utxorpc` endpoint and API key.** The committed `tracker.toml` points at Demeter's Cardano preview endpoint; sign up at [demeter.run](https://demeter.run) for a free `dmtr_…` key. +- **Rust stable** (for building and running the tracker from `tx3-lang/tx3-lift`) — only required for the from-source path. +- **Node 24** (the version Nitro and TanStack Start are tested against in CI) — only required for the from-source path. +- **pnpm 11** — install with `corepack enable` (honours the `packageManager` field) or `npm install -g pnpm@11` — only required for the from-source path. +- **A sibling clone of [`tx3-lang/tx3-lift`](https://github.com/tx3-lang/tx3-lift)** at `../tx3-lift` (relative to this repo) — only required for the from-source path. The dashboard does not embed the tracker; you run the tracker binary from that clone as a sidecar. +- **A Demeter `utxorpc` API key.** Sign up at [demeter.run](https://demeter.run) for a free `dmtr_…` key. The recommended way to supply the key is the `DMTR_API_KEY` environment variable — you do not need to edit `tracker.toml` (the committed file keeps `api_key` commented out for this reason). + +## Running with Docker + +The simplest way to run the full system is with Docker Compose. You need: + +- **Docker with Compose v2** (`docker compose` as a subcommand, not `docker-compose`). +- **A Demeter `utxorpc` API key** (see Prerequisites above). + +```bash +# 1. Copy the example env file and fill in your key. +cp .env.example .env +# Open .env and set DMTR_API_KEY=dmtr_... + +# 2. Start the stack. +docker compose up +# Add -d to detach: docker compose up -d + +# 3. Open the dashboard. +# http://localhost:3000 +``` + +> **Pin an image SHA first.** Images are published to GHCR tagged with the git +> SHA (no `latest`). `docker-compose.yml` ships with a `GIT_SHA` placeholder — +> before the first run, replace it with a published SHA (run the "Publish … to +> GHCR" workflows to produce images), or build the images locally and tag them +> to match. + +### How it works + +The `tracker` service reads `deploy/tracker.toml` and the TII files from `protocols/` (both bind-mounted read-only into the container). It writes `tracker.db` into a named Docker volume (`tracker-data`). The `dashboard` service mounts the same named volume and reads the database. The dashboard waits for the tracker's healthcheck — which checks that `/data/tracker.db` exists — before starting, so you never see a `SQLITE_CANTOPEN` crash from a race at startup. + +### Docker troubleshooting + +- **Images not found** — the images are pulled from `ghcr.io/tx3-lang/tx3-lift-tracker` and `ghcr.io/tx3-lang/dashboard-frontend`, tagged with the git SHA. `docker-compose.yml` pins a specific SHA; make sure it points at a published tag. If `docker compose pull` fails, check the SHA is published and the packages are public in the GitHub org. +- **Named volume on a network filesystem** — SQLite WAL mode (`-wal` / `-shm` sidecar files) is not safe on NFS or other network-backed filesystems. The `tracker-data` named volume must reside on a local filesystem. Docker Desktop on macOS and Linux with the default local volume driver both satisfy this requirement. +- **Empty list at `/`** — the tracker needs to scan the tip of mainnet and find a matching transaction before the dashboard has anything to show. Mainnet matches for the configured protocols typically appear within a few minutes. Check `docker compose logs tracker` to confirm blocks are flowing in. +- **Stopping and data lifecycle** — `docker compose down` stops the containers but keeps the `tracker-data` volume (the database is preserved). `docker compose down -v` removes the volume and wipes all stored matches. ## Environment variables | Variable | Used by | Required | Default | Purpose | |----------|---------|----------|---------|---------| -| `DMTR_API_KEY` | Tracker | Yes | — | Demeter API key for the configured `utxorpc` endpoint. The tracker fails to start without it. | -| `TRACKER_DB_PATH` | Dashboard | No | `./tracker.db` | Path the dashboard opens read-only. Leave unset to read the file the tracker writes alongside `tracker.toml`. | +| `DMTR_API_KEY` | Tracker | Yes | — | Demeter API key for the configured `utxorpc` endpoint. Supply via `.env` for Docker, or export it in your shell for bare-metal. The tracker reads this variable directly when `api_key` is absent from `tracker.toml`; no wrapper script is needed. | +| `TRACKER_DB_PATH` | Dashboard | No | `./tracker.db` | Path the dashboard opens read-only. Leave unset to read the file the tracker writes alongside `tracker.toml`. In the Docker stack this is set to `/data/tracker.db` inside the container. | | `RUST_LOG` | Tracker | No | (warn) | Log level for the tracker binary. `info` is comfortable for first-run; `debug` for protocol-level debugging. | +| `PORT` | Dashboard | No | `3000` | Host port the dashboard is published on. In Docker the container always listens on 3000; only the host-side binding uses this variable. | -## First run +## Running from source -You'll need two terminals. From a fresh clone of this repo: +If you prefer to build from source (or are developing the tracker or dashboard itself), you'll need two terminals. From a fresh clone of this repo: ```bash # Once: clone the tx3-lift sibling @@ -45,6 +83,8 @@ pnpm dev Visit . The tracker writes `dashboard/tracker.db` (plus its `-wal` / `-shm` companion files); the dashboard reads from the same file. New matches appear after a page reload. +> **API key**: supply `DMTR_API_KEY` as an environment variable (shown above). Do not add it to `tracker.toml` — the committed file keeps `api_key` commented out so the key is never accidentally committed. + ## Production build For an operator-managed deployment without `vite dev`. From the repo root: @@ -64,7 +104,7 @@ Set `TRACKER_DB_PATH` if `tracker.db` lives outside the working directory. The N The dashboard renders "No matches yet — confirm the tracker is running." If you're seeing it indefinitely: -- Confirm the tracker terminal shows `Apply` events flowing in (or, on preview, that the protocol's policy filter actually matches recent on-chain activity). +- Confirm the tracker terminal shows `Apply` events flowing in; on mainnet, confirmed matches for the tracked protocols typically appear within minutes. - Confirm `tracker.db` exists in the dashboard working directory and is non-empty: `sqlite3 tracker.db 'SELECT COUNT(*) FROM matches;'`. - Reload the page — the MVP does not auto-refresh. @@ -93,12 +133,11 @@ If the upstream chain rolls back past a slot the tracker had recorded, the track ## Tested with - `tx3-lang/tx3-lift` tracker commit: `04d0b90` (`docs: add integration test report (#8)`) -- Node 24, pnpm 10, Rust stable. +- Node 24, pnpm 11, Rust stable. ## Deferred deployment polish These are out of scope for M3 and tracked for follow-up iterations: -- **Docker compose** stack that runs both the tracker and the dashboard with a shared volume for `tracker.db`. - **Service-manager units** (systemd / launchd / pm2 templates) committed alongside the repo for one-shot operator install. - **Postgres backend** for the tracker (1–3 days upstream PR) plus the dashboard-side dialect swap (~half a day) — see [`architecture.md` § Forward-looking](architecture.md#forward-looking-postgres). diff --git a/docs/superpowers/plans/2026-06-16-dashboard-docker-compose.md b/docs/superpowers/plans/2026-06-16-dashboard-docker-compose.md new file mode 100644 index 0000000..7b14cba --- /dev/null +++ b/docs/superpowers/plans/2026-06-16-dashboard-docker-compose.md @@ -0,0 +1,108 @@ +# Dashboard Docker image + compose stack — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Publish a Docker image of the dashboard to GHCR (multi-arch) and ship a `docker-compose.yml` that runs the complete monitoring system (tracker + dashboard) from a clone with only `DMTR_API_KEY` set. + +**Architecture:** A multi-stage Node image builds the Nitro SSR bundle and runs it in a slim runtime. A compose file wires the published tracker image and the dashboard image with the correct volume split — bind mounts (read-only) for `protocols/` and a Docker-specific `deploy/tracker.toml`, a named volume (read-write) for the shared `tracker.db` — and orders startup with a tracker healthcheck. + +**Tech Stack:** Node 24, pnpm, TanStack Start / Nitro, better-sqlite3, Docker Compose, Docker buildx, GitHub Actions, GHCR. + +**Spec:** `docs/superpowers/specs/2026-06-16-dashboard-docker-compose-design.md` + +**Cross-repo dependency:** Consumes `ghcr.io/tx3-lang/tracker` and the tracker's `DMTR_API_KEY` env fallback (tx3-lift plan, Task 1). During development, build the tracker image locally; the committed compose pins the published image. + +**Conventions for this plan:** Per the team's code-free-plan convention, steps state intent, exact files, and verification commands/expected output — not source content. Authoring follows the spec sections referenced in each task. + +--- + +### Task 1: Dashboard Dockerfile + +**Files:** +- Create: `frontend/Dockerfile` +- Create: `frontend/.dockerignore` + +- [ ] **Step 1: Author the multi-stage Dockerfile** per spec §4.1: builder on `node:24` with the toolchain to compile `better-sqlite3`'s native binding, running `pnpm install` then `pnpm build` to produce `.output`; runtime on `node:24-slim` running `node .output/server/index.mjs`; same base/arch across stages; env `TRACKER_DB_PATH=/data/tracker.db`, `PORT=3000`. `.dockerignore` excludes `node_modules`, `.output`, `*.db*`, `.git`. +- [ ] **Step 2: Build the image** — `docker build -t dashboard:dev frontend`. Expected: success. +- [ ] **Step 3: Run against a seeded DB** — create a throwaway `tracker.db` (copy an existing one or let a local tracker create one), mount it at `/data/tracker.db`, run the container, hit `http://localhost:3000`. Expected: page renders (matches list or the "No matches yet" empty state) — proves the native binding loads in the slim runtime and `fileMustExist` is satisfied. +- [ ] **Step 4: Commit.** + +--- + +### Task 2: Docker-specific tracker config + +**Files:** +- Create: `deploy/tracker.toml` + +- [ ] **Step 1: Author `deploy/tracker.toml`** per spec §4.3 + §4.4: absolute `database_path = /data/tracker.db`; mainnet `[[sources]]` with absolute `tii_path = /protocols/.tii` for the chosen mainnet set (the five TIIs already in `protocols/`); keep the mainnet `endpoint`; **no** `api_key`; decide and set `[matching] mode` (`best` to mitigate over-matching across multiple protocols, per spec §4.4). +- [ ] **Step 2: Validate the referenced TIIs exist** — confirm each `tii_path` maps to a file present in `protocols/` (so the bind mount resolves). Expected: all present. +- [ ] **Step 3: Commit.** + +--- + +### Task 3: Environment template + +**Files:** +- Create: `.env.example` + +- [ ] **Step 1: Author `.env.example`** with `DMTR_API_KEY=` (required) and optional `PORT` / `RUST_LOG`, each with a short comment. +- [ ] **Step 2:** Confirm `.env` is git-ignored (add to `.gitignore` if not) so a real key never gets committed. +- [ ] **Step 3: Commit.** + +--- + +### Task 4: docker-compose.yml + +**Files:** +- Create: `docker-compose.yml` (repo root) + +- [ ] **Step 1: Author the compose** per spec §4.2: service `tracker` (`ghcr.io/tx3-lang/tracker`) with bind mounts `./protocols:/protocols:ro` and `./deploy/tracker.toml:/etc/tracker/tracker.toml:ro`, named volume `tracker-data:/data`, `DMTR_API_KEY` from env, command pointing at the mounted config, and a healthcheck `test -f /data/tracker.db` with enough retries for the first cursor advance; service `dashboard` (`ghcr.io/tx3-lang/dashboard`) with `tracker-data:/data`, `TRACKER_DB_PATH=/data/tracker.db`, `ports: 3000:3000`, `depends_on: tracker (condition: service_healthy)`; declare the `tracker-data` named volume. +- [ ] **Step 2: Validate config** — `docker compose config`. Expected: resolves with no errors; variables interpolate. +- [ ] **Step 3: End-to-end run** — build/pull the tracker image locally, `cp .env.example .env`, set a real `DMTR_API_KEY`, `docker compose up`. Expected: tracker becomes healthy after it creates the DB; dashboard starts only after (no `SQLITE_CANTOPEN`); `http://localhost:3000` shows real mainnet matches (spec D2, D3, B1). +- [ ] **Step 4: Restart/persistence check** — `docker compose down` then `up`; expected: `tracker.db` persists in the named volume and matches survive. +- [ ] **Step 5: Commit.** + +--- + +### Task 5: CI workflow — publish dashboard image to GHCR + +**Files:** +- Create: `.github/workflows/docker-dashboard.yml` + +- [ ] **Step 1: Author the workflow** per spec §3/§4.1: trigger on tag/release; buildx; GHCR login with workflow token; build `linux/amd64,linux/arm64` from `frontend/`; push to `ghcr.io/tx3-lang/dashboard` with tags = version, SHA, `latest`; `permissions: packages: write`. +- [ ] **Step 2: Validate syntax** — `actionlint .github/workflows/docker-dashboard.yml`. Expected: no errors. +- [ ] **Step 3: Dry-run** — `docker buildx build --platform linux/amd64,linux/arm64 frontend` (no push). Expected: both arches build. +- [ ] **Step 4: Commit.** +- [ ] **Step 5: Post-merge verification (manual):** push a tag, confirm the image publishes and the GHCR package is **public** (spec D1). + +--- + +### Task 6: Docs + demo alignment + secret cleanup + +**Files:** +- Modify: `docs/running.md` (add "Running with Docker") +- Modify: `README.md` (point quickstart at the compose; align demo-protocol claim to mainnet) +- Modify: `tracker.toml` (remove the inline `api_key`; align active sources with the mainnet demo) + +- [ ] **Step 1: Grep for committed secrets** — `git grep -nE 'api_key\s*=\s*"(utxorpc|dmtr_)'`. Record hits. +- [ ] **Step 2:** Remove the inline `api_key` from `tracker.toml`; re-grep, expected: no hits. (The key is in history and must be **rotated** out-of-band — note in the PR.) +- [ ] **Step 3:** Add a "Running with Docker" section to `docs/running.md` per spec §4.6: prerequisites (Docker + Demeter key), the clone → `.env` → `docker compose up` quickstart, the bind-mount-vs-named-volume model, and troubleshooting (empty list, startup race, WAL must be a local filesystem, where the DB lives). +- [ ] **Step 4:** Align README + `tracker.toml` so the documented demo matches the mainnet compose (spec §4.4 doc cleanup). +- [ ] **Step 5: Verify** the docs by following them from a clean clone (the Task 4 run already exercised the path). Expected: clone-to-running works using only documented steps (spec D5). +- [ ] **Step 6: Commit.** + +--- + +### Task 7: Final verification & PR + +- [ ] **Step 1:** Re-run `docker compose config` and a full `docker compose up` smoke; confirm the dashboard shows live matches. +- [ ] **Step 2:** Walk spec acceptance criteria D1–D5; confirm each maps to a completed task (D1 has a manual post-merge step). +- [ ] **Step 3:** Open a PR from `docs/docker-deploy-spec` (or a fresh feature branch) summarising the image, the compose stack, the demo config, and the **key-rotation** action item. Note the dependency on the tracker plan's Task 1 (env fallback) and that the compose pins `ghcr.io/tx3-lang/tracker` once published. + +--- + +## Self-review notes + +- **Spec coverage:** §4.1→Task 1+5; §4.2→Task 4; §4.3→Task 2; §4.4→Task 2+6; §4.5→Task 3; §4.6→Task 6; §6/§7 (secret/startup)→Task 4+6; D1 manual step flagged. +- **Cross-repo:** Tasks 2/4 depend on the tracker plan's env fallback (Task 1 there) and published image; called out in the header and Task 7. +- **TDD note:** This subsystem is infra (Dockerfile, compose, CI, docs); verification is build-and-run against the acceptance criteria rather than unit tests. diff --git a/docs/superpowers/specs/2026-06-16-dashboard-docker-compose-design.md b/docs/superpowers/specs/2026-06-16-dashboard-docker-compose-design.md new file mode 100644 index 0000000..5320514 --- /dev/null +++ b/docs/superpowers/specs/2026-06-16-dashboard-docker-compose-design.md @@ -0,0 +1,158 @@ +# Dashboard Docker image + compose stack — design spec + +- **Date**: 2026-06-16 +- **Repo**: `tx3-lang/dashboard` +- **Milestone**: Catalyst — "Publicly available Docker images for installing the complete monitoring system" +- **Status**: design approved, plan pending +- **Companion spec**: `tx3-lang/tx3-lift` → `2026-06-16-tracker-docker-image-design.md` (the tracker image this stack consumes) + +--- + +> **Convention update (post-implementation):** to match the txpipe GHCR pattern, +> the published images are `ghcr.io/tx3-lang/dashboard-frontend` and +> `ghcr.io/tx3-lang/tx3-lift-tracker` (named `-`), tagged with the +> **git SHA only** (no semver / `latest`), and the publish workflows run on +> `workflow_dispatch`. The committed `docker-compose.yml` pins a published SHA. +> Inline references below to `ghcr.io/tx3-lang/{tracker,dashboard}` and to +> semver/`latest` tags predate this alignment. + +## 1. Context + +### 1.1 What we have + +- The dashboard is a pure TypeScript app (TanStack Start + Nitro SSR). It opens `tracker.db` **read-only** via `better-sqlite3` + Kysely inside Nitro server functions (`frontend/src/lib/db.ts`), with `fileMustExist: true` and WAL pragma. +- The tracker (external, `tx3-lang/tx3-lift`) is the writer. The two are **decoupled at the SQLite file**; WAL mode lets the reader and writer run concurrently. +- `TRACKER_DB_PATH` (default `./tracker.db`) selects the file the dashboard opens; `PORT` (default 3000) the bind port. +- `docs/running.md` already lists this exact work as deferred polish: *"Docker compose stack that runs both the tracker and the dashboard with a shared volume for tracker.db."* + +### 1.2 Catalyst requirement + +- **A** — Publicly available Docker images for installing the complete monitoring system. +- **A1** — The Docker images allow the monitoring system to be installed and run successfully **without additional configuration**. + +This spec covers the **dashboard image** and the **`docker-compose.yml`** that wires the dashboard and the (companion-spec) tracker image into the complete, runnable system. The compose stack is the operator-facing entry point and lives in this repo. + +### 1.3 Audience + +dApp builders / operators who want to stand up the full monitoring system with one command. + +--- + +## 2. Scope + +### 2.1 In scope + +1. A multi-stage **Docker image** for the dashboard, published to **GHCR** (`ghcr.io/tx3-lang/dashboard`), multi-arch (amd64 + arm64). +2. A **`docker-compose.yml`** orchestrating the tracker and dashboard with the correct volume topology and startup ordering. +3. A **Docker-specific tracker config** (`deploy/tracker.toml`) with absolute paths, so the bare-metal config stays untouched. +4. A **`.env.example`** carrying `DMTR_API_KEY` (and optional `PORT` / `RUST_LOG`). +5. A **"Running with Docker"** section in `docs/running.md`. +6. **Point the demo at mainnet** using the TIIs already in `protocols/`, and align the README/`tracker.toml` docs that disagree on what the demo tracks (§4.4). +7. **Secret hygiene**: remove the inline `api_key` from the committed `tracker.toml`. + +### 2.2 Out of scope (YAGNI) + +- The tracker image internals and its release binary (companion spec, tx3-lift). +- An all-in-one image bundling both processes. +- Postgres backend; the SSR-side dialect swap is a separate future task. +- systemd / pm2 units; dashboard auto-refresh. + +--- + +## 3. Decisions (locked during brainstorming) + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Deliverable shape | Two images + compose | Matches the documented "decoupled at SQLite" architecture; one process per container; each repo owns its image. | +| Compose location | This repo (`dashboard`) | Operator-facing entry point; `running.md` already promises it. | +| Registry | GHCR under `tx3-lang` | Consistent with the tracker image; public pulls, no extra credentials. | +| Architectures | amd64 + arm64 | Apple-Silicon operators. | +| `tracker.db` | **named volume**, `rw`, shared | Generated data, must persist and be the same inode for both processes (WAL). | +| protocols + config | **bind mount**, `ro` | Versioned input supplied by the host repo, not generated by the container. | +| Startup ordering | tracker healthcheck + dashboard `depends_on: service_healthy` | The dashboard opens with `fileMustExist: true`; it must start after the tracker creates the DB. | + +--- + +## 4. Component design + +### 4.1 Dashboard image + +Multi-stage build: + +- **Builder stage** on `node:24`: `pnpm install` (with the build tools needed to compile `better-sqlite3`'s native binding) then `pnpm build`, producing the Nitro `.output` bundle. +- **Runtime stage** on `node:24-slim`: copy `.output`, run `node .output/server/index.mjs`. Builder and runtime share the same base/arch so the native binding stays compatible. +- Environment: `TRACKER_DB_PATH=/data/tracker.db`, `PORT=3000`. + +### 4.2 `docker-compose.yml` + +Two services on a shared topology: + +- **`tracker`** (`ghcr.io/tx3-lang/tracker`): bind-mounts `./protocols` (`ro`) and `./deploy/tracker.toml` (`ro`), mounts the named volume `tracker-data` at `/data`, reads `DMTR_API_KEY` from `.env`, runs against the mounted config. Healthcheck: the DB file exists at `/data/tracker.db`. +- **`dashboard`** (`ghcr.io/tx3-lang/dashboard`): mounts the same `tracker-data` volume at `/data`, sets `TRACKER_DB_PATH=/data/tracker.db`, publishes `:3000`, and declares `depends_on: tracker (condition: service_healthy)`. + +The volume split is the crux: **bind mounts** carry host-sourced read-only input (protocols, config); the **named volume** carries the generated, shared `tracker.db`. WAL across the named volume works because both containers see the same inode on the same host filesystem — this must not be a network filesystem. + +### 4.3 Docker-specific tracker config (`deploy/tracker.toml`) + +The tracker resolves `tii_path` relative to its working directory, so the Docker config uses **absolute** paths to remove CWD ambiguity: + +- `database_path` → the named-volume path (`/data/tracker.db`). +- each `tii_path` → the bind-mount path (`/protocols/…`). + +This is a separate file from the bare-metal `tracker.toml` (which keeps relative paths), so neither setup breaks the other. It carries **no** `api_key`; the key arrives via `DMTR_API_KEY`, which depends on the tracker's env-fallback behaviour (companion spec §4.2). + +### 4.4 Demo-protocol selection (decided: mainnet) + +The compose demo tracks **mainnet** protocols, sourced directly from this repo's `protocols/`. The five mainnet TII files already live here — `indigo.tii`, `vyfi.tii`, `bodega_market.tii`, `fluid-aquarium.tii`, `strike-staking.tii` — alongside `buidler-fest/ticketing-2026.tii`. No vendoring step is required; the compose just bind-mounts `./protocols`. + +- `deploy/tracker.toml` enables the mainnet `[[sources]]` with absolute `tii_path`s under `/protocols/…`, keeps the mainnet `endpoint`, and ships **no** `api_key`. +- Mainnet with `intersect = "tip"` and no upstream filter produces real matches quickly (criterion B1), at the cost of higher bandwidth — acceptable for the demo. +- **Over-matching**: with several mainnet sources and no filter, one tx can match multiple TIIs (see `tx3-lift/issue-over-matching.md`). Consider `[matching] mode = "best"` in `deploy/tracker.toml` to keep only the highest-ranked candidate per tx, leaning on the anchor-gate / anchor-strength matching already landed in the tracker. The exact source list (all five vs a subset) and the matching mode are a planning detail. +- Minor doc cleanup: the committed `tracker.toml` and the README disagree on what the demo tracks (README says `buidler-fest`, `tracker.toml` enables `indigo`). Align the docs to the mainnet demo while we're here. + +### 4.5 `.env.example` + +`DMTR_API_KEY=` (required), with optional `PORT` and `RUST_LOG`. Committed; the operator copies it to `.env`. + +### 4.6 Documentation + +A "Running with Docker" section in `docs/running.md`: prerequisites (Docker + a Demeter key), the three-step quickstart, the volume model, and troubleshooting (empty list, startup race, where the DB lives). + +--- + +## 5. Operator experience (criterion A1) + +1. Clone this repo (brings the compose, `protocols/`, `deploy/tracker.toml`, `.env.example`). +2. `cp .env.example .env` and set `DMTR_API_KEY`. +3. `docker compose up`. +4. Open `http://localhost:3000`. + +The only required input is the Demeter API key — everything else is committed. This satisfies "run without additional configuration." + +--- + +## 6. Acceptance criteria + +| ID | Criterion | +|----|-----------| +| D1 | The dashboard image builds in CI for amd64 + arm64 and is published, public, on `ghcr.io/tx3-lang/dashboard`. | +| D2 | `docker compose up` with only `DMTR_API_KEY` set brings up both services; the dashboard starts after the DB exists (no `SQLITE_CANTOPEN`). | +| D3 | The dashboard at `:3000` shows real on-chain matches produced by the tracker (criterion B1). | +| D4 | No live `api_key` remains in any committed file. | +| D5 | `docs/running.md` lets a developer go from clone to running stack using only the documented steps. | + +--- + +## 7. Risks & open points + +- **Startup race**: handled by the tracker healthcheck (`test -f /data/tracker.db`) + `depends_on: service_healthy`; the healthcheck needs enough retries for the tracker's first cursor advance. +- **`better-sqlite3` native compatibility**: builder and runtime must share base/arch; verify the binding loads in the slim runtime. +- **WAL on a network filesystem**: breaks the writer/reader concurrency. The named volume must be local; documented. +- **Image availability ordering**: the compose references the published tracker image; during development, build the tracker image locally or pin a tag once the companion CI publishes it. + +--- + +## 8. Dependencies + +- Consumes `ghcr.io/tx3-lang/tracker` from the companion spec. +- Requires the tracker's **`DMTR_API_KEY` env fallback** (companion spec §4.2) so `deploy/tracker.toml` can ship without a secret. diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..0082a9e --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.output +*.db +*.db-shm +*.db-wal +.git +.env +.env.* diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..512745a --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,46 @@ +# Stage 1: builder — full node:24 (Debian) for native compilation +FROM node:24 AS builder + +WORKDIR /app + +# Build toolchain for the better-sqlite3 native addon. +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Enable corepack. The pnpm version is driven by the packageManager field in +# package.json and resolved on the first pnpm invocation below. +RUN corepack enable + +# Copy manifests first for layer caching. pnpm-workspace.yaml carries the +# allowBuilds allowlist that lets better-sqlite3 (and esbuild/msw) run their +# native/post-install build scripts under pnpm 10+/11; .npmrc carries repo +# install settings. +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ + +# Install dependencies (frozen lockfile). +RUN pnpm install --frozen-lockfile + +# Copy source and build the Nitro bundle. +COPY . . +RUN pnpm build + +# Stage 2: runtime — node:24-slim (same Debian family / arch → ABI-compatible binding) +FROM node:24-slim AS runtime + +WORKDIR /app + +ENV NODE_ENV=production +ENV TRACKER_DB_PATH=/data/tracker.db +ENV PORT=3000 + +EXPOSE 3000 + +# Copy the Nitro output bundle. Nitro traces better-sqlite3 — including its +# native .node binding — into .output/server/node_modules, so the bundle is +# self-contained; no separate node_modules copy is needed. +COPY --from=builder /app/.output ./.output + +CMD ["node", ".output/server/index.mjs"] diff --git a/frontend/package.json b/frontend/package.json index 815edae..675fcde 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,11 +17,11 @@ "@fontsource-variable/inter": "^5.2.8", "@tabler/icons-react": "^3.40.0", "@tailwindcss/vite": "^4.1.18", - "@tanstack/react-devtools": "latest", - "@tanstack/react-router": "latest", - "@tanstack/react-router-devtools": "latest", - "@tanstack/react-router-ssr-query": "latest", - "@tanstack/react-start": "latest", + "@tanstack/react-devtools": "^0.10.0", + "@tanstack/react-router": "^1.167.4", + "@tanstack/react-router-devtools": "^1.166.9", + "@tanstack/react-router-ssr-query": "^1.166.9", + "@tanstack/react-start": "^1.166.16", "@tanstack/router-plugin": "^1.132.0", "better-sqlite3": "^12.9.0", "class-variance-authority": "^0.7.1", @@ -40,7 +40,7 @@ "devDependencies": { "@biomejs/biome": "2.4.5", "@tailwindcss/typography": "^0.5.16", - "@tanstack/devtools-vite": "latest", + "@tanstack/devtools-vite": "^0.6.0", "@testing-library/dom": "^10.4.1", "@testing-library/react": "^16.3.0", "@types/better-sqlite3": "^7.6.13", @@ -60,5 +60,6 @@ "lightningcss", "better-sqlite3" ] - } + }, + "packageManager": "pnpm@11.7.0+sha512.19cc852c120c7125760f2443ee6be0ca5b40f9f50598de1a09a1f177503e010e57c23c77646e01e761de59bf874fb22a3398c33ab9691fc13eb946b6f0f4d620" } \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index bceb5eb..17f3d36 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -21,19 +21,19 @@ importers: specifier: ^4.1.18 version: 4.2.1(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) '@tanstack/react-devtools': - specifier: latest + specifier: ^0.10.0 version: 0.10.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11) '@tanstack/react-router': - specifier: latest + specifier: ^1.167.4 version: 1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-router-devtools': - specifier: latest + specifier: ^1.166.9 version: 1.166.9(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.4)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-router-ssr-query': - specifier: latest + specifier: ^1.166.9 version: 1.166.9(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.21(react@19.2.4))(@tanstack/react-router@1.167.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.167.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-start': - specifier: latest + specifier: ^1.166.16 version: 1.166.16(crossws@0.4.4(srvx@0.11.12))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) '@tanstack/router-plugin': specifier: ^1.132.0 @@ -85,7 +85,7 @@ importers: specifier: ^0.5.16 version: 0.5.19(tailwindcss@4.2.1) '@tanstack/devtools-vite': - specifier: latest + specifier: ^0.6.0 version: 0.6.0(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) '@testing-library/dom': specifier: ^10.4.1 diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml new file mode 100644 index 0000000..4f1f627 --- /dev/null +++ b/frontend/pnpm-workspace.yaml @@ -0,0 +1,11 @@ +# pnpm 10+/11 reads the build-script allowlist from this file (not from +# package.json's pnpm.onlyBuiltDependencies). Without it, pnpm refuses to run +# dependencies' native/post-install build scripts and fails the install with +# ERR_PNPM_IGNORED_BUILDS — most importantly, better-sqlite3 would then ship +# without its compiled native binding. +allowBuilds: + better-sqlite3: true + esbuild: true + msw: true + lightningcss: true +minimumReleaseAge: 2880