From fc05a43264237f66b0000ba5a6f8c1e0c7085152 Mon Sep 17 00:00:00 2001 From: nholtware Date: Thu, 5 Nov 2026 12:57:41 -0600 Subject: [PATCH] docs: README Co-Authored-By: Claude Sonnet 5.5 --- README.md | 91 +++++++++---------------------------------------------- 1 file changed, 14 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 1a933b2..2b05612 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,19 @@ # adventure -Express 5 + TypeScript backend generated from `nholtware/service-template-express`. +Backend for the choose-your-adventure builder/player (Express 5 + Postgres), scaffolded +by `platform/new-service adventure --kind backend --with postgres`. Frontend: `adventure-web`. -## Endpoints +A story is one JSONB doc: `{ start, nodes: { key: { title, text, ending?, choices: [{label, to}] } } }`. +Validation (`src/lib/story.ts`): errors block publishing (missing start/targets, dead ends, +no reachable ending); warnings are advisory (unreachable scenes, choices on endings). -- `GET /healthz` liveness, always 200 once listening -- `GET /readyz` readiness: Postgres `SELECT 1` and Redis `PING` for whichever of `DATABASE_URL` / `REDIS_URL` are set; 503 + JSON reason otherwise -- `GET /` service name, version (git sha), pod, `greeting` (runtime config), a `service_info` row, Redis hit counter, and `banner` while the `hello-banner` flag is on -- `GET /admin/runtime-config` current runtime config `values`, `loadedAt`, `source` (`file` | `defaults`) -- `GET /log-demo?msg=...` logs `{ demo: true, text }` with message `log-demo` (logging-pipeline demo; delete when you don't need it) +| Method | Path | | +|--------|------|-| +| GET | `/stories` | published stories (`?mine=1` includes drafts) | +| GET | `/stories/:id` | story + `issues` | +| POST / PUT | `/stories`, `/stories/:id` | create / replace; 400 on bad shape | +| POST | `/stories/:id/publish` | 422 + `issues` if errors | +| POST | `/stories/:id/unpublish` | | +| DELETE | `/stories/:id` | 204 | -## Local dev - -```sh -cp .env.example .env # then export the vars, or use your shell's env loader -npm ci && npm run dev -npm run lint && npm test -``` - -Env is validated at boot (`src/lib/config.ts`): `PORT`, `SERVICE_NAME` (required), `LOG_LEVEL`, `DATABASE_URL?`, `REDIS_URL?`, `UNLEASH_URL?`, `UNLEASH_API_TOKEN?` (required with `UNLEASH_URL`), `UNLEASH_REFRESH_MS` (default 5000), `RUNTIME_CONFIG_PATH?`. -Migrations: SQL files in `migrations/`, applied with `npm run migrate`. -Logs: one JSON object per line on stdout (`level,time,msg,service,version`, plus `req.*`/`res.statusCode`/`responseTime` per request). - -## Seeing your logs - -Every container's stdout is shipped to Elasticsearch by Fluent Bit; nothing to configure per service. - -1. Open Kibana: http://kibana.192.168.1.83.nip.io, user `elastic`, password from - `kubectl get secret -n logging logging-es-elastic-user -o jsonpath='{.data.elastic}' | base64 -d`. -2. **Discover**, data view `kube-*` (the default). -3. Query with KQL, e.g. `service:"adventure" and demo:true`, or `service:"adventure" and level:"error"`. - -Any field you pass to the logger becomes a queryable field: -`req.log.info({ orderId }, "order placed")` → KQL `orderId:123`. The message string always -overrides a `msg` key in the object, so don't name a field `msg`. Lines are searchable within a few seconds; -retention is 14 days. - -## Feature flags - -Flags come from Unleash (UI: http://unleash.192.168.1.83.nip.io, environment `development`). -`src/lib/flags.ts` wraps the official `unleash-client` SDK; routes get it as `deps.flags`: - -```ts -if (deps.flags?.isEnabled("my-flag", { userId })) { ... } -const v = deps.flags?.getVariant("my-experiment"); // v.name, v.enabled, v.payload -``` - -- In the cluster the deployment sets `UNLEASH_URL=http://unleash.unleash.svc:4242/api` and - `UNLEASH_API_TOKEN` from Secret `unleash-client-token` (key `token`) in the service's namespace. -- The SDK polls every 5 s; flipping a flag in the UI changes behaviour within seconds, no redeploy. -- With `UNLEASH_URL` unset (local dev, tests) every flag reads `false` and one log line says so. - If Unleash is down, flags keep their last fetched value (or `false`) and the service keeps serving. -- Create the flag in the Unleash UI (project `default`) before using it; unknown flags read `false`. -- Tests: pass a fake `Flags` to `createApp` (see `src/app.test.ts`); never hit a real Unleash. -- `hello-banner` on `GET /` is the platform demo flag; delete it when you don't need it. - -## Runtime config - -Typed values you change with a git commit, live in running pods without a restart -(timeouts, limits, copy; not on/off-for-whom, that's a feature flag; never secrets). - -- Schema + defaults: `src/runtime-config.schema.ts` (zod). Every key needs a default. -- Read it per request: `deps.runtimeConfig.get().maxItems`. React to changes with - `runtimeConfig.onChange((values, changedKeys) => ...)`. -- In the cluster: ConfigMap `-runtime-config` (key `runtime.yaml`) in gitops - `manifests//runtime-config.yaml`, mounted as a directory at `/etc/app/`, - `RUNTIME_CONFIG_PATH=/etc/app/runtime.yaml`. Edit, commit, push: live within ~4 min - (ArgoCD polls every 3 min, kubelet refreshes the volume within ~1 min). - Log line `runtime config reloaded` lists `changedKeys`. -- A bad value (invalid YAML, fails the schema) keeps the last good values and logs `error` - with the zod issues. The pod never crashes on a config commit. Fix it with another commit. -- Local dev: `RUNTIME_CONFIG_PATH=./runtime.yaml npm run dev`; edits reload immediately. - Unset = schema defaults (`source: "defaults"`). -- `GET /admin/runtime-config` is unauthenticated, which is why nothing secret goes in here. - -## Using this template - -1. **Generate** a repo from this template in Forgejo (public). The repo name is substituted into `package.json`, this README, `.env.example`, and the first migration (CI derives the app name from the repo name itself). -2. **Set 3 Actions secrets** on the new repo: `REGISTRY_USER` (`nholtware`), `REGISTRY_TOKEN` (registry push token), `GITOPS_TOKEN` (ci-bot gitops token). -3. **Add gitops entries**: render `templates/backend/` (see its README) into `manifests//` and add `apps/.yaml` in `nholtware/gitops`. -4. **Push** to `main`. Generating a repo emits no push event, so CI only starts on your first push. +No auth (homelab). Without `DATABASE_URL` an in-memory store is used (tests).