docs: sync README and wiki with AFV, cluster, and REST surface

Bring operator and design docs in line with recent landings: -afv,
rqlite/FSD mesh, operator REST versioning, and Instructor1+ editor/sweatbox
authz. Mark AFV P0/mesh memory path and distributed openfsd as implemented.
This commit is contained in:
Reese Norris
2026-07-28 18:00:34 -04:00
parent a8cf860299
commit 227f20b912
13 changed files with 272 additions and 70 deletions

110
README.md
View File

@@ -13,28 +13,37 @@ As of May 2025, FSD is still used to facilitate over 140,000 active members conn
## Features
- Multiplayer flight simulation with VATSIM protocol compatibility
- Web-based management for users, settings, and connections
- SQLite for persistent storage (single file; easy backups)
- **Single binary** — FSD and web share one process and one database; enable services with CLI flags
- Web-based management: users, config, live connections, account self-service
- SQLite for persistent storage (single file; easy backups); optional **rqlite** for multi-node durable state
- **Single binary** — FSD, web, and optional AFV share one process and one database; enable services with CLI flags
- **Sweatbox** ground/taxi simulator + map-first **airport editor** (`.apt` / `.air`)
- **Operator REST** under `/api/v1` with date microversions, discovery, and embedded OpenAPI
- **Optional AFV** voice server (`-afv`: REST + UDP CryptoDTO) for TrackAudio / xPilot-shaped clients
- **Optional FSD cluster** (rqlite + TCP mesh) for multi-edge deployments
## Package layout
```
cmd/openfsd/ # Binary entrypoint (FSD + web; image CMD is /openfsd)
pkg/protocol/ # Pure wire format (parse/marshal; no I/O)
pkg/fsdclient/ # Mock/real FSD client for e2e and tools
pkg/twrfiles/ # Pure .apt/.air parse + format (sweatbox + editor)
internal/server/ # TCP accept, login, handlers, service HTTP
internal/session/ # Per-connection state + outbound send worker
internal/postoffice/ # Callsign registry + geospatial index
internal/geo/ # Pure haversine / bounding box
internal/auth/ # JWT + VATSIM client auth
internal/metar/ # METAR worker pool (injectable HTTP)
internal/db/ # Shared repositories + migrations
internal/serviceapi/ # Pure JSON DTOs for FSD service HTTP (shared by server + web)
internal/web/ # Gin MPA + /api/v1
internal/sweatbox/ # Ground/taxi sim (stdlib + geo)
cmd/aptdat2apt/ # Optional: convert XP12 apt.dat → sweatbox .apt (user-fetched data)
cmd/openfsd/ # Binary entrypoint (FSD + web; -afv opt-in)
cmd/openfsd-migrate-to-sqlite # Postgres → SQLite row copy
cmd/openfsd-migrate-to-rqlite # SQLite file → rqlite HTTP row copy
cmd/aptdat2apt/ # Optional: XP12 apt.dat sweatbox .apt
pkg/protocol/ # Pure FSD wire format (parse/marshal; no I/O)
pkg/fsdclient/ # Mock/real FSD client for e2e and tools
pkg/twrfiles/ # Pure .apt/.air parse + format
pkg/afvprotocol/ # Pure AFV CryptoDTO + DTOs + AEAD
internal/server/ # TCP accept, login, handlers, service HTTP, FSD mesh
internal/session/ # Per-connection state + outbound send worker
internal/postoffice/ # Callsign registry + geospatial index
internal/geo/ # Pure haversine / bounding box
internal/auth/ # JWT + VATSIM client auth
internal/metar/ # METAR worker pool (injectable HTTP)
internal/db/ # Shared repositories + migrations (sqlite | rqlite)
internal/serviceapi/ # Pure JSON DTOs for FSD service HTTP
internal/web/ # Gin MPA + progressive enhancement + /api/v1
internal/sweatbox/ # Ground/taxi sim (stdlib + geo + twrfiles)
internal/cluster/ # FSD mesh framing, claim, directory, interest
internal/afv/ # AFV REST + UDP voice + optional AFV mesh
```
### Airport layout data (sweatbox)
@@ -51,10 +60,11 @@ Sources, licensing (Gateway / Global Airports GPLv2), and packaging rules:
### Airport editor (web)
Administrators can author paired **`.apt`** (geometry) and **`.air`** (scenario
Instructors (**Instructor1+**) can author paired **`.apt`** (geometry) and **`.air`** (scenario
aircraft) files in the browser at **`/airport-editor`**:
- Map-first editing (Leaflet; JS required for the map; text paste + echo-download works without JS)
- Undo/redo (keyboard; client-only history stack)
- **No server persistence** of APT/AIR — open/download only (Blob download with JS; form echo-download without)
- Validate tab: live client parse + soft cross-file warnings; optional **Confirm with server**
(`POST /api/v1/editor/validate-apt` and `validate-air`)
@@ -62,30 +72,49 @@ aircraft) files in the browser at **`/airport-editor`**:
Design notes: [docs/design/apt-air-editor.md](docs/design/apt-air-editor.md).
### AFV voice (optional)
Opt-in with **`-afv`**. Serves AFV REST (auth, callsign session, transceivers) and UDP
CryptoDTO voice (H/HA heartbeat, AT→AR radio routing). Not started by default.
```bash
./openfsd -fsd -web -afv # colocated FSD + web + AFV
./openfsd -afv # AFV only (shares DATABASE_* with FSD for users)
```
Minimum config: set `AFV_UDP_ADVERTISE_IPV4` (and typically `AFV_API_PUBLIC_BASE_URL`) so
clients learn the correct voice endpoint. See [wiki/Configuration](wiki/Configuration.md#afv-voice-optional)
and design [docs/design/afv-server.md](docs/design/afv-server.md). Multi-node voice directory
relay is in-tree (MemoryMesh tests); production TCP mesh is PR-10b.
## Build and run
```bash
go build -o openfsd ./cmd/openfsd
./openfsd # both FSD and web (default)
./openfsd # FSD + web (default; AFV remains off)
./openfsd -fsd # FSD only (:6809 + service HTTP :13618)
./openfsd -web # web only (:8000)
./openfsd -afv # AFV voice only (REST + UDP)
./openfsd -fsd -web -afv
```
| Flag | Effect |
|------|--------|
| *(none)* | Both services |
| *(none)* | FSD + web (AFV off) |
| `-fsd` | FSD only |
| `-web` | Web only |
| `-fsd -web` | Both (same as default) |
| `-afv` | AFV only |
| `-fsd -web` | Both data plane + UI (same as default) |
| `-fsd -web -afv` | All three services in one process |
### Environment
| Variable | Default | Notes |
|----------|---------|-------|
| `DATABASE_SOURCE_NAME` | `openfsd.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)` | SQLite path (preferred) or `:memory:`; must be shared by both services. Bare `:memory:` is rewritten to a process-shared DSN when FSD+web run together. |
| `DATABASE_SOURCE_NAME` | `openfsd.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)` | SQLite path (preferred) or rqlite HTTP URL; must be shared by colocated services. Bare `:memory:` is rewritten to a process-shared DSN when ≥2 of FSD/web/AFV run together. |
| `DATABASE_DRIVER` | `sqlite` | `sqlite` (default) or `rqlite`; Postgres removed (use migrate tools) |
| `DATABASE_AUTO_MIGRATE` | `true` | FSD and web apply migrations on startup (idempotent) |
| `DATABASE_AUTO_MIGRATE` | `true` | Apply migrations on startup (idempotent; rqlite uses `DATABASE_MIGRATE_LEADER`) |
| `FSD_LISTEN_ADDRS` | `:6809` | FSD TCP listen address(es) |
| `SERVICE_HTTP_LISTEN_ADDR` | `:13618` | Internal FSD admin HTTP |
| `FSD_HTTP_SERVICE_ADDRESS` | `http://127.0.0.1:13618` | Web → FSD service HTTP |
@@ -93,6 +122,8 @@ go build -o openfsd ./cmd/openfsd
| `LOG_DEBUG` | *(unset)* | Set `true` for slog debug logging (default is info / release) |
| `GIN_MODE` | `release` | Gin mode for web + FSD service HTTP; set `debug` for Gin debug output |
AFV, cluster, and multi-FSD web vars: [wiki/Configuration](wiki/Configuration.md).
Colocated mode (default) uses the shared DB and in-process service HTTP. For `-web` against a remote FSD, set `FSD_HTTP_SERVICE_ADDRESS`.
## Quick start (Docker)
@@ -101,12 +132,14 @@ Preferred for operators. See the [Deployment Wiki](https://github.com/renorris/o
Images: **`ghcr.io/renorris/openfsd`** — CI publishes `:latest` from **`main` only**, `:dev` from **`dev`** (unstable), plus `sha-*` / branch tags.
**Upgrading from PostgreSQL?** Use [`openfsd-migrate-to-sqlite`](cmd/openfsd-migrate-to-sqlite) and [Migrating from PostgreSQL](wiki/Migrating-from-PostgreSQL.md). Optional multi-node: rqlite + mesh (`CLUSTER_ENABLED`, see [wiki/Deployment](wiki/Deployment.md) and `docs/design/distributed-openfsd.md`).
**Upgrading from PostgreSQL?** Use [`openfsd-migrate-to-sqlite`](cmd/openfsd-migrate-to-sqlite) and [Migrating from PostgreSQL](wiki/Migrating-from-PostgreSQL.md).
**Optional multi-node:** rqlite + FSD mesh (`CLUSTER_ENABLED`, see [wiki/Deployment](wiki/Deployment.md) and `docs/design/distributed-openfsd.md`). Sample: `docker-compose.cluster.yml`.
```bash
git clone https://github.com/renorris/openfsd.git
cd openfsd
docker compose up -d # pull/build single image; both services
docker compose up -d # pull/build single image; FSD + web
# or: docker compose up -d --build
```
@@ -118,7 +151,7 @@ docker compose up -d # pull/build single image; both services
### Service selection
```bash
# Both (default CMD)
# FSD + web (default CMD)
docker run --rm -p 6809:6809 -p 8000:8000 ghcr.io/renorris/openfsd:latest
# FSD only
@@ -128,6 +161,13 @@ docker run --rm -p 6809:6809 ghcr.io/renorris/openfsd:latest /openfsd -fsd
docker run --rm -p 8000:8000 \
-e FSD_HTTP_SERVICE_ADDRESS=http://fsd-host:13618 \
ghcr.io/renorris/openfsd:latest /openfsd -web
# AFV only (example ports; set advertise + public base URL for real clients)
docker run --rm -p 8080:8080 -p 50000:50000/udp \
-e AFV_API_LISTEN=0.0.0.0:8080 \
-e AFV_UDP_ADVERTISE_IPV4=host.example:50000 \
-e AFV_API_PUBLIC_BASE_URL=https://voice.example \
ghcr.io/renorris/openfsd:latest /openfsd -afv
```
### Local smoke
@@ -144,16 +184,22 @@ docker compose down
```bash
go test -race ./...
bash scripts/check-coverage.sh 80 # overall ≥80%; pure-pkg floors (see AGENTS.md)
bash scripts/check-webjs.sh # Node ≥20 unit tests for airport-editor JS modules
bash scripts/check-webjs.sh # Node ≥20 (CI pins 24) airport-editor JS modules
go test -bench=. -benchmem ./internal/postoffice/ ./pkg/protocol/
go test -tags=stress -count=1 -timeout=120s ./internal/server/ -run TestStress -v
```
E2E: `internal/server/e2e_test.go` via `pkg/fsdclient` + `StartTestServer`. Stress is optional (CI schedule / `workflow_dispatch`).
E2E: `internal/server` via `pkg/fsdclient` + `StartTestServer`. Stress is optional (CI schedule / `workflow_dispatch`).
## API
`/api/v1` covers auth, users, config, and FSD connections. See [internal/web](https://github.com/renorris/openfsd/tree/main/internal/web).
Operator automation under **`/api/v1`**: versioned discovery, users, config, FSD connections, sweatbox control, editor validate, and provisional account self-service.
- Operator guide + authz matrix: [`internal/web/README.md`](internal/web/README.md)
- OpenAPI (embedded): `GET /api/v1/openapi.yaml` or `internal/web/openapi/openapi.v1.yaml`
- Design: [docs/design/rest-api-versioning.md](docs/design/rest-api-versioning.md)
Send `OpenFSD-API-Version: YYYY-MM-DD` on production resource calls (see the web README).
## Protocol docs
@@ -163,3 +209,7 @@ Unofficial reverse-engineered FSD protocol docs live under `docs/`:
pip install mkdocs
mkdocs serve
```
## Operator wiki
Tracked source for the GitHub wiki is under [`wiki/`](wiki/) (Deployment, Configuration, Client Connection, Postgres migration).

View File

@@ -5,12 +5,21 @@
| **Title** | AFV multi-node mesh — directory sync + AT frame relay |
| **Author** | openfsd design (implementer-ready) |
| **Date** | 2026-07-28 |
| **Status** | Draft (rev 3 — hard testing bar + residual review) |
| **Status** | **Memory path Implemented** (PR-10 on `dev`: framing, Interest, AudioRelay, MemoryMesh e2e Cases AF, fail-closed `ENABLED` without TCP). **TCP mesh = PR-10b** (not in production binary). |
| **Parent** | `docs/design/afv-server.md` (Clustering, KD-8/9/16/17, PR-10) |
| **Contract** | `docs/design/afv-mesh-implementer-prompt.md` |
---
## Implementation status (as of 2026-07-28)
| Item | State |
|------|--------|
| Framing, Interest, remote directory, AudioRelay, MemoryMesh e2e | **Landed** (`internal/afv/mesh_*.go`, Cases AF) |
| Fail-closed `AFV_CLUSTER_ENABLED` without TCP | **Landed** |
| Production `TCPMesh` | **PR-10b** — not in binary; enable fails with clear error |
| Operator wiki for multi-node AFV | Single-node ops documented; multi-host voice deferred until TCP |
## Overview
P0 openfsd AFV is a single-node REST + UDP CryptoDTO voice service (`internal/afv`). Multi-region or multi-host voice requires two or more AFV processes to hear each other without shipping client AEAD keys off-node and without putting Opus into rqlite.

View File

@@ -5,15 +5,26 @@
| **Document** | AFV (Audio for VATSIM) server — full feature design |
| **Author** | _(design author / implementer)_ |
| **Date** | 2026-07-28 |
| **Status** | **Draft** (rev 2 — review e59fdb24 addressed) |
| **Status** | **P0 Implemented** (single-node REST + UDP on `dev`; design history retained). Mesh: MemoryMesh path landed (PR-10); production TCP = PR-10b. |
| **Project** | openfsd |
| **Target land path** | `docs/design/afv-server.md` (when accepted) |
| **Target land path** | `docs/design/afv-server.md` |
| **Related** | `Agents.md`, `docs/design/distributed-openfsd.md`, `internal/auth`, `internal/geo`, `internal/cluster`, `internal/db`, `cmd/openfsd/main.go`, [AFV-Native](https://github.com/xsquawkbox/AFV-Native) (BSD-3) |
| **Protocol source of truth** | AFV-Native headers/sources (Chris Collins); also used by TrackAudio, xPilot, VectorAudio, vPilot (VATSIM clients) |
| **Revision** | rev 2: JWT/`internal/auth` alignment; normative HA recipe; bootstrap; mesh/XC/concurrency/coverage/PR-plan fixes |
---
## Implementation status (as of 2026-07-28)
| Slice | State |
|-------|--------|
| P0 single-node (`pkg/afvprotocol`, `internal/afv`, `-afv`) | **Landed** — REST auth/callsign/transceivers; UDP H/HA + AT→AR; range model; reaper; empty stations route |
| PR-10 mesh (MemoryMesh + framing + Interest + AudioRelay) | **Landed** (in-process / test mesh; e2e Cases AF) |
| PR-10b production TCP mesh | **Not landed**`AFV_CLUSTER_ENABLED=true` fails closed without TCP in binary |
| Operator docs | Wiki Configuration/Deployment AFV sections; root README |
Design body below is the engineering record (decisions, protocol, PR plan). Prefer wiki + `internal/afv` for day-to-day ops.
## Overview
openfsd today is an FSD data-plane + boring-web control plane. Clients (vPilot, xPilot, TrackAudio, AFV-Native) obtain **voice** from a completely separate service: **Audio for VATSIM (AFV)**. AFV is a two-plane system:

View File

@@ -16,7 +16,7 @@
> **Closeout (2026-07-27):** Code landed in `history.js` / `main.js` with pure tests under `webjs/airport-editor/history.test.js`. Keyboard-only undo/redo (`Mod+Z` / `Mod+Shift+Z` / `Mod+Y` when `!inField`), gesture-coalesced vertex/aircraft drag, snapshot stack (max depth 100), and K13 clean-content hash seeding at bootstrap / successful Open / New. Design history below is retained as the engineering record (key decisions, mutation inventory, checklist). Parent dirty-hash baseline prose retconned in [`apt-air-editor.md`](apt-air-editor.md).
The openfsd **Airport Editor** (`/airport-editor`) is a map-first, Admin-only progressive-enhancement tool for authoring paired `.apt` / `.air` documents entirely in the browser. Geometry, headers, and aircraft are mutated in place via `model.js` helpers. Before this work there was **no** undo stack: a mis-dragged vertex, accidental surface delete, or botched raw Apply was permanent until the user re-opened a file.
The openfsd **Airport Editor** (`/airport-editor`) is a map-first, Instructor1+ progressive-enhancement tool for authoring paired `.apt` / `.air` documents entirely in the browser. Geometry, headers, and aircraft are mutated in place via `model.js` helpers. Before this work there was **no** undo stack: a mis-dragged vertex, accidental surface delete, or botched raw Apply was permanent until the user re-opened a file.
This design adds a **pure, Node-testable history module** and thin wiring in `main.js` so every **committed** document mutation is undoable/redoable via keyboard shortcuts. History is **snapshot-based** (deep clones of airport + aircraft + selection), with **gesture coalescing** so a vertex/aircraft drag is one undo step, not one step per pointer move. Dirty tracking continues to use FNV clean-content hashes (`last*DownloadHash`, K13); download itself is not an undo entry. **No toolbar undo/redo buttons** — keyboard only. No server persistence of history.

View File

@@ -19,8 +19,8 @@ Shipped. Summary of the tree as of closeout:
|------|------------------|
| Parse + Format | `pkg/twrfiles` (`ParseAPT`/`ParseAIR`, `FormatAPT`/`FormatAIR`, fixtures under `pkg/twrfiles/testdata`) |
| Sweatbox | Thin aliases/wrappers over `pkg/twrfiles` (engine still pure) |
| Editor MPA | `/airport-editor` shell + Leaflet PE; echo-download; Admin-only |
| Validate API | `POST /api/v1/editor/validate-apt` / `validate-air` via `pkg/twrfiles` |
| Editor MPA | `/airport-editor` shell + Leaflet PE; echo-download; **Instructor1+** (product KD-5; was Admin-only in original design) |
| Validate API | `POST /api/v1/editor/validate-apt` / `validate-air` via `pkg/twrfiles` (**Instructor1+**) |
| Web JS | Pure modules under `internal/web/static/js/openfsd/`; Node tests in `webjs/` + `scripts/check-webjs.sh` in CI |
| Persistence | **None** durable (no disk/DB); Blob download + transient request bodies only |
| Playwright | **Cancelled** — no browser automation suite |
@@ -35,7 +35,7 @@ Design history below is retained. Stale “Format missing / JS tests none” row
openfsd already loads TWRTrainer-compatible **`.apt`** (airport geometry) and **`.air`** (scenario aircraft snapshots) into an in-process sweatbox simulator and exposes an instructor control panel at `/sweatbox`. Instructors can also author those files in-product via the integrated map editor.
This design delivers a **single, integrated map-first editor** on the openfsd admin Web UI for creating and editing **paired** `.apt` and `.air` files. The editor is a **server-rendered MPA shell** with a **JavaScript-heavy progressive enhancement** (Leaflet map + geometry tools). **No durable server persistence** of `.apt` / `.air` (no disk, no DB): primary load is client-local (FileReader / paste) and primary save is **browser Blob download**. Optional Admin-only echo-download and validate POSTs may hold bodies **transiently in process memory** for the duration of the request only — they never write files or store rows.
This design delivers a **single, integrated map-first editor** on the openfsd admin Web UI for creating and editing **paired** `.apt` and `.air` files. The editor is a **server-rendered MPA shell** with a **JavaScript-heavy progressive enhancement** (Leaflet map + geometry tools). **No durable server persistence** of `.apt` / `.air` (no disk, no DB): primary load is client-local (FileReader / paste) and primary save is **browser Blob download**. Optional Instructor1+ echo-download and validate POSTs may hold bodies **transiently in process memory** for the duration of the request only — they never write files or store rows.
The page targets a dense **16:9 operator console** matching the sweatbox rail aesthetic, reuses in-tree Leaflet assets, and respects the boring-web house standard and the web ↛ sweatbox import-graph edge.
@@ -94,7 +94,7 @@ Airport geometry is inherently geographic. A text-only form would satisfy “no
1. **Integrated .apt + .air editor** on one bookmarkable URL, map canvas + dense rail.
2. **OpenStreetMap base map** with airport-intuitive layering (documented tile choice).
3. **Utilitarian 16:9 console** consistent with sweatbox visual language (tokens, panels, mono numbers).
4. **No durable server persistence** of APT/AIR (disk/DB); document lives in the browser; download only. Transient request bodies allowed for echo-download and validate (Admin-only, size-capped).
4. **No durable server persistence** of APT/AIR (disk/DB); document lives in the browser; download only. Transient request bodies allowed for echo-download and validate (Instructor1+, size-capped).
5. **Boring-web compliance**: real route, server authz, no SPA router, no global server-state store, CSRF only when mutating the server. Map geometry authoring is a **documented complexity-gate exception** (like dashboard Leaflet); no-JS paste + echo-download remains for the data path.
6. **JS heavily tested**: unit coverage as close to 100% as practical on pure modules; e2e covering editor flows.
7. **Go tests** for any new routes/handlers (authz, shell render, optional echo-download).
@@ -286,7 +286,7 @@ Also update `Agents.md` §1 package table: sweatbox = stdlib + `internal/geo` +
|------|--------|
| **URL** | `GET /airport-editor` |
| **Auth** | Session cookie (`requireSessionHTML`) |
| **Authz** | **Administrator** (`requireMinRatingHTML(protocol.NetworkRatingAdministator)`) — same tier as `/sweatbox` and config editor |
| **Authz** | **Instructor1+** (`requireMinRatingHTML(protocol.NetworkRatingInstructor1)`) — same tier as `/sweatbox`; config editor remains Administrator |
| **Nav** | Layout header: link **Airport Editor** next to Sweatbox when `User.CanEditConfig` |
| **Does not require** | `SWEATBOX_ENABLED` or FSD process |
@@ -861,7 +861,7 @@ Content-Disposition: attachment; filename="airport.apt"
- No `os.WriteFile`, `os.Create`, `os.OpenFile` for user content; no DB repository calls.
- Go tests: snapshot workdir (or `t.TempDir` as cwd if needed) before/after — file set unchanged.
- `slog` allowlist: `cid`, `content_length`, `icao` (if parsed), `error_count`, `path`**never** full body text.
- Transient RAM only for request lifetime; Admin-only so body-in-RAM is accepted risk.
- Transient RAM only for request lifetime; Instructor1+ so body-in-RAM is accepted risk.
Default filename: from form field `filename` if safe (`^[A-Za-z0-9._-]{1,64}$`), else `airport.apt` / `scenario.air`. Prefer ICAO when parse succeeds: `KBTV.apt`. **Do not block download on validation errors** (WIP authoring); optional `strict=1` may reject later — not v1 default.
@@ -880,7 +880,7 @@ api.POST("/validate-air", s.handleAPIValidateAIR)
| Condition | Response |
|-----------|----------|
| Unauthenticated | Existing `jwtBearerMiddleware` behavior (401 JSON) |
| Authenticated but rating < Administrator | **`403`** + `writeAPIV1Response(c, http.StatusForbidden, &genericAPIV1Forbidden)` — same as `handleAPISweatboxState` / `api_tokens.go` |
| Authenticated but rating < Instructor1 | **`403`** + `writeAPIV1Response(c, http.StatusForbidden, &genericAPIV1Forbidden)` — same as sweatbox JSON proxies |
| Never | `303` redirect to `/dashboard` (that is **HTML-only** middleware) |
**Do not** introduce `requireMinRatingAPI` that wraps `requireMinRatingHTML`. Prefer **inline rating check** after claims (copy sweatbox API pattern). If a shared helper is added later, it must return JSON 403 only and must not redirect.
@@ -966,7 +966,7 @@ webjs/
format-air.test.js
model.test.js
README.md # how to run; Node 20 LTS pin
README.md # how to run; Node 20 floor (CI pins 24)
```
```text
@@ -997,7 +997,7 @@ cd "$(dirname "$0")/../webjs"
node --test airport-editor/**/*.test.js
```
- Pin **Node 24** in CI (`actions/setup-node@v7` with `node-version: "24"`; local floor ≥20).
- Pin **Node 24** in CI (`actions/setup-node@v7` with `node-version: "24"`; local floor ≥20). *(Landed: CI workflow uses Node 24.)*
- Prefer **zero npm deps** for unit tests (`node:test` only). If `package.json` scripts only, `npm test` can invoke node without `node_modules`.
- Add `node_modules/` to `.gitignore` if any package is ever added.
- Update `Agents.md` §9 and CI workflow in **PR 4**.
@@ -1124,10 +1124,10 @@ Update `scripts/check-import-graph.sh`: `check_stdlib_only` for `pkg/twrfiles`;
| Open redirect | N/A |
| Tile server data leakage | Only lat/lon tile coords leave browser to OSM/Esri — expected for maps |
| Sensitive scenario data in localStorage | v1: do not persist full drafts |
| Validate API as parse oracle | Admin-only; rate not critical; still size-capped |
| Validate API as parse oracle | Instructor1+; rate not critical; still size-capped |
| Content-Disposition injection | Sanitize filename (no CR/LF/quotes) |
| Accidental durable write | Handler criteria: no file/DB APIs; PE tests assert workdir unchanged |
| Body in RAM (echo/validate) | Admin-only + 2 MiB cap; transient request scope only — **not** a persistence path |
| Body in RAM (echo/validate) | Instructor1+ + 2 MiB cap; transient request scope only — **not** a persistence path |
**Persistence wording (airtight):**
@@ -1290,7 +1290,7 @@ bash scripts/check-webjs.sh
| 5 | PR6: read-only map; PR7: edit + Blob; PR8: validation polish |
| 6 | ~~PR9 Playwright~~ **cancelled** — docs only; no browser automation |
**Feature flag:** not required — Admin-only page is the gate. If needed, env `AIRPORT_EDITOR_ENABLED=false` can hide route; **default on** once shipped.
**Feature flag:** not required — Instructor1+ page authz is the gate. If needed, env `AIRPORT_EDITOR_ENABLED=false` can hide route; **default on** once shipped.
**Rollback:** revert PRs; no DB migration. Static embed reverts with binary.
@@ -1300,7 +1300,7 @@ bash scripts/check-webjs.sh
## Open Questions
1. **Should Supervisors access the editor?** **Resolved for v1:** Admin-only (product default; Supervisor access deferred).
1. **Should Supervisors / instructors access the editor?** **Resolved:** **Instructor1+** (aligned with sweatbox; see user-dashboard KD-5). Original v1 draft was Admin-only.
2. **Esri imagery ToS** for each deployer's traffic profile — keep optional and documented.
3. **Undo stack?** **Shipped** — keyboard-only snapshot history; see `docs/design/airport-editor-undo-redo.md`. (Earlier “deferred v1.1” superseded.)
4. **~~Playwright CI~~** — **cancelled.** No browser automation suite.
@@ -1314,7 +1314,7 @@ Resolved by this rev: WIP download allowed (yes); fixture path = `pkg/twrfiles/t
| # | Decision | Rationale |
|---|----------|-----------|
| 1 | **Single page `/airport-editor`**, not nested under live sweatbox | Independent of `SWEATBOX_ENABLED`; clear authoring vs control-plane split |
| 2 | **Admin-only** (same as `/sweatbox`) | Consistent training-ops audience; tight authz |
| 2 | **Instructor1+** (same as `/sweatbox`; product update from Admin-only) | Training-ops audience; instructors author layouts |
| 3 | **No durable server persistence**; Blob primary; transient echo/validate only | User requirement; RAM OK for Admin-sized POSTs |
| 4 | **Optional form echo-download** for no-JS | Boring-web data path without map |
| 5 | **`pkg/twrfiles` stdlib package** + **sweatbox type aliases/wrappers in PR1** | Web cannot import sweatbox; sim blast radius controlled; Format missing today |

View File

@@ -5,17 +5,29 @@
| **Document** | Distributed multi-node openfsd (mesh + rqlite) |
| **Author** | _(design author / implementer)_ |
| **Date** | 2026-07-28 |
| **Status** | **Draft** (rev 4 — product decisions: full mesh path, synthetic #DP, FPL cache) |
| **Status** | **Implemented (opt-in)** on `dev` — rqlite driver, FSD TCP mesh, `docker-compose.cluster.yml`, migrate-to-rqlite, wiki Deployment/Configuration. Single-node SQLite remains default. Design history retained. |
| **Project** | openfsd |
| **Target land path** | `docs/design/distributed-openfsd.md` (when accepted) |
| **Target land path** | `docs/design/distributed-openfsd.md` |
| **Related** | `Agents.md`, `internal/postoffice`, `internal/server/deps.go`, `internal/db/*`, `internal/serviceapi`, `docs/design/sweatbox-integrated-simulator.md`, `wiki/Deployment.md`, rqlite docs |
| **Revision** | rev 4: product G0=full mesh; synthetic #DP on peer death; FPL cache+RPC miss; ≤8 peers; ring reconfig remains ops-drain v1 |
---
## Implementation status (as of 2026-07-28)
| Slice | State |
|-------|--------|
| rqlite driver + migrate tool | **Landed** (`DATABASE_DRIVER=rqlite`, `cmd/openfsd-migrate-to-rqlite`) |
| FSD TCP mesh (claim, directory, interest, HomeRPC) | **Landed** (`internal/cluster`, `CLUSTER_*`) |
| Sample multi-node compose | **Landed** (`docker-compose.cluster.yml`) |
| Operator wiki | **Landed** (Deployment multi-node section, Configuration cluster env) |
| Default path | Still **single-node SQLite** (`CLUSTER_ENABLED=false`) |
Design body below retains the full plan and feasibility report.
## Overview
openfsd today is a **single-process, single-node** FSD stack: gnet TCP plane, in-process `postoffice` registry (lock-free O(N) slab scan, VATSIM-scale ≤~15k), session-local flight plans, SQLite for users/config, and a boring-web control plane that talks to FSD only via service HTTP. That design is correct and fast for club / VA / regional networks. It does **not** give VATSIM-style properties: clients cannot pin to a geographically nearest edge, formation flying across distant peers cannot exploit local RTT, and a node death takes the whole network offline.
openfsds **default** is a **single-process, single-node** FSD stack: gnet TCP plane, in-process `postoffice` registry (lock-free O(N) slab scan, VATSIM-scale ≤~15k), session-local flight plans, SQLite for users/config, and a boring-web control plane that talks to FSD only via service HTTP. That design is correct and fast for club / VA / regional networks. Without the optional cluster path it does **not** give VATSIM-style properties: clients cannot pin to a geographically nearest edge, formation flying across distant peers cannot exploit local RTT, and a node death takes the whole network offline.
This document is a **full implementation plan plus an honest feasibility/rationality report** for making openfsd **optionally distributed**:

View File

@@ -5,7 +5,7 @@
| **Document** | Operator REST surface expansion + durable versioning/compatibility |
| **Author** | _(design author / implementer)_ |
| **Date** | 2026-07-28 |
| **Status** | **Accepted** (design review consensus 2026-07-28) |
| **Status** | **Accepted + Implemented** (design review 2026-07-28; middleware, discovery, OpenAPI embed, first-train routes, goldens, operator guide in `internal/web/README.md`) |
| **Project** | openfsd |
| **Target land path** | `docs/design/rest-api-versioning.md` |
| **Related** | `internal/web/README.md`, `Agents.md` §2/§6, `docs/design/user-dashboard-self-service.md`, `docs/design/sweatbox-integrated-simulator.md`, `internal/serviceapi/*` |

View File

@@ -18,5 +18,15 @@ See [openfsd](https://github.com/renorris/openfsd) for a free and open-source FS
| Enumerations | [enumerations.md](enumerations.md) |
| FSD JWT login tokens | [authentication-token.md](authentication-token.md) |
| In-band client authenticity (`$ZC`/`$ZR`) | [vatsim-auth.md](vatsim-auth.md) |
| X-Plane airport data (sweatbox layouts) | [xplane-airport-data.md](xplane-airport-data.md) |
## Related openfsd docs (outside this protocol set)
| Topic | Location |
|-------|----------|
| Operator wiki (deploy / config / clients) | Repository [`wiki/`](../wiki/) |
| Operator REST + OpenAPI | [`internal/web/README.md`](../internal/web/README.md) |
| Design notes (AFV, cluster, sweatbox, editor) | [`docs/design/`](design/) |
| Contributor package rules | [`AGENTS.md`](../AGENTS.md) |
*This documentation is an independent work and is not affiliated with, endorsed by, or associated with VATSIM, Inc.*

View File

@@ -44,3 +44,16 @@ See [vPilot Patch Utility](https://github.com/renorris/vpilot-patch-utility)
## xPilot
See [here](https://github.com/renorris/openfsd-client-patch-utility).
## Voice (AFV)
FSD data and **Audio for VATSIM (AFV)** voice are separate attachment planes. openfsd can serve AFV when started with **`-afv`** (see [Configuration](Configuration.md#afv-voice-optional) and [Deployment](Deployment.md#optional-afv-voice)).
Clients such as **TrackAudio**, **xPilot**, and **VectorAudio** (AFV-Native-derived) need:
1. FSD host/port (or private-server entry) as usual
2. A **voice base URL** pointing at the AFV REST API (`AFV_API_PUBLIC_BASE_URL` / your published HTTPS URL)
AFV authenticates against the same certificate database as FSD (CID + password). UDP voice endpoints are advertised in the AFV channel config from `AFV_UDP_ADVERTISE_IPV4` (must be reachable by clients, including through NAT).
Without `-afv`, use an external voice solution (Discord, etc.); FSD multiplayer still works independently of voice.

View File

@@ -2,7 +2,7 @@
## Persistent configuration (database)
Some settings are stored in the SQLite database and shared by FSD and web when they use the same file.
Some settings are stored in the SQLite (or rqlite) database and shared by FSD, web, and AFV when they use the same DSN.
**Important values to set after first boot:**
@@ -25,7 +25,7 @@ Use the web UI (**Configure Server**) to set these.
| Name | Description | Default |
|------|-------------|---------|
| `DATABASE_SOURCE_NAME` | SQLite DSN (file path preferred) **or** rqlite HTTP base URL when `DATABASE_DRIVER=rqlite` (e.g. `http://rqlite:4001`). Bare `:memory:` is private per connection; when both services run in one process it is rewritten to a shared in-memory DSN. | `openfsd.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)` |
| `DATABASE_SOURCE_NAME` | SQLite DSN (file path preferred) **or** rqlite HTTP base URL when `DATABASE_DRIVER=rqlite` (e.g. `http://rqlite:4001`). Bare `:memory:` is private per connection; when ≥2 of FSD/web/AFV run in one process it is rewritten to a shared in-memory DSN. | `openfsd.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)` |
| `DATABASE_DRIVER` | `sqlite` (default) or `rqlite`. Legacy `postgres` fails startup — see [Migrating from PostgreSQL](Migrating-from-PostgreSQL.md). | `sqlite` |
| `DATABASE_AUTO_MIGRATE` | When `true`, apply migrations on startup. For rqlite, only the process with `DATABASE_MIGRATE_LEADER=true` migrates. | `true` |
| `DATABASE_MIGRATE_LEADER` | When `true` with rqlite, this process runs schema migrations (exactly one leader). | `false` |
@@ -68,12 +68,60 @@ Default is **off**. See [Deployment](Deployment.md#optional-multi-node-cluster)
| `FSD_HTTP_SERVICE_ADDRESS` | Base URL of the FSD internal HTTP API. Default assumes colocated FSD. | `http://127.0.0.1:13618` |
| `FSD_HTTP_SERVICE_ADDRESSES` | Multi-FSD list: bare URLs or `nodeID=http://host:port` pairs (kick routing + online_users aggregate) | *(empty → single address)* |
| `COOKIE_SECURE` | `true` / `false` force Secure cookies; empty derives from TLS / `X-Forwarded-Proto` | *(empty)* |
| `ALLOW_PERMANENT_ACCOUNT_DELETE` | When `true`, account self-service may hard-delete; default soft-delete only | `false` |
### AFV voice (optional)
Enabled only when the process is started with **`-afv`**. Shares `DATABASE_*` with FSD/web for certificate auth. Design: `docs/design/afv-server.md`.
| Name | Description | Default |
|------|-------------|---------|
| `AFV_API_LISTEN` | REST API listen address | `127.0.0.1:8080` |
| `AFV_API_PUBLIC_BASE_URL` | Public base URL clients use for REST (scheme + host) | *(empty)* |
| `AFV_UDP_LISTEN` | UDP CryptoDTO listen address | `0.0.0.0:50000` |
| `AFV_UDP_ADVERTISE_IPV4` | **Required to start UDP** — host:port embedded in channel config for clients | *(empty)* |
| `AFV_UDP_ADVERTISE_IPV6` | Optional IPv6 advertise host:port | *(empty)* |
| `AFV_TLS_CERT_FILE` / `AFV_TLS_KEY_FILE` | TLS for REST (prefer in production) | *(empty → HTTP)* |
| `AFV_JWT_SECRET` | Override JWT secret; else shared config KV `JWT_SECRET_KEY` | *(empty)* |
| `AFV_JWT_TTL` | AFV bearer token lifetime | `1h` |
| `AFV_HEARTBEAT_TIMEOUT` | UDP heartbeat timeout | `10s` |
| `AFV_SESSION_IDLE_TIMEOUT` | Idle session reaper | `30s` |
| `AFV_MAX_SESSIONS` | Global session cap | `5000` |
| `AFV_MAX_SESSIONS_PER_CID` | Per-CID session cap | `5` |
| `AFV_AUTH_FAIL_MAX` / `AFV_AUTH_FAIL_WINDOW` | Auth brute-force window | `20` / `1m` |
| `AFV_MAX_DATAGRAM` | Reject larger UDP frames before decrypt | `8192` |
| `AFV_REQUIRE_FSD_ONLINE` | Gate voice on FSD online_users | `false` |
| `AFV_FSD_HTTP_SERVICE_ADDRESS` | Service HTTP for online gate | `http://127.0.0.1:13618` |
| `AFV_CALLSIGN_STRICT` | Reject replace on callsign conflict (default allows replace) | `false` |
| `AFV_RANGE_UNICOM_NM` | Unicom max range (NM) | `15` |
| `AFV_RANGE_DEFAULT_NM` | Default pilot max range (NM) | `40` |
| `AFV_RANGE_ATC_NM` | ATC max range (NM) | `150` |
| `AFV_RANGE_EDGE_RATIO` | Range edge ratio for volume falloff | `0.1` |
| `AFV_STATIONS_FILE` | Optional station alias file (empty → `[]` aliases) | *(empty)* |
| `AFV_CROSS_COUPLE` | Multi-freq ATC cross-coupling | `true` |
| `AFV_CLUSTER_ENABLED` | AFV multi-node mesh | `false` |
| `AFV_CLUSTER_NODE_ID` | AFV mesh node id | *(required when enabled)* |
| `AFV_CLUSTER_LISTEN` | AFV mesh listen | *(required when enabled)* |
| `AFV_CLUSTER_PEERS` | `id=host:port,...` (max 4 remote) | *(required when enabled)* |
| `AFV_CLUSTER_PSK` | AFV mesh shared secret | *(required when enabled)* |
**Note:** `AFV_CLUSTER_ENABLED=true` currently **fails closed** without production TCP mesh in the binary (MemoryMesh is tests-only). See [Deployment](Deployment.md#optional-afv-voice).
Minimal single-node AFV example:
```text
AFV_API_LISTEN=0.0.0.0:8080
AFV_API_PUBLIC_BASE_URL=https://voice.example.com
AFV_UDP_LISTEN=0.0.0.0:50000
AFV_UDP_ADVERTISE_IPV4=voice.example.com:50000
DATABASE_SOURCE_NAME=/db/openfsd.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)
```
### Logging
| Name | Description | Default |
|------|-------------|---------|
| `LOG_DEBUG` | Set to `true` for slog debug logging (FSD + web). Default is info / release. | *(unset)* |
| `LOG_DEBUG` | Set to `true` for slog debug logging (FSD + web + AFV). Default is info / release. | *(unset)* |
| `GIN_MODE` | Gin mode for the web UI and FSD service HTTP. openfsd defaults to `release` when unset (Gins own default is `debug`). | `release` (when unset) |
| `GIN_LOGGER` | When set (any non-empty value), enables Gins request logger middleware on the web server. | *(unset)* |
@@ -81,7 +129,9 @@ Default is **off**. See [Deployment](Deployment.md#optional-multi-node-cluster)
| Flag | Effect |
|------|--------|
| *(none)* | Both FSD and web |
| *(none)* | FSD + web (AFV off) |
| `-fsd` | FSD only |
| `-web` | Web only |
| `-fsd -web` | Both (same as default) |
| `-afv` | AFV voice only |
| `-fsd -web` | FSD + web (same as default) |
| `-fsd -web -afv` | All three services in one process |

View File

@@ -4,10 +4,11 @@ openfsd is a **single binary** (`openfsd`) that can run:
1. The FSD server (TCP protocol + internal service HTTP)
2. The web UI and `/api/v1`
3. Optionally, the **AFV** voice API + UDP voice server (`-afv`)
By default both run in one process. Use CLI flags to select services.
By default **FSD + web** run in one process (AFV stays off). Use CLI flags to select services.
Storage is **SQLite only** (a file on disk, or `:memory:` for ephemeral use). If you are upgrading from a PostgreSQL deployment, see [Migrating from PostgreSQL](Migrating-from-PostgreSQL.md).
Storage is **SQLite only** by default (a file on disk, or `:memory:` for ephemeral use). Optional multi-node uses **rqlite** for durable users/config. If you are upgrading from a PostgreSQL deployment, see [Migrating from PostgreSQL](Migrating-from-PostgreSQL.md).
## Docker Compose (recommended)
@@ -32,7 +33,7 @@ Images: **`ghcr.io/renorris/openfsd`** — CI publishes `:latest` from **`main`
### Service selection
```bash
# Both (default CMD)
# FSD + web (default CMD)
docker run --rm -p 6809:6809 -p 8000:8000 ghcr.io/renorris/openfsd:latest
# FSD only
@@ -42,6 +43,14 @@ docker run --rm -p 6809:6809 ghcr.io/renorris/openfsd:latest /openfsd -fsd
docker run --rm -p 8000:8000 \
-e FSD_HTTP_SERVICE_ADDRESS=http://fsd-host:13618 \
ghcr.io/renorris/openfsd:latest /openfsd -web
# AFV only (set advertise + public URL for real clients)
docker run --rm -p 8080:8080 -p 50000:50000/udp \
-e AFV_API_LISTEN=0.0.0.0:8080 \
-e AFV_UDP_ADVERTISE_IPV4=your.public.host:50000 \
-e AFV_API_PUBLIC_BASE_URL=https://voice.example \
-e DATABASE_SOURCE_NAME=/path/to/shared.db?... \
ghcr.io/renorris/openfsd:latest /openfsd -afv
```
Back up the SQLite volume (or the `.db` file) regularly.
@@ -54,12 +63,14 @@ go build -o openfsd ./cmd/openfsd
export DATABASE_SOURCE_NAME=./openfsd.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)
export DATABASE_AUTO_MIGRATE=true
./openfsd # both FSD and web
./openfsd # FSD + web
./openfsd -fsd # FSD only
./openfsd -web # web only
./openfsd -afv # AFV only
./openfsd -fsd -web -afv
```
See [Configuration](Configuration.md) for the full environment list.
See [Configuration](Configuration.md) for the full environment list (including `AFV_*`).
## Windows
@@ -85,7 +96,9 @@ To reset the database, delete the created `openfsd.db` file next to `run-windows
- Prefer a single pod/process with a persistent volume for the SQLite file for small/medium networks.
- If you split `-fsd` and `-web` across hosts, set `FSD_HTTP_SERVICE_ADDRESS` on the web process to the FSD internal service HTTP address (default `http://127.0.0.1:13618` when colocated).
- Multi-FSD web: set `FSD_HTTP_SERVICE_ADDRESSES` (`nodeID=http://host:port,...`) for online_users aggregation and kick routing.
- Do not put two writers on the same SQLite file over NFS without understanding SQLite locking limits; local disk or a block volume is preferred.
- AFV may run colocated (`-fsd -web -afv`) or as a separate process sharing the same user database DSN.
## Optional multi-node cluster
@@ -100,6 +113,7 @@ See also `docs/design/distributed-openfsd.md` and the sample compose file `docke
| Durable users/config | **rqlite** (Raft SQLite) | `DATABASE_DRIVER=rqlite`, HTTP DSN |
| Live callsigns/positions | In-process postoffice + **TCP mesh** | Never stored in rqlite |
| Control plane | Web ×N → service HTTP on each FSD | `FSD_HTTP_SERVICE_ADDRESSES` |
| Voice (optional) | AFV process(es) | Separate from FSD mesh; see AFV section |
### Required env (FSD edges)
@@ -154,3 +168,20 @@ Default `docker compose up` remains **one container + SQLite**.
### CI note
Optional multi-node compose is **manual / ops** (`docker compose -f docker-compose.cluster.yml`). Default CI remains single-node `go test` + MemoryMesh unit/e2e tests (no required cluster compose job in v1).
## Optional AFV voice
AFV is **opt-in** (`-afv`). It is not started by the default Docker CMD.
| Plane | Port (defaults) | Notes |
|-------|-----------------|--------|
| AFV REST | `AFV_API_LISTEN` default `127.0.0.1:8080` | Auth, callsign, transceivers; prefer TLS in production |
| AFV UDP | `AFV_UDP_LISTEN` default `0.0.0.0:50000` | CryptoDTO Opus relay (opaque; no decode) |
**Required for clients to connect UDP:** `AFV_UDP_ADVERTISE_IPV4` (and optionally `AFV_UDP_ADVERTISE_IPV6`) — the address embedded in channel config for clients. Behind NAT, this must be the **public** host:port.
Share the same `DATABASE_*` as FSD so AFV authenticates against the same certificates. Optional `AFV_REQUIRE_FSD_ONLINE=true` gates voice sessions on the FSD online_users service.
Multi-node AFV mesh (`AFV_CLUSTER_*`) directory + AT relay is implemented for in-process/MemoryMesh testing; **production TCP mesh is not enabled** in the binary yet (`AFV_CLUSTER_ENABLED=true` fails closed until PR-10b). See `docs/design/afv-server.md` and `docs/design/afv-mesh-pr10.md`.
Full env list: [Configuration](Configuration.md#afv-voice-optional).

View File

@@ -8,20 +8,36 @@ Operator documentation for [openfsd](https://github.com/renorris/openfsd).
| Page | Description |
|------|-------------|
| [Deployment](Deployment.md) | Docker Compose, Windows, and single-binary run |
| [Configuration](Configuration.md) | Env vars and persistent DB settings |
| [Client Connection](Client-Connection.md) | VRC, Euroscope, Swift, vPilot, xPilot |
| [Deployment](Deployment.md) | Docker Compose, Windows, single-binary run, optional cluster + AFV |
| [Configuration](Configuration.md) | Env vars (FSD, web, cluster, AFV) and persistent DB settings |
| [Client Connection](Client-Connection.md) | VRC, Euroscope, Swift, vPilot, xPilot; optional AFV voice |
| [Migrating from PostgreSQL](Migrating-from-PostgreSQL.md) | Convert an existing Postgres DB to SQLite |
## Admin web tools (in the product UI)
## Admin / instructor web tools (in the product UI)
| URL | Who | Notes |
|-----|-----|--------|
| `/sweatbox` | Administrator | Live ground/taxi simulator control (needs FSD + sweatbox enabled) |
| `/sweatbox/manual` | Administrator | Instructor user manual (also linked from the control panel, opens in a new tab) |
| `/airport-editor` | Administrator | Map-first `.apt` / `.air` authoring; **download only** (no server save). Validate tab has live client checks + optional server confirm. Download files, then load them on `/sweatbox`. Design: repo `docs/design/apt-air-editor.md` |
| `/dashboard` | Any signed-in user (OBS+) | Connection summary (HTML); optional Leaflet map PE |
| `/account` | Any signed-in user | Change password; soft-delete account (optional hard-delete via env) |
| `/usereditor` | Supervisor+ | Certificate directory create/update |
| `/configeditor` | Administrator | Server config, API tokens, JWT secret reset |
| `/sweatbox` | Instructor1+ | Live ground/taxi simulator control (needs FSD + sweatbox enabled) |
| `/sweatbox/manual` | Instructor1+ | Instructor user manual (also linked from the control panel) |
| `/airport-editor` | Instructor1+ | Map-first `.apt` / `.air` authoring; **download only** (no server save). Validate tab has live client checks + optional server confirm. Download files, then load them on `/sweatbox`. Design: repo `docs/design/apt-air-editor.md` |
## Operator REST
JSON under `/api/v1` (Bearer API token or session cookie + CSRF). Version pin via `OpenFSD-API-Version`. Full guide and OpenAPI: repository `internal/web/README.md` and `GET /api/v1/openapi.yaml`.
## Optional subsystems
| Feature | Flag / env | Docs |
|---------|------------|------|
| AFV voice | `-afv` + `AFV_*` | [Configuration](Configuration.md#afv-voice-optional), `docs/design/afv-server.md` |
| Multi-node FSD | `CLUSTER_ENABLED` + rqlite | [Deployment](Deployment.md#optional-multi-node-cluster), `docs/design/distributed-openfsd.md` |
## Quick links
- Images: `ghcr.io/renorris/openfsd` (`:latest` = main, `:dev` = unstable tip, `sha-*`)
- Protocol notes: repository `docs/`
- Package / import rules for contributors: repository `AGENTS.md`

View File

@@ -14,7 +14,7 @@ go build -o openfsd-migrate-to-sqlite ./cmd/openfsd-migrate-to-sqlite
## Steps
1. **Stop** openfsd (FSD + web) so nothing writes to either database during the copy.
1. **Stop** openfsd (FSD, web, and AFV if running) so nothing writes to either database during the copy.
2. **Back up** PostgreSQL (`pg_dump`) and keep a copy until you confirm the new server works.
3. **Run the migrator**: