From 78888ea2f8c257ac3ef37a95c0de23607c2ad17e Mon Sep 17 00:00:00 2001 From: Alexandre Wavelet Date: Thu, 11 Jun 2026 02:53:25 +0200 Subject: [PATCH 01/11] chore: make run.sh executable Co-Authored-By: Claude Opus 4.8 (1M context) --- run.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 run.sh diff --git a/run.sh b/run.sh old mode 100644 new mode 100755 From 36b27468f916d01da9ecb9b8f570999eae43071f Mon Sep 17 00:00:00 2001 From: Alexandre Wavelet Date: Thu, 11 Jun 2026 02:53:25 +0200 Subject: [PATCH 02/11] chore: ignore .idea/ (JetBrains IDE) Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 9f50e46..2d54d94 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ obj/ # Aspire / tooling *.user + +# JetBrains IDE +.idea/ From 4c3fa73c889b31fc51f32cf11fb7c3a376711928 Mon Sep 17 00:00:00 2001 From: Alexandre Wavelet Date: Thu, 11 Jun 2026 02:53:26 +0200 Subject: [PATCH 03/11] feat: add uniques-search-api (Rust Unique-cards search) to the dev env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New standalone 'uniques' service: Altered-Re-Union/uniques-search-api (fork of Taum/rust-cards-api), a Rust in-memory search engine over the Altered Unique cards (/api/v2/*). Distinct from the prod cards API — no DB, no Keycloak, no seed, nothing in DbGate. - uniques/Dockerfile + docker-entrypoint.sh: dev image (the repo's own Dockerfile is prod-only and COPYs an uncommitted deployment/production.toml). Source is bind-mounted and 'cargo run'; the ~270 MB prebuilt index is downloaded once into a volume by the entrypoint (the binary loads it from disk, source=disk). - apphost.cs: repoUrls entry + altered-uniques-api resource (host 8003 -> container 8080), target/index volumes, PORT/INDEX_PATH env, dashboard URL. No WaitFor/seed. - appsettings(.Local.json.example): uniques toggle (enabled by default). - README: service row + 'uniques (uniques-search-api)' section. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 41 +++++++++++++++++++++++++++++++++ apphost.cs | 42 ++++++++++++++++++++++++++++++++++ appsettings.Local.json.example | 1 + appsettings.json | 1 + uniques/Dockerfile | 34 +++++++++++++++++++++++++++ uniques/docker-entrypoint.sh | 29 +++++++++++++++++++++++ 6 files changed, 148 insertions(+) create mode 100644 uniques/Dockerfile create mode 100755 uniques/docker-entrypoint.sh diff --git a/README.md b/README.md index f9865ac..0e7144b 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ is also printed in the console). | `altered-decks-api` | http://decks.altered.local.gd:8001 (or http://localhost:8001) | local | Symfony/FrankenPHP + Postgres; admin at `/admin/login` | | `altered-collection-api` | http://collection.altered.local.gd:8002 (or http://localhost:8002) | local | Symfony/API Platform/FrankenPHP + Postgres; docs at `/api/docs` | | `altered-website` | http://website.altered.local.gd:18181 (or http://localhost:18181) | local | Plain PHP/Apache + MariaDB; Keycloak SSO via the `main-site` client | +| `altered-uniques-api` | http://uniques.altered.local.gd:8003 (or http://localhost:8003) | local | Rust in-memory search over **Unique** cards (`/api/v2/*`); no DB/auth. Standalone — NOT the prod cards API | | `altered-dbgate` | http://localhost:18182 | local | One web DB client for **all** project DBs (decks + collection Postgres, website MariaDB) | | cards | https://cards.alteredcore.org | **prod** | decks (and the website) read cards from prod | @@ -114,6 +115,46 @@ on start (entrypoint); the DB persists in the `altered-collection-pg-data` volum The local **website** points its `COLLECTION_API_URL` at this service (over the Aspire network), so the collection features use it — though the DB starts empty. +### uniques (uniques-search-api) + +`altered-uniques-api` is [Altered-Re-Union/uniques-search-api](https://github.com/Altered-Re-Union/uniques-search-api) +(the Altered-Re-Union fork of [Taum/rust-cards-api](https://github.com/Taum/rust-cards-api)), +a Rust in-memory search engine over the Altered **Unique** cards. It is **not** the +prod cards API: it has its own contract (`/api/v2/cards`, `/api/v2/card/{reference}`, +`/api/v2/effects`) and only covers Unique characters, so it does **not** replace the +`cards.alteredcore.org` source the other services read. It runs standalone for now — +nothing else is wired to it yet (future consumers reach it at +`http://altered-uniques-api:8080` over the Aspire network). + +It's the simplest service in the stack — no database, no Keycloak, no seed, nothing +in DbGate. Two wrinkles are handled here rather than upstream: + +- **Build** — the repo ships only a prod Dockerfile (it `COPY`s a + `deployment/production.toml` that isn't committed, and bakes a no-index image), so + the dev scaffolding lives here in [uniques/](uniques/): a thin `rust:1.86` image + whose entrypoint downloads the index, then runs `cargo run -p uniques-http-api + --release`. The AppHost bind-mounts the repo source at `/app` and keeps the cargo + build cache in the `altered-uniques-api-target` volume (first build is slow, like + decks; later starts are fast). +- **Index** — the server loads a ~270 MB prebuilt card index from disk (it doesn't + fetch it itself). The entrypoint downloads `full_index.tar.zst` from + `storage.googleapis.com/taum-reunion-public` into the `altered-uniques-index` volume + on first start only; the loader reads the archive directly (no extraction). Wipe + that volume to re-download. + +Config is driven entirely by env (`PORT`, `INDEX_PATH`) — the app's per-environment +toml files are optional, so no config file is bind-mounted. The `[formats]` source +(needed by `/api/v2/effects` and the `format=` filter) is a separate artifact and is +**not** wired yet, so those stay disabled; card search works from the index alone. + +To re-download the index / rebuild this service from scratch — **without touching any +project DB** — wipe its own two volumes (the downloaded index + the cargo build cache) +and restart: + +```sh +docker volume rm altered-uniques-index altered-uniques-api-target +``` + ### dbgate `altered-dbgate` (http://localhost:18182) is a single web DB client for **every** diff --git a/apphost.cs b/apphost.cs index fd425c0..5dcae74 100644 --- a/apphost.cs +++ b/apphost.cs @@ -45,6 +45,7 @@ ["altered-core-collection-api"] = "https://github.com/Altered-Community/altered-core-collection-api.git", ["alteredcore-website"] = "https://github.com/Altered-Community/alteredcore-website.git", ["altered-deckbuilder-poc-v2"] = "https://github.com/Altered-Community/altered-deckbuilder-poc-v2.git", + ["uniques-search-api"] = "https://github.com/Altered-Re-Union/uniques-search-api.git", }; // Handles to cross-referenced resources, assigned as each service is mounted, so @@ -454,6 +455,47 @@ }); } +// =========================================================================== +// uniques — uniques-search-api (local; the Altered-Re-Union fork of Taum/rust-cards-api). +// A Rust/Axum in-memory search engine over the Altered "Unique" cards. NOTE: this is +// NOT the prod cards API — it has its own +// contract (/api/v2/cards, /api/v2/card/{ref}, /api/v2/effects) and only covers +// Unique characters, so it does NOT replace ALTERED_CORE_URL/CARDS_API_URL. It's +// the simplest service here: no DB, no Keycloak, no seed, nothing in DbGate. +// +// The repo's own Dockerfile is PROD-only (it COPYs a deployment/production.toml +// the repo doesn't ship, and bakes a no-index image), so we build a DEV image +// from uniques/ and bind-mount the source like decks/collection. Config is driven +// purely by env (config.rs honours PORT/INDEX_PATH as overrides; per-env tomls are +// optional, default.toml ships in the source), so no custom toml is needed. The +// ~270 MB prebuilt index is downloaded once into a volume by the entrypoint (the +// binary loads it from disk and won't fetch it itself). The server binds +// 0.0.0.0:$PORT, so it's reachable on the Aspire network by its resource name — +// future consumers (website, decks-api) can point at http://altered-uniques-api:8080 +// with no change here (add CORS to the Rust service only if a browser calls it +// directly). +// =========================================================================== +if (Enabled("uniques")) +{ + var uniquesRepo = Repo("uniques-search-api"); + + builder.AddDockerfile("altered-uniques-api", Path.Combine(appHostDir, "uniques")) + .WithBindMount(uniquesRepo, "/app") + // target/ (cargo build cache) and build/ (the downloaded index) in + // container-managed volumes — they shadow the host checkout's subpaths, so + // the first build is slow then cached, and the index persists across + // restarts. Same idea as decks/collection's vendor/ volume. + .WithVolume("altered-uniques-api-target", "/app/target") + .WithVolume("altered-uniques-index", "/app/build") + .WithHttpEndpoint(port: 8003, targetPort: 8080, name: "http") + // config.rs honours these as legacy overrides, so no custom toml is needed. + .WithEnvironment("PORT", "8080") + .WithEnvironment("INDEX_PATH", "/app/build/full_index.tar.zst") + // Dashboard link: a friendly *.local.gd host (resolves to 127.0.0.1 -> the + // Aspire proxy on :8003) hitting a tiny sample query. + .WithUrl("http://uniques.altered.local.gd:8003/api/v2/cards?limit=1", "cards (sample)"); +} + // =========================================================================== // dbgate — one web DB client for ALL project databases (replaces the per-service // phpMyAdmin). It reaches each DB container over the Aspire network by its diff --git a/appsettings.Local.json.example b/appsettings.Local.json.example index 50bdd9a..41a6e56 100644 --- a/appsettings.Local.json.example +++ b/appsettings.Local.json.example @@ -10,6 +10,7 @@ "decks": { "Enabled": true }, "collection": { "Enabled": true }, "website": { "Enabled": true }, + "uniques": { "Enabled": true }, "cards": { "Enabled": false } } } diff --git a/appsettings.json b/appsettings.json index 86380ce..8769db6 100644 --- a/appsettings.json +++ b/appsettings.json @@ -4,6 +4,7 @@ "decks": { "Enabled": true }, "collection": { "Enabled": true }, "website": { "Enabled": true }, + "uniques": { "Enabled": true }, "cards": { "Enabled": false } } } diff --git a/uniques/Dockerfile b/uniques/Dockerfile new file mode 100644 index 0000000..6b9d1b7 --- /dev/null +++ b/uniques/Dockerfile @@ -0,0 +1,34 @@ +# syntax=docker/dockerfile:1 + +# DEV image for uniques-search-api (the `uniques-http-api` server), kept HERE in the +# dev-environment (not in the uniques-search-api repo, which ships only a PROD-only +# Dockerfile: it targets a `runtime` stage that COPYs deployment/production.toml +# — a file the repo does not ship — and bakes the binary with no card index). +# +# Instead, mirroring decks-api/collection-api, the AppHost bind-mounts the repo +# source at /app and we `cargo run` it; the build cache lives in a volume +# (/app/target) and the ~270 MB prebuilt index is downloaded once into another +# volume (/app/build) by the entrypoint — the binary won't fetch it on its own +# (config index.source = "disk"). The dev stage copies no app source, so the +# source does not need to be in the build context. +# +# rust:1.86 matches the toolchain the upstream Dockerfile pins (the workspace is +# edition 2024). + +FROM rust:1.86-bookworm + +# curl + ca-certificates: download the prebuilt index from GCS in the entrypoint +# (the bookworm base is minimal on tools). +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY --link --chmod=755 docker-entrypoint.sh /usr/local/bin/docker-entrypoint + +ENTRYPOINT ["docker-entrypoint"] + +# `just api` equivalent. cargo checks freshness, so after the first (slow) build +# the target/ volume makes later starts near-instant. +CMD ["cargo", "run", "-p", "uniques-http-api", "--release"] diff --git a/uniques/docker-entrypoint.sh b/uniques/docker-entrypoint.sh new file mode 100755 index 0000000..0adbda7 --- /dev/null +++ b/uniques/docker-entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/sh +set -e + +# Download the prebuilt card index once, into the /app/build volume, before +# starting the server. The uniques-http-api binary loads the index from disk +# (config index.source = "disk") and does NOT fetch it itself, so we provide it +# here. The named volume persists it, so this only downloads on the first start. +# +# INDEX_PATH is the same env var the AppHost passes to the server (config.rs +# honours it as a legacy override), so the file we download is exactly the file +# the server then reads. The loader reads the .tar.zst archive directly — no +# extraction (and no zstd) needed at runtime. + +INDEX_FILE="${INDEX_PATH:-/app/build/full_index.tar.zst}" +INDEX_URL="https://storage.googleapis.com/taum-reunion-public/index/full_index.tar.zst" + +if [ ! -f "$INDEX_FILE" ]; then + echo "[uniques] index not found at $INDEX_FILE — downloading (~270 MB) from GCS..." + mkdir -p "$(dirname "$INDEX_FILE")" + # Download to a .partial then rename: an interrupted download must not leave a + # truncated file that looks "present" (and fails to load) on the next start. + curl -fSL "$INDEX_URL" -o "$INDEX_FILE.partial" + mv "$INDEX_FILE.partial" "$INDEX_FILE" + echo "[uniques] index downloaded." +else + echo "[uniques] index present at $INDEX_FILE — skipping download." +fi + +exec "$@" From c0598e5dbc624ae473f9471e9346e121af0fa03f Mon Sep 17 00:00:00 2001 From: Alexandre Wavelet Date: Thu, 11 Jun 2026 03:05:35 +0200 Subject: [PATCH 04/11] docs: clarify uniques formats are not wired (only the format= filter is gated) Earlier wording wrongly implied /api/v2/effects needs formats; it serves from the index (effects_body). Only the format= filter on /api/v2/cards is gated by the formats artifact, which the prebuilt index bundle doesn't ship and the repo doesn't generate. Add a dedicated Formats paragraph explaining what's left to wire. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0e7144b..9809750 100644 --- a/README.md +++ b/README.md @@ -143,9 +143,18 @@ in DbGate. Two wrinkles are handled here rather than upstream: that volume to re-download. Config is driven entirely by env (`PORT`, `INDEX_PATH`) — the app's per-environment -toml files are optional, so no config file is bind-mounted. The `[formats]` source -(needed by `/api/v2/effects` and the `format=` filter) is a separate artifact and is -**not** wired yet, so those stay disabled; card search works from the index alone. +toml files are optional, so no config file is bind-mounted. + +**Formats (not wired yet).** A "format" (e.g. `standard`) is a curated card subset — a +small JSON allowlist/denylist of card references, loaded from a `build/formats/` dir +(`manifest.json` + one file per format) and compiled against the index. It's **separate +from the index**: the prebuilt bundle ships none, and nothing in the repo generates one, +so the `[formats]` section stays off. It gates **only** the `format=` filter on +`/api/v2/cards` (otherwise `400 unknown format`) — plain card search and `/api/v2/effects` +work from the index alone. To enable it once we have the files: make a `build/formats/` +available to the container (mount it, or download it in the entrypoint like the index) +and turn on `[formats]` — a few lines. Open question: where those definitions come from +(hand-authored, or published somewhere by Re-Union). To re-download the index / rebuild this service from scratch — **without touching any project DB** — wipe its own two volumes (the downloaded index + the cargo build cache) From 9531fda7dffc53e1795a5aafde16632cd7d75591 Mon Sep 17 00:00:00 2001 From: Alexandre Wavelet Date: Thu, 11 Jun 2026 09:38:04 +0200 Subject: [PATCH 05/11] feat: add uniques-ui (demo-ui dev server) to the dev env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs uniques-search-api/demo-ui (Vite 6 + React 19 SPA) via the Vite dev server (HMR). Browser-only: the SPA calls the API directly (the API sets permissive CORS), so VITE_API_BASE_URL points at the browser-reachable API (localhost:8003) and no proxy / CORS work is needed. - uniques-ui/Dockerfile + docker-entrypoint.sh: node:22 dev image; demo-ui/ is bind-mounted, node_modules in a volume (npm ci on first start), npm run dev. - apphost.cs: altered-uniques-ui resource; Vite port published directly (-p, like Keycloak) on the same internal/external port (8004) so the HMR websocket lines up. Declares a graph relationship to altered-uniques-api (edge only, no startup gating) — the dependency is browser-side, so WaitFor would needlessly block the UI behind the API's long first build. - appsettings(.Local.json.example): uniques-ui toggle (enabled by default). - README: service row + 'uniques-ui (demo-ui)' section. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 17 +++++++++++++ apphost.cs | 43 ++++++++++++++++++++++++++++++++- appsettings.Local.json.example | 1 + appsettings.json | 1 + uniques-ui/Dockerfile | 24 ++++++++++++++++++ uniques-ui/docker-entrypoint.sh | 16 ++++++++++++ 6 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 uniques-ui/Dockerfile create mode 100755 uniques-ui/docker-entrypoint.sh diff --git a/README.md b/README.md index 9809750..e7d7380 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ is also printed in the console). | `altered-collection-api` | http://collection.altered.local.gd:8002 (or http://localhost:8002) | local | Symfony/API Platform/FrankenPHP + Postgres; docs at `/api/docs` | | `altered-website` | http://website.altered.local.gd:18181 (or http://localhost:18181) | local | Plain PHP/Apache + MariaDB; Keycloak SSO via the `main-site` client | | `altered-uniques-api` | http://uniques.altered.local.gd:8003 (or http://localhost:8003) | local | Rust in-memory search over **Unique** cards (`/api/v2/*`); no DB/auth. Standalone — NOT the prod cards API | +| `altered-uniques-ui` | http://localhost:8004 | local | Vite/React demo SPA for the uniques API (Vite dev server, HMR; browser → API direct) | | `altered-dbgate` | http://localhost:18182 | local | One web DB client for **all** project DBs (decks + collection Postgres, website MariaDB) | | cards | https://cards.alteredcore.org | **prod** | decks (and the website) read cards from prod | @@ -164,6 +165,22 @@ and restart: docker volume rm altered-uniques-index altered-uniques-api-target ``` +### uniques-ui (demo-ui) + +`altered-uniques-ui` runs the repo's `demo-ui/` — a Vite + React SPA for the uniques API +— via the Vite **dev server** (HMR). The repo ships no Dockerfile for it, so the dev +image lives in [uniques-ui/](uniques-ui/): `demo-ui/` is bind-mounted, `node_modules` +lives in the `altered-uniques-ui-node-modules` volume (`npm ci` on first start), and +`npm run dev` serves it. + +The SPA calls the API **straight from the browser** (the API sets `CorsLayer::permissive`), +so `VITE_API_BASE_URL` points at the browser-reachable API (`http://localhost:8003`) — no +Vite proxy, no CORS work. Vite's port is published directly (`-p`, like Keycloak) on the +same internal/external port (8004) so the HMR websocket lines up. **Open it at +http://localhost:8004** — Vite's host allowlist permits `localhost` but not the +`*.local.gd` host (that would need `server.allowedHosts`). Needs the `uniques` service +running to have an API to talk to. + ### dbgate `altered-dbgate` (http://localhost:18182) is a single web DB client for **every** diff --git a/apphost.cs b/apphost.cs index 5dcae74..e4a5c5e 100644 --- a/apphost.cs +++ b/apphost.cs @@ -60,6 +60,10 @@ IResource? collectionPgResource = null; IResource? websiteDbResource = null; +// The uniques API resource, captured so the demo-ui can declare a graph relationship to +// it (browser-side runtime dependency — edge only, no startup gating). +IResource? uniquesApiResource = null; + // =========================================================================== // auth — Keycloak (local), built from AlteredAuth/build/Dockerfile so it carries // the custom "unique-attribute" provider (pseudo uniqueness) + Altered themes. @@ -479,7 +483,7 @@ { var uniquesRepo = Repo("uniques-search-api"); - builder.AddDockerfile("altered-uniques-api", Path.Combine(appHostDir, "uniques")) + var uniquesApi = builder.AddDockerfile("altered-uniques-api", Path.Combine(appHostDir, "uniques")) .WithBindMount(uniquesRepo, "/app") // target/ (cargo build cache) and build/ (the downloaded index) in // container-managed volumes — they shadow the host checkout's subpaths, so @@ -494,6 +498,43 @@ // Dashboard link: a friendly *.local.gd host (resolves to 127.0.0.1 -> the // Aspire proxy on :8003) hitting a tiny sample query. .WithUrl("http://uniques.altered.local.gd:8003/api/v2/cards?limit=1", "cards (sample)"); + + uniquesApiResource = uniquesApi.Resource; +} + +// =========================================================================== +// uniques-ui — uniques-search-api/demo-ui (local). A Vite 6 + React 19 SPA demo for +// the uniques API, run via the Vite dev server (HMR). Browser-only: it calls the API +// directly (the API sets CorsLayer::permissive), so VITE_API_BASE_URL points at the +// browser-reachable API URL — no proxy, no CORS work. The repo ships no Dockerfile for +// it, so a dev image lives in uniques-ui/; demo-ui/ is bind-mounted and node_modules +// lives in a volume (npm ci on first start). +// +// We publish Vite's port directly with -p (like Keycloak), NOT through the Aspire +// proxy, and run Vite on the same port we publish (8004) so the HMR websocket — which +// the browser opens to the page's own host:port — lines up. Open it at +// http://localhost:8004 (Vite's host allowlist permits localhost; the *.local.gd host +// would need server.allowedHosts). No WaitFor: the SPA needs no backend to start +// serving, and not blocking on the API's long first build keeps the UI available fast. +// =========================================================================== +if (Enabled("uniques-ui")) +{ + var uniquesUiRepo = Repo("uniques-search-api"); + + var uniquesUi = builder.AddDockerfile("altered-uniques-ui", Path.Combine(appHostDir, "uniques-ui")) + .WithBindMount(Path.Combine(uniquesUiRepo, "demo-ui"), "/app") + .WithVolume("altered-uniques-ui-node-modules", "/app/node_modules") + // Publish Vite on 0.0.0.0:8004 directly (bypass the Aspire proxy) so the HMR + // websocket works; internal port == published port so the HMR client lines up. + .WithContainerRuntimeArgs("-p", "0.0.0.0:8004:8004") + // Must be reachable FROM THE BROWSER (not the Aspire alias). Vite reads VITE_- + // prefixed vars from the environment (see demo-ui/README). + .WithEnvironment("VITE_API_BASE_URL", "http://localhost:8003") + .WithUrl("http://localhost:8004/", "demo-ui"); + + // Express the runtime (browser-side) dependency on the API as a graph edge ONLY — + // no startup gating (same mechanism DbGate uses for the DBs). Null if uniques is off. + if (uniquesApiResource is not null) uniquesUi.WithReferenceRelationship(uniquesApiResource); } // =========================================================================== diff --git a/appsettings.Local.json.example b/appsettings.Local.json.example index 41a6e56..9ffefae 100644 --- a/appsettings.Local.json.example +++ b/appsettings.Local.json.example @@ -11,6 +11,7 @@ "collection": { "Enabled": true }, "website": { "Enabled": true }, "uniques": { "Enabled": true }, + "uniques-ui": { "Enabled": true }, "cards": { "Enabled": false } } } diff --git a/appsettings.json b/appsettings.json index 8769db6..243da42 100644 --- a/appsettings.json +++ b/appsettings.json @@ -5,6 +5,7 @@ "collection": { "Enabled": true }, "website": { "Enabled": true }, "uniques": { "Enabled": true }, + "uniques-ui": { "Enabled": true }, "cards": { "Enabled": false } } } diff --git a/uniques-ui/Dockerfile b/uniques-ui/Dockerfile new file mode 100644 index 0000000..fe968a9 --- /dev/null +++ b/uniques-ui/Dockerfile @@ -0,0 +1,24 @@ +# syntax=docker/dockerfile:1 + +# DEV image for the demo-ui (uniques-search-api/demo-ui), a Vite 6 + React 19 SPA. +# Kept HERE in the dev-environment (the repo ships no Dockerfile for it). Mirrors the +# decks/uniques pattern: the AppHost bind-mounts demo-ui/ at /app and we run the Vite +# dev server (HMR); node_modules lives in a volume (npm ci into it on first start). +# +# The SPA talks to the API straight from the browser — the API sets permissive CORS +# (CorsLayer::permissive) — so VITE_API_BASE_URL points at the browser-reachable API +# URL and no Vite proxy / CORS work is needed. Vite reads VITE_-prefixed vars from the +# environment (see demo-ui/README), so WithEnvironment is enough. + +FROM node:22-bookworm-slim + +WORKDIR /app + +COPY --link --chmod=755 docker-entrypoint.sh /usr/local/bin/docker-entrypoint + +ENTRYPOINT ["docker-entrypoint"] + +# Run Vite on the SAME port we publish (8004) so the HMR websocket — which the browser +# opens to the page's own host:port — lines up. --host binds 0.0.0.0; --strictPort +# fails loudly rather than drifting to another port. +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "8004", "--strictPort"] diff --git a/uniques-ui/docker-entrypoint.sh b/uniques-ui/docker-entrypoint.sh new file mode 100755 index 0000000..8f06ac5 --- /dev/null +++ b/uniques-ui/docker-entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +# Install node deps into the (volume-backed) node_modules on first start only, then run +# the dev server. Mirrors decks/collection installing vendor/ into a volume, and uniques +# compiling into its target/ volume: the host checkout stays clean and the install is +# cached across restarts. +if [ -z "$(ls -A node_modules 2>/dev/null)" ]; then + echo "[uniques-ui] node_modules empty — running npm ci (first start)..." + npm ci + echo "[uniques-ui] deps installed." +else + echo "[uniques-ui] node_modules present — skipping install." +fi + +exec "$@" From 0357773a57fbef0e9ba814a7f6d0b385b7592963 Mon Sep 17 00:00:00 2001 From: Alexandre Wavelet Date: Fri, 12 Jun 2026 09:28:13 +0200 Subject: [PATCH 06/11] fix: address code-review nits on the uniques services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apphost.cs: drop the redundant INDEX_PATH env override — default.toml already points at ./build/full_index.tar.zst (-> /app/build/...), so setting it again just logged config.rs's 'prefer index.path in config' warning on every start. (L1) - apphost.cs: fix the uniques comment — CORS is already CorsLayer::permissive in the app's http.rs, so browser-direct callers (the demo-ui) work without adding anything. (M2) - uniques/docker-entrypoint.sh: guard against a 0-byte/truncated index download being promoted (then skipped forever on restart) — abort if the .partial file is empty. (M1) - README: warn against shadowing VITE_API_BASE_URL with a blank value in a demo-ui/.env*, which would route calls through Vite's unused :8234 proxy. (H1) Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 +++- apphost.cs | 17 ++++++++++------- uniques/docker-entrypoint.sh | 3 +++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e7d7380..8617317 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,9 @@ Vite proxy, no CORS work. Vite's port is published directly (`-p`, like Keycloak same internal/external port (8004) so the HMR websocket lines up. **Open it at http://localhost:8004** — Vite's host allowlist permits `localhost` but not the `*.local.gd` host (that would need `server.allowedHosts`). Needs the `uniques` service -running to have an API to talk to. +running to have an API to talk to. The API URL comes from the `VITE_API_BASE_URL` env +var — don't shadow it with a blank value in a `demo-ui/.env` / `.env.local`, which would +route calls through Vite's unused `:8234` proxy instead. ### dbgate diff --git a/apphost.cs b/apphost.cs index e4a5c5e..bd49646 100644 --- a/apphost.cs +++ b/apphost.cs @@ -469,15 +469,16 @@ // // The repo's own Dockerfile is PROD-only (it COPYs a deployment/production.toml // the repo doesn't ship, and bakes a no-index image), so we build a DEV image -// from uniques/ and bind-mount the source like decks/collection. Config is driven -// purely by env (config.rs honours PORT/INDEX_PATH as overrides; per-env tomls are -// optional, default.toml ships in the source), so no custom toml is needed. The +// from uniques/ and bind-mount the source like decks/collection. Config is driven by +// env + the app's own default.toml (config.rs honours a PORT override; per-env tomls +// are optional, and default.toml already ships the right index path), so no custom +// toml is needed. The // ~270 MB prebuilt index is downloaded once into a volume by the entrypoint (the // binary loads it from disk and won't fetch it itself). The server binds // 0.0.0.0:$PORT, so it's reachable on the Aspire network by its resource name — // future consumers (website, decks-api) can point at http://altered-uniques-api:8080 -// with no change here (add CORS to the Rust service only if a browser calls it -// directly). +// with no change here. CORS is already CorsLayer::permissive in the app's http.rs, so +// even browser-direct callers (the demo-ui) work out of the box. // =========================================================================== if (Enabled("uniques")) { @@ -492,9 +493,11 @@ .WithVolume("altered-uniques-api-target", "/app/target") .WithVolume("altered-uniques-index", "/app/build") .WithHttpEndpoint(port: 8003, targetPort: 8080, name: "http") - // config.rs honours these as legacy overrides, so no custom toml is needed. + // PORT is honoured by config.rs (no custom toml needed). The index path is left + // to the app's default.toml (./build/full_index.tar.zst -> /app/build/...), which + // the entrypoint downloads into — setting INDEX_PATH too would just duplicate it + // and log a "prefer index.path in config" warning on every start. .WithEnvironment("PORT", "8080") - .WithEnvironment("INDEX_PATH", "/app/build/full_index.tar.zst") // Dashboard link: a friendly *.local.gd host (resolves to 127.0.0.1 -> the // Aspire proxy on :8003) hitting a tiny sample query. .WithUrl("http://uniques.altered.local.gd:8003/api/v2/cards?limit=1", "cards (sample)"); diff --git a/uniques/docker-entrypoint.sh b/uniques/docker-entrypoint.sh index 0adbda7..1dd421b 100755 --- a/uniques/docker-entrypoint.sh +++ b/uniques/docker-entrypoint.sh @@ -20,6 +20,9 @@ if [ ! -f "$INDEX_FILE" ]; then # Download to a .partial then rename: an interrupted download must not leave a # truncated file that looks "present" (and fails to load) on the next start. curl -fSL "$INDEX_URL" -o "$INDEX_FILE.partial" + # Guard against a 0-byte / truncated "success": don't promote (and then "skip + # download" forever on the next start) an unusable file. + [ -s "$INDEX_FILE.partial" ] || { echo "[uniques] downloaded index is empty — aborting"; rm -f "$INDEX_FILE.partial"; exit 1; } mv "$INDEX_FILE.partial" "$INDEX_FILE" echo "[uniques] index downloaded." else From da6df7fbc4dc38b96ae0dbf98143d3df65ebb9f1 Mon Sep 17 00:00:00 2001 From: Alexandre Wavelet Date: Fri, 12 Jun 2026 21:00:59 +0200 Subject: [PATCH 07/11] chore: use uniques-search-api's own docker/dev image for the uniques service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dev Dockerfile + entrypoint now live in the uniques-search-api repo under docker/dev/ (separate from its prod root Dockerfile), so the AppHost points the uniques resource at that Dockerfile and drops its local uniques/ copy. REQUIRES Altered-Re-Union/uniques-search-api#1 to be merged to main FIRST: a fresh clone pulls uniques-search-api's main, so docker/dev/ must be there or the image build fails. (uniques-ui is unchanged — its dev image still lives here.) Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 21 +++++++++++---------- apphost.cs | 8 ++++---- uniques/Dockerfile | 34 ---------------------------------- uniques/docker-entrypoint.sh | 32 -------------------------------- 4 files changed, 15 insertions(+), 80 deletions(-) delete mode 100644 uniques/Dockerfile delete mode 100755 uniques/docker-entrypoint.sh diff --git a/README.md b/README.md index 8617317..e600a2c 100644 --- a/README.md +++ b/README.md @@ -128,22 +128,23 @@ nothing else is wired to it yet (future consumers reach it at `http://altered-uniques-api:8080` over the Aspire network). It's the simplest service in the stack — no database, no Keycloak, no seed, nothing -in DbGate. Two wrinkles are handled here rather than upstream: - -- **Build** — the repo ships only a prod Dockerfile (it `COPY`s a - `deployment/production.toml` that isn't committed, and bakes a no-index image), so - the dev scaffolding lives here in [uniques/](uniques/): a thin `rust:1.86` image - whose entrypoint downloads the index, then runs `cargo run -p uniques-http-api - --release`. The AppHost bind-mounts the repo source at `/app` and keeps the cargo - build cache in the `altered-uniques-api-target` volume (first build is slow, like - decks; later starts are fast). +in DbGate. Two things to know: + +- **Build** — the repo's root Dockerfile is prod-only (a multi-stage Cloud Run build + that bakes the binary), so the repo ships a separate dev image at + `docker/dev/Dockerfile`: a thin `rust:1.86` image whose entrypoint downloads the + index, then runs `cargo run -p uniques-http-api --release`. The AppHost points the + `uniques` resource at that Dockerfile, bind-mounts the repo source at `/app`, and + keeps the cargo build cache in the `altered-uniques-api-target` volume (first build + is slow, like decks; later starts are fast). - **Index** — the server loads a ~270 MB prebuilt card index from disk (it doesn't fetch it itself). The entrypoint downloads `full_index.tar.zst` from `storage.googleapis.com/taum-reunion-public` into the `altered-uniques-index` volume on first start only; the loader reads the archive directly (no extraction). Wipe that volume to re-download. -Config is driven entirely by env (`PORT`, `INDEX_PATH`) — the app's per-environment +Config is driven by env + the app's own `default.toml` (`PORT` override; the index path +comes from `default.toml`) — its per-environment toml files are optional, so no config file is bind-mounted. **Formats (not wired yet).** A "format" (e.g. `standard`) is a curated card subset — a diff --git a/apphost.cs b/apphost.cs index bd49646..3032566 100644 --- a/apphost.cs +++ b/apphost.cs @@ -467,9 +467,9 @@ // Unique characters, so it does NOT replace ALTERED_CORE_URL/CARDS_API_URL. It's // the simplest service here: no DB, no Keycloak, no seed, nothing in DbGate. // -// The repo's own Dockerfile is PROD-only (it COPYs a deployment/production.toml -// the repo doesn't ship, and bakes a no-index image), so we build a DEV image -// from uniques/ and bind-mount the source like decks/collection. Config is driven by +// The repo's root Dockerfile is PROD-only (a multi-stage Cloud Run build that bakes +// the binary), so the repo ships a separate DEV image at docker/dev/Dockerfile that we +// build and bind-mount the source into like decks/collection. Config is driven by // env + the app's own default.toml (config.rs honours a PORT override; per-env tomls // are optional, and default.toml already ships the right index path), so no custom // toml is needed. The @@ -484,7 +484,7 @@ { var uniquesRepo = Repo("uniques-search-api"); - var uniquesApi = builder.AddDockerfile("altered-uniques-api", Path.Combine(appHostDir, "uniques")) + var uniquesApi = builder.AddDockerfile("altered-uniques-api", Path.Combine(uniquesRepo, "docker", "dev")) .WithBindMount(uniquesRepo, "/app") // target/ (cargo build cache) and build/ (the downloaded index) in // container-managed volumes — they shadow the host checkout's subpaths, so diff --git a/uniques/Dockerfile b/uniques/Dockerfile deleted file mode 100644 index 6b9d1b7..0000000 --- a/uniques/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -# syntax=docker/dockerfile:1 - -# DEV image for uniques-search-api (the `uniques-http-api` server), kept HERE in the -# dev-environment (not in the uniques-search-api repo, which ships only a PROD-only -# Dockerfile: it targets a `runtime` stage that COPYs deployment/production.toml -# — a file the repo does not ship — and bakes the binary with no card index). -# -# Instead, mirroring decks-api/collection-api, the AppHost bind-mounts the repo -# source at /app and we `cargo run` it; the build cache lives in a volume -# (/app/target) and the ~270 MB prebuilt index is downloaded once into another -# volume (/app/build) by the entrypoint — the binary won't fetch it on its own -# (config index.source = "disk"). The dev stage copies no app source, so the -# source does not need to be in the build context. -# -# rust:1.86 matches the toolchain the upstream Dockerfile pins (the workspace is -# edition 2024). - -FROM rust:1.86-bookworm - -# curl + ca-certificates: download the prebuilt index from GCS in the entrypoint -# (the bookworm base is minimal on tools). -RUN apt-get update \ - && apt-get install -y --no-install-recommends curl ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -COPY --link --chmod=755 docker-entrypoint.sh /usr/local/bin/docker-entrypoint - -ENTRYPOINT ["docker-entrypoint"] - -# `just api` equivalent. cargo checks freshness, so after the first (slow) build -# the target/ volume makes later starts near-instant. -CMD ["cargo", "run", "-p", "uniques-http-api", "--release"] diff --git a/uniques/docker-entrypoint.sh b/uniques/docker-entrypoint.sh deleted file mode 100755 index 1dd421b..0000000 --- a/uniques/docker-entrypoint.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh -set -e - -# Download the prebuilt card index once, into the /app/build volume, before -# starting the server. The uniques-http-api binary loads the index from disk -# (config index.source = "disk") and does NOT fetch it itself, so we provide it -# here. The named volume persists it, so this only downloads on the first start. -# -# INDEX_PATH is the same env var the AppHost passes to the server (config.rs -# honours it as a legacy override), so the file we download is exactly the file -# the server then reads. The loader reads the .tar.zst archive directly — no -# extraction (and no zstd) needed at runtime. - -INDEX_FILE="${INDEX_PATH:-/app/build/full_index.tar.zst}" -INDEX_URL="https://storage.googleapis.com/taum-reunion-public/index/full_index.tar.zst" - -if [ ! -f "$INDEX_FILE" ]; then - echo "[uniques] index not found at $INDEX_FILE — downloading (~270 MB) from GCS..." - mkdir -p "$(dirname "$INDEX_FILE")" - # Download to a .partial then rename: an interrupted download must not leave a - # truncated file that looks "present" (and fails to load) on the next start. - curl -fSL "$INDEX_URL" -o "$INDEX_FILE.partial" - # Guard against a 0-byte / truncated "success": don't promote (and then "skip - # download" forever on the next start) an unusable file. - [ -s "$INDEX_FILE.partial" ] || { echo "[uniques] downloaded index is empty — aborting"; rm -f "$INDEX_FILE.partial"; exit 1; } - mv "$INDEX_FILE.partial" "$INDEX_FILE" - echo "[uniques] index downloaded." -else - echo "[uniques] index present at $INDEX_FILE — skipping download." -fi - -exec "$@" From 8c54fa23fe0944c355e8cb03c28d835144ceb93a Mon Sep 17 00:00:00 2001 From: Alexandre Wavelet Date: Fri, 12 Jun 2026 21:08:31 +0200 Subject: [PATCH 08/11] chore: use the demo-ui's own docker/dev image for uniques-ui The demo-ui dev Dockerfile + entrypoint now live in uniques-search-api at demo-ui/docker/dev/ (alongside the API's docker/dev/), so the AppHost points the uniques-ui resource at that path and drops its local uniques-ui/ copy. Same merge-order requirement: uniques-search-api#1 must land on main first. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 9 +++++---- apphost.cs | 8 ++++---- uniques-ui/Dockerfile | 24 ------------------------ uniques-ui/docker-entrypoint.sh | 16 ---------------- 4 files changed, 9 insertions(+), 48 deletions(-) delete mode 100644 uniques-ui/Dockerfile delete mode 100755 uniques-ui/docker-entrypoint.sh diff --git a/README.md b/README.md index e600a2c..36ef579 100644 --- a/README.md +++ b/README.md @@ -169,10 +169,11 @@ docker volume rm altered-uniques-index altered-uniques-api-target ### uniques-ui (demo-ui) `altered-uniques-ui` runs the repo's `demo-ui/` — a Vite + React SPA for the uniques API -— via the Vite **dev server** (HMR). The repo ships no Dockerfile for it, so the dev -image lives in [uniques-ui/](uniques-ui/): `demo-ui/` is bind-mounted, `node_modules` -lives in the `altered-uniques-ui-node-modules` volume (`npm ci` on first start), and -`npm run dev` serves it. +— via the Vite **dev server** (HMR). The dev image lives in the upstream repo at +`demo-ui/docker/dev/Dockerfile`; the AppHost points the `uniques-ui` resource at it, +bind-mounts `demo-ui/` at `/app`, keeps `node_modules` in the +`altered-uniques-ui-node-modules` volume (`npm ci` on first start), and `npm run dev` +serves it. The SPA calls the API **straight from the browser** (the API sets `CorsLayer::permissive`), so `VITE_API_BASE_URL` points at the browser-reachable API (`http://localhost:8003`) — no diff --git a/apphost.cs b/apphost.cs index 3032566..096ddf6 100644 --- a/apphost.cs +++ b/apphost.cs @@ -509,9 +509,9 @@ // uniques-ui — uniques-search-api/demo-ui (local). A Vite 6 + React 19 SPA demo for // the uniques API, run via the Vite dev server (HMR). Browser-only: it calls the API // directly (the API sets CorsLayer::permissive), so VITE_API_BASE_URL points at the -// browser-reachable API URL — no proxy, no CORS work. The repo ships no Dockerfile for -// it, so a dev image lives in uniques-ui/; demo-ui/ is bind-mounted and node_modules -// lives in a volume (npm ci on first start). +// browser-reachable API URL — no proxy, no CORS work. The repo ships the dev image at +// demo-ui/docker/dev/; demo-ui/ is bind-mounted and node_modules lives in a volume +// (npm ci on first start). // // We publish Vite's port directly with -p (like Keycloak), NOT through the Aspire // proxy, and run Vite on the same port we publish (8004) so the HMR websocket — which @@ -524,7 +524,7 @@ { var uniquesUiRepo = Repo("uniques-search-api"); - var uniquesUi = builder.AddDockerfile("altered-uniques-ui", Path.Combine(appHostDir, "uniques-ui")) + var uniquesUi = builder.AddDockerfile("altered-uniques-ui", Path.Combine(uniquesUiRepo, "demo-ui", "docker", "dev")) .WithBindMount(Path.Combine(uniquesUiRepo, "demo-ui"), "/app") .WithVolume("altered-uniques-ui-node-modules", "/app/node_modules") // Publish Vite on 0.0.0.0:8004 directly (bypass the Aspire proxy) so the HMR diff --git a/uniques-ui/Dockerfile b/uniques-ui/Dockerfile deleted file mode 100644 index fe968a9..0000000 --- a/uniques-ui/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -# syntax=docker/dockerfile:1 - -# DEV image for the demo-ui (uniques-search-api/demo-ui), a Vite 6 + React 19 SPA. -# Kept HERE in the dev-environment (the repo ships no Dockerfile for it). Mirrors the -# decks/uniques pattern: the AppHost bind-mounts demo-ui/ at /app and we run the Vite -# dev server (HMR); node_modules lives in a volume (npm ci into it on first start). -# -# The SPA talks to the API straight from the browser — the API sets permissive CORS -# (CorsLayer::permissive) — so VITE_API_BASE_URL points at the browser-reachable API -# URL and no Vite proxy / CORS work is needed. Vite reads VITE_-prefixed vars from the -# environment (see demo-ui/README), so WithEnvironment is enough. - -FROM node:22-bookworm-slim - -WORKDIR /app - -COPY --link --chmod=755 docker-entrypoint.sh /usr/local/bin/docker-entrypoint - -ENTRYPOINT ["docker-entrypoint"] - -# Run Vite on the SAME port we publish (8004) so the HMR websocket — which the browser -# opens to the page's own host:port — lines up. --host binds 0.0.0.0; --strictPort -# fails loudly rather than drifting to another port. -CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "8004", "--strictPort"] diff --git a/uniques-ui/docker-entrypoint.sh b/uniques-ui/docker-entrypoint.sh deleted file mode 100755 index 8f06ac5..0000000 --- a/uniques-ui/docker-entrypoint.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -set -e - -# Install node deps into the (volume-backed) node_modules on first start only, then run -# the dev server. Mirrors decks/collection installing vendor/ into a volume, and uniques -# compiling into its target/ volume: the host checkout stays clean and the install is -# cached across restarts. -if [ -z "$(ls -A node_modules 2>/dev/null)" ]; then - echo "[uniques-ui] node_modules empty — running npm ci (first start)..." - npm ci - echo "[uniques-ui] deps installed." -else - echo "[uniques-ui] node_modules present — skipping install." -fi - -exec "$@" From af2ed759134a6d6a9e9e34e77f1aa9d49bb4ce16 Mon Sep 17 00:00:00 2001 From: Alexandre Wavelet Date: Fri, 12 Jun 2026 23:16:02 +0200 Subject: [PATCH 09/11] feat: wire the uniques format filters (frontier, living-legend) with hot-reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bind-mounts a dev-env-owned uniques/local.toml over the uniques service's config/local.toml, enabling [formats] (disk source = /app/formats = the repo's committed formats/ dir) with 5s hot-reload polling. README Formats section updated. Depends on Altered-Re-Union/uniques-search-api#1 (which ships formats/) being on main — same merge-order requirement already flagged in this PR. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 22 ++++++++++++---------- apphost.cs | 6 ++++++ uniques/local.toml | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 uniques/local.toml diff --git a/README.md b/README.md index 36ef579..98633d8 100644 --- a/README.md +++ b/README.md @@ -147,16 +147,18 @@ Config is driven by env + the app's own `default.toml` (`PORT` override; the ind comes from `default.toml`) — its per-environment toml files are optional, so no config file is bind-mounted. -**Formats (not wired yet).** A "format" (e.g. `standard`) is a curated card subset — a -small JSON allowlist/denylist of card references, loaded from a `build/formats/` dir -(`manifest.json` + one file per format) and compiled against the index. It's **separate -from the index**: the prebuilt bundle ships none, and nothing in the repo generates one, -so the `[formats]` section stays off. It gates **only** the `format=` filter on -`/api/v2/cards` (otherwise `400 unknown format`) — plain card search and `/api/v2/effects` -work from the index alone. To enable it once we have the files: make a `build/formats/` -available to the container (mount it, or download it in the entrypoint like the index) -and turn on `[formats]` — a few lines. Open question: where those definitions come from -(hand-authored, or published somewhere by Re-Union). +**Formats.** A "format" (e.g. `frontier`, `living-legend`) is a curated card subset — a +JSON allowlist (`included_refs`) or denylist (`excluded_sets` / `excluded_refs`) of card +references, loaded from a `formats/` dir (`manifest.json` + one file per format) and +compiled against the index. Two dev formats ship in the upstream repo's `formats/`: +**`frontier`** (50 random cards per faction, include) and **`living-legend`** (exclude the +CORE/COREKS sets + 10 random cards per faction). The dev-env enables them via a +bind-mounted [`uniques/local.toml`](uniques/local.toml) (`[formats]` source `/app/formats`) +with **hot-reload** polling (5 s). Query `?format=frontier` / `?format=living-legend`; an +unknown id → `400 unknown format`, a format that fails to load → `500` (check the logs). + +**Hot-reload note:** the poller compares the manifest's per-id `version`, so to pick up an +edit you must bump `version` in BOTH the format file and its `manifest.json` row. To re-download the index / rebuild this service from scratch — **without touching any project DB** — wipe its own two volumes (the downloaded index + the cargo build cache) diff --git a/apphost.cs b/apphost.cs index 096ddf6..e77bb0b 100644 --- a/apphost.cs +++ b/apphost.cs @@ -486,6 +486,12 @@ var uniquesApi = builder.AddDockerfile("altered-uniques-api", Path.Combine(uniquesRepo, "docker", "dev")) .WithBindMount(uniquesRepo, "/app") + // Dev formats wiring: a dev-env-owned local.toml bind-mounted over the checkout's + // config/local.toml — enables [formats] (source = /app/formats = the repo's + // committed formats/ dir) with hot-reload polling. Env alone can't set the reload + // interval, only the toml can. + .WithBindMount(Path.Combine(appHostDir, "uniques", "local.toml"), + "/app/uniques-http-api/config/local.toml", isReadOnly: true) // target/ (cargo build cache) and build/ (the downloaded index) in // container-managed volumes — they shadow the host checkout's subpaths, so // the first build is slow then cached, and the index persists across diff --git a/uniques/local.toml b/uniques/local.toml new file mode 100644 index 0000000..3e66c36 --- /dev/null +++ b/uniques/local.toml @@ -0,0 +1,17 @@ +# Dev formats config for the uniques service, owned by altered-dev-environment and +# bind-mounted (read-only) over the checkout's config/local.toml. It enables the +# `[formats]` filters from the repo's committed formats/ dir, with hot-reload polling. +# +# Why a file (not just env): the FORMATS_PATH env var enables formats but can't set the +# reload interval — only the [formats] toml can. The server loads default.toml then this +# local.toml from CONFIG_DIR (/app/uniques-http-api/config); PORT still comes from env. +# +# Hot-reload note: the poller compares the manifest's per-id `version` numbers, so to +# pick up an edit you must bump `version` in BOTH the format file and its manifest row. + +[formats] +reload_interval_secs = 5 + +[formats.source] +type = "disk" +path = "/app/formats" # = uniques-search-api/formats/ (repo bind-mounted at /app) From ec80bb5045f352ac25efe862c5dd74f63de32721 Mon Sep 17 00:00:00 2001 From: Alexandre Wavelet Date: Mon, 15 Jun 2026 11:22:58 +0200 Subject: [PATCH 10/11] feat(uniques-ui): bake deps in the image; hash-keyed, auto-pruned node_modules volume Build the demo-ui dev image from the repo's docker/dev/Dockerfile (context = demo-ui) now that deps are baked at build time. The node_modules volume only un-shadows the baked deps under the source bind-mount and is seeded from the image (no runtime install). Its name is keyed to a hash of package-lock.json so a dependency change auto-uses a fresh (re-seeded) volume; the AppHost prunes stale ones at startup. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 10 ++++++---- apphost.cs | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 98633d8..736155c 100644 --- a/README.md +++ b/README.md @@ -172,10 +172,12 @@ docker volume rm altered-uniques-index altered-uniques-api-target `altered-uniques-ui` runs the repo's `demo-ui/` — a Vite + React SPA for the uniques API — via the Vite **dev server** (HMR). The dev image lives in the upstream repo at -`demo-ui/docker/dev/Dockerfile`; the AppHost points the `uniques-ui` resource at it, -bind-mounts `demo-ui/` at `/app`, keeps `node_modules` in the -`altered-uniques-ui-node-modules` volume (`npm ci` on first start), and `npm run dev` -serves it. +`demo-ui/docker/dev/Dockerfile` (`npm ci` baked at build time). The AppHost points the +`uniques-ui` resource at it, bind-mounts `demo-ui/` at `/app` for HMR, and keeps a +`node_modules` volume **seeded from the image** (the bind-mount would otherwise shadow the +baked deps) — no runtime install. The volume name is keyed to a hash of `package-lock.json`, +so a dependency change auto-uses a fresh (re-seeded) volume and the AppHost prunes the +stale ones. `npm run dev` serves it. The SPA calls the API **straight from the browser** (the API sets `CorsLayer::permissive`), so `VITE_API_BASE_URL` points at the browser-reachable API (`http://localhost:8003`) — no diff --git a/apphost.cs b/apphost.cs index d069283..da55d24 100644 --- a/apphost.cs +++ b/apphost.cs @@ -593,8 +593,9 @@ // the uniques API, run via the Vite dev server (HMR). Browser-only: it calls the API // directly (the API sets CorsLayer::permissive), so VITE_API_BASE_URL points at the // browser-reachable API URL — no proxy, no CORS work. The repo ships the dev image at -// demo-ui/docker/dev/; demo-ui/ is bind-mounted and node_modules lives in a volume -// (npm ci on first start). +// demo-ui/docker/dev/ (npm ci is baked at build time). demo-ui/ is bind-mounted for +// HMR; the node_modules volume is seeded from the image (the bind-mount would otherwise +// shadow the baked node_modules) — no runtime install. // // We publish Vite's port directly with -p (like Keycloak), NOT through the Aspire // proxy, and run Vite on the same port we publish (8004) so the HMR websocket — which @@ -607,9 +608,20 @@ { var uniquesUiRepo = Repo("uniques-search-api"); - var uniquesUi = builder.AddDockerfile("altered-uniques-ui", Path.Combine(uniquesUiRepo, "demo-ui", "docker", "dev")) + // node_modules is baked into the image; this volume only un-shadows it under the + // source bind-mount, seeded from the image. Key the volume name to a hash of + // package-lock.json so a dependency change automatically gets a FRESH (re-seeded) + // volume — no manual wipe. Stale volumes from older hashes are pruned (best-effort). + var nmLock = Path.Combine(uniquesUiRepo, "demo-ui", "package-lock.json"); + var nmTag = File.Exists(nmLock) + ? Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(File.ReadAllBytes(nmLock)))[..8].ToLowerInvariant() + : "default"; + var nmVolume = $"altered-uniques-ui-node-modules-{nmTag}"; + PruneStaleVolumes("altered-uniques-ui-node-modules-", keep: nmVolume); + + var uniquesUi = builder.AddDockerfile("altered-uniques-ui", Path.Combine(uniquesUiRepo, "demo-ui"), "docker/dev/Dockerfile") .WithBindMount(Path.Combine(uniquesUiRepo, "demo-ui"), "/app") - .WithVolume("altered-uniques-ui-node-modules", "/app/node_modules") + .WithVolume(nmVolume, "/app/node_modules") // Publish Vite on 0.0.0.0:8004 directly (bypass the Aspire proxy) so the HMR // websocket works; internal port == published port so the HMR client lines up. .WithContainerRuntimeArgs("-p", "0.0.0.0:8004:8004") @@ -703,6 +715,43 @@ // Absolute directory containing this apphost.cs file (compile-time path). static string AppHostDirectory([CallerFilePath] string path = "") => Path.GetDirectoryName(path)!; +// Best-effort cleanup: remove docker volumes whose name starts with `prefix` except +// `keep` — i.e. stale node_modules volumes from previous package-lock hashes. Old-hash +// volumes aren't used by any running container, so removal is safe; any failure (docker +// not ready, or a volume still in use) is ignored so it never blocks startup. +static void PruneStaleVolumes(string prefix, string keep) +{ + try + { + using var list = Process.Start(new ProcessStartInfo("docker", $"volume ls -q --filter name={prefix}") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }); + if (list is null) return; + var names = list.StandardOutput.ReadToEnd(); + list.WaitForExit(); + foreach (var name in names.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + if (name == keep || !name.StartsWith(prefix)) continue; + try + { + using var rm = Process.Start(new ProcessStartInfo("docker", $"volume rm {name}") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }); + rm?.WaitForExit(); + if (rm?.ExitCode == 0) Console.WriteLine($"[altered] pruned stale node_modules volume {name}"); + } + catch { /* in use or already gone — ignore */ } + } + } + catch { /* docker unavailable — ignore */ } +} + // A presentational-only initial snapshot that keeps a resource OUT of the dashboard // (graph + table) without changing its behaviour — used for the dev DB passwords and // the auto-created database nodes. Aspire updates snapshots with record `with` From b65591f5a94a86c2785271ca9759414ff166f6fe Mon Sep 17 00:00:00 2001 From: Alexandre Wavelet Date: Tue, 16 Jun 2026 09:18:52 +0200 Subject: [PATCH 11/11] fix: move the uniques API to host port 8005 (8003 now used by ownership) PR #3 (ownership) landed on main and publishes on host port 8003; the uniques API also grabbed 8003 -> the two Aspire endpoints would collide. 8004 is uniques-ui, so the API moves to 8005 (endpoint + dashboard URL + the demo-ui's VITE_API_BASE_URL + README). The internal alias altered-uniques-api:8080 is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 ++-- apphost.cs | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 736155c..4262712 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ is also printed in the console). | `altered-decks-api` | http://decks.altered.local.gd:8001 (or http://localhost:8001) | local | Symfony/FrankenPHP + Postgres; admin at `/admin/login` | | `altered-collection-api` | http://collection.altered.local.gd:8002 (or http://localhost:8002) | local | Symfony/API Platform/FrankenPHP + Postgres; docs at `/api/docs` | | `altered-website` | http://website.altered.local.gd:18181 (or http://localhost:18181) | local | Plain PHP/Apache + MariaDB; Keycloak SSO via the `main-site` client | -| `altered-uniques-api` | http://uniques.altered.local.gd:8003 (or http://localhost:8003) | local | Rust in-memory search over **Unique** cards (`/api/v2/*`); no DB/auth. Standalone — NOT the prod cards API | +| `altered-uniques-api` | http://uniques.altered.local.gd:8005 (or http://localhost:8005) | local | Rust in-memory search over **Unique** cards (`/api/v2/*`); no DB/auth. Standalone — NOT the prod cards API | | `altered-uniques-ui` | http://localhost:8004 | local | Vite/React demo SPA for the uniques API (Vite dev server, HMR; browser → API direct) | | `altered-dbgate` | http://localhost:18182 | local | One web DB client for **all** project DBs (decks + collection Postgres, website MariaDB) | | cards | https://cards.alteredcore.org | **prod** | decks (and the website) read cards from prod | @@ -180,7 +180,7 @@ so a dependency change auto-uses a fresh (re-seeded) volume and the AppHost prun stale ones. `npm run dev` serves it. The SPA calls the API **straight from the browser** (the API sets `CorsLayer::permissive`), -so `VITE_API_BASE_URL` points at the browser-reachable API (`http://localhost:8003`) — no +so `VITE_API_BASE_URL` points at the browser-reachable API (`http://localhost:8005`) — no Vite proxy, no CORS work. Vite's port is published directly (`-p`, like Keycloak) on the same internal/external port (8004) so the HMR websocket lines up. **Open it at http://localhost:8004** — Vite's host allowlist permits `localhost` but not the diff --git a/apphost.cs b/apphost.cs index da55d24..5bf3029 100644 --- a/apphost.cs +++ b/apphost.cs @@ -575,15 +575,16 @@ // restarts. Same idea as decks/collection's vendor/ volume. .WithVolume("altered-uniques-api-target", "/app/target") .WithVolume("altered-uniques-index", "/app/build") - .WithHttpEndpoint(port: 8003, targetPort: 8080, name: "http") + // Host port 8005: 8003 is taken by the ownership service (PR #3), 8004 by uniques-ui. + .WithHttpEndpoint(port: 8005, targetPort: 8080, name: "http") // PORT is honoured by config.rs (no custom toml needed). The index path is left // to the app's default.toml (./build/full_index.tar.zst -> /app/build/...), which // the entrypoint downloads into — setting INDEX_PATH too would just duplicate it // and log a "prefer index.path in config" warning on every start. .WithEnvironment("PORT", "8080") // Dashboard link: a friendly *.local.gd host (resolves to 127.0.0.1 -> the - // Aspire proxy on :8003) hitting a tiny sample query. - .WithUrl("http://uniques.altered.local.gd:8003/api/v2/cards?limit=1", "cards (sample)"); + // Aspire proxy on :8005) hitting a tiny sample query. + .WithUrl("http://uniques.altered.local.gd:8005/api/v2/cards?limit=1", "cards (sample)"); uniquesApiResource = uniquesApi.Resource; } @@ -627,7 +628,7 @@ .WithContainerRuntimeArgs("-p", "0.0.0.0:8004:8004") // Must be reachable FROM THE BROWSER (not the Aspire alias). Vite reads VITE_- // prefixed vars from the environment (see demo-ui/README). - .WithEnvironment("VITE_API_BASE_URL", "http://localhost:8003") + .WithEnvironment("VITE_API_BASE_URL", "http://localhost:8005") .WithUrl("http://localhost:8004/", "demo-ui"); // Express the runtime (browser-side) dependency on the API as a graph edge ONLY —