diff --git a/astro-rust-plan.md b/astro-rust-plan.md new file mode 100644 index 0000000..057bac5 --- /dev/null +++ b/astro-rust-plan.md @@ -0,0 +1,565 @@ +# Astro + Rust Web Project Plan + +## 1. Architecture + +Use Astro as a statically built frontend with one interactive React island, backed by a Rust/Axum API. + +```text +Browser + | + +-- Astro-generated HTML, CSS, JS, and PWA assets + | + +-- /api/v1/* + | + v + Rust/Axum + | + +-- WMATA client + +-- cache/request coalescing + +-- event normalization + +-- static Astro files +``` + +Production uses a single container: + +1. Node builds the Astro site. +2. Cargo builds the Rust binary. +3. Axum serves `web/dist`. +4. Axum handles `/api/v1/*`. +5. Only the Rust process receives `WMATA_API_KEY`. + +This avoids CORS, requires no Node runtime in production, and prevents the WMATA key from entering the browser bundle. + +## 2. Repository Layout + +```text +trackling/ +|-- web/ +| |-- astro.config.mjs +| |-- package.json +| |-- public/ +| | |-- icons/ +| | |-- manifest.webmanifest +| | `-- pet/ +| `-- src/ +| |-- components/ +| | |-- MetroGame.tsx +| | |-- map/ +| | |-- pet/ +| | |-- panels/ +| | `-- minigames/ +| |-- data/ +| | `-- network.json +| |-- layouts/ +| |-- lib/ +| | |-- api.ts +| | |-- events.ts +| | |-- pet-state.ts +| | `-- storage.ts +| |-- pages/ +| | |-- index.astro +| | |-- about.astro +| | `-- privacy.astro +| `-- styles/ +|-- server/ +| |-- Cargo.toml +| |-- data/ +| | `-- circuit-map.json +| |-- fixtures/ +| `-- src/ +| |-- main.rs +| |-- config.rs +| |-- error.rs +| |-- cache.rs +| |-- routes/ +| |-- wmata/ +| `-- world/ +|-- tests/ +|-- Dockerfile +|-- compose.yaml +|-- Makefile +|-- .env.example +|-- README.md +`-- slop.md +``` + +Keep a single Rust package initially. Split it into workspace crates only if the WMATA adapter or domain model becomes independently reusable. + +## 3. Astro Frontend + +### Astro Shell + +Astro owns: + +- Initial HTML and metadata +- About, privacy, and accessibility pages +- PWA metadata +- App loading fallback +- Attribution and WMATA disclaimer +- Non-JavaScript explanation + +Use static output. No Astro server adapter is needed because Axum serves the generated files. + +### Interactive Island + +Install `@astrojs/react` and mount the main application from `index.astro`: + +```astro + +``` + +`client:load` is appropriate because the map is the primary interface and must become interactive immediately. Avoid `client:only` unless browser-only APIs make server rendering impractical; normal hydration preserves initial HTML. + +### Frontend Responsibilities + +- Render the original SVG rail schematic +- Poll the Rust API +- Animate train and pet positions +- Manage local pet care and progression +- Persist pet state in IndexedDB +- Deduplicate consumed world events +- Run minigames +- Provide offline and stale-data states + +Use React context plus reducers initially. Do not add a global state library until the state model demonstrates a concrete need. + +## 4. Rust Backend + +Recommended dependencies: + +- `axum`: HTTP routing +- `tokio`: async runtime +- `reqwest` with Rustls: WMATA requests +- `serde` and `serde_json`: API models +- `tower-http`: tracing, compression, static files, and request limits +- `tracing` and `tracing-subscriber`: structured logs +- `thiserror`: internal errors +- `time`: timestamps and cache expiration +- `url`: safe request construction + +Optional later: + +- `moka` if the initial cache becomes cumbersome +- `utoipa` for OpenAPI generation +- `governor` for application rate limiting + +### Rust Modules + +```text +config + Environment validation and startup configuration + +wmata + Authentication, HTTP requests, response DTOs, timeouts + +cache + TTL entries and request coalescing + +world + Convert raw transit data into application events + +routes + Axum handlers and public response types + +error + Internal errors mapped to safe HTTP responses +``` + +Do not leak raw upstream errors or URLs containing credentials to clients or logs. + +## 5. API Contract + +### `GET /api/v1/health` + +Returns application health without exposing configuration: + +```json +{ + "status": "ok", + "wmataReachable": true, + "lastSuccessfulUpdate": "2026-08-09T16:30:00Z" +} +``` + +### `GET /api/v1/topology` + +Returns application-specific station and route geometry: + +```json +{ + "version": "2026-08-01", + "stations": [], + "segments": [], + "lines": [] +} +``` + +Alternatively, bake this into Astro as `network.json` and eliminate this endpoint for the MVP. + +### `GET /api/v1/world?station=A01` + +Returns the current map state and predictions for one selected station: + +```json +{ + "generatedAt": "2026-08-09T16:30:00Z", + "stale": false, + "trains": [], + "predictions": [], + "incidents": [], + "elevatorOutages": [], + "lineActivity": { + "RD": 18, + "BL": 10 + }, + "events": [] +} +``` + +Train responses should contain application map coordinates or segment progress, not raw circuit data unless the browser genuinely needs it. + +### Event Shape + +```json +{ + "id": "arrival:A01:RD:20260809T1630", + "kind": "train_arrival", + "stationCode": "A01", + "lineCode": "RD", + "occurredAt": "2026-08-09T16:30:00Z", + "expiresAt": "2026-08-09T16:32:00Z" +} +``` + +Use deterministic event IDs so polling the same data cannot repeatedly award items. + +## 6. WMATA Integration + +Implement adapters for: + +- Train Positions +- Standard Routes +- Track Circuits +- Rail Predictions +- Rail Incidents +- Elevator/Escalator Outages +- Station and line information + +### Polling and Caching + +| Resource | Backend TTL | +|---|---:| +| Train positions | 8-10 seconds | +| Predictions | 15-20 seconds | +| Incidents | 30 seconds | +| Elevator outages | 30 seconds | +| Routes and track circuits | 24 hours | +| Station metadata | 24 hours | + +The browser can poll `/api/v1/world` every 10 seconds while visible. Pause or slow polling when the tab is hidden. + +Coalesce concurrent upstream requests. If 100 users request the world snapshot simultaneously, the backend should make one WMATA request and share the result. + +### Failure Rules + +- Serve the last successful snapshot with `stale: true`. +- Include the snapshot timestamp. +- Apply a maximum stale lifetime. +- Return an empty transit layer if no valid snapshot exists. +- Never let upstream failures affect pet health. +- Never retry aggressively after `429` or `5xx` responses. + +## 7. Map Positioning + +WMATA train positions identify track circuits rather than browser coordinates. Add a preprocessing step: + +1. Fetch Standard Routes and Track Circuits. +2. Associate circuit IDs with ordered line segments. +3. Map each segment onto the original SVG schematic. +4. Produce `circuit-map.json`. +5. At runtime, convert each train's circuit into `{ segmentId, progress }`. +6. Let the frontend interpolate between SVG coordinates. + +Create a Rust maintenance binary such as: + +```text +cargo run --bin sync-topology +``` + +This command should update topology deliberately rather than on every production startup. Review WMATA's storage and redistribution terms before committing generated transit data. + +## 8. Pet Simulation + +Keep pet simulation in TypeScript for the MVP because state is local to the browser and tightly coupled to UI interactions. + +```ts +interface PetState { + version: number; + species: string; + growthStage: number; + homeStation: string; + hunger: number; + energy: number; + happiness: number; + curiosity: number; + personality: PetPersonality; + inventory: InventoryItem[]; + discoveredStations: string[]; + lineBadges: string[]; + consumedEventIds: string[]; + lastCareAt: string; + playTimeSeconds: number; +} +``` + +Rules: + +- Pet health changes only through care and recoverable neglect. +- Transit events grant temporary animations, exploration, or collectibles. +- Cap offline care decay. +- Provide vacation mode. +- Store a schema version and run migrations. +- Offer save export, import, and reset. + +Rust/Wasm can be reconsidered later if deterministic cross-platform simulation becomes valuable. It is unnecessary for the initial architecture. + +## 9. Offline and PWA Behavior + +Use a service worker to cache: + +- Astro application shell +- SVG map and pet sprites +- Fonts and icons +- Minigame assets +- Last successful topology version + +Use network-first behavior for `/api/v1/*`. Do not indefinitely cache live transit responses as though they were current. + +When offline: + +- Display the last snapshot as stale or hide live trains. +- Continue ordinary pet care. +- Allow minigames. +- Suspend transit-driven rewards. +- Queue only local save operations, never upstream WMATA calls. + +## 10. Security + +- Store `WMATA_API_KEY` only in the Rust process environment. +- Never prefix it with `PUBLIC_`. +- Do not place it in Astro's environment schema. +- Use strict upstream timeouts and response-size limits. +- Validate station and line codes. +- Escape incident descriptions before display. +- Add same-origin security headers and a Content Security Policy. +- Rate-limit public API routes. +- Do not turn the backend into a general WMATA proxy. +- Redact credentials and query strings from logs. +- Commit `.env.example`, never `.env`. + +Example server configuration: + +```text +WMATA_API_KEY= +BIND_ADDR=0.0.0.0:8080 +STATIC_DIR=/app/web +RUST_LOG=info +WMATA_TIMEOUT_SECONDS=5 +``` + +## 11. Accessibility + +- Provide keyboard zooming, panning, and station selection. +- Include a list-based alternative to the SVG map. +- Label every station and train state for assistive technology. +- Do not rely only on WMATA line colors. +- Respect `prefers-reduced-motion`. +- Pause decorative animation without pausing transit updates. +- Keep arrivals and incidents readable when Companion Mode is disabled. +- Ensure the hidden pet can also be enabled through settings. + +## 12. Development Workflow + +### Local Development + +Run two processes: + +```text +Astro: http://localhost:4321 +Axum: http://localhost:8080 +``` + +Configure Vite to proxy `/api` to Axum. Production does not need CORS because Axum serves both surfaces from one origin. + +Provide a fixture mode: + +```text +WMATA_FIXTURE_DIR=server/fixtures +``` + +This lets frontend and end-to-end tests run without a WMATA key or live service. + +Plan for root commands equivalent to: + +```text +make dev +make test +make lint +make build +make container +``` + +Each command should delegate to Cargo and the frontend package manager rather than hiding complex behavior in custom scripts. + +## 13. Testing + +### Rust + +- Deserialize recorded WMATA responses. +- Reject malformed or oversized responses. +- Verify TTL behavior. +- Verify concurrent requests are coalesced. +- Test stale-data fallback. +- Test deterministic event IDs. +- Test circuit-to-segment conversion. +- Test API routes with fixture data. + +Run: + +```text +cargo fmt --check +cargo clippy --all-targets -- -D warnings +cargo test +``` + +### Frontend + +- Pet reducer and need boundaries +- IndexedDB migrations +- Event deduplication +- Offline behavior +- Map keyboard navigation +- Reduced-motion rendering +- Error and stale-data displays + +Use Vitest and React Testing Library. + +### End-to-End + +Use Playwright against fixture mode: + +1. Load the map. +2. Select a home station. +3. Discover Companion Mode. +4. Hatch and care for the pet. +5. Receive one train event. +6. Confirm polling does not duplicate its reward. +7. Reload and verify persistence. +8. Simulate API failure and verify health does not change. +9. Test desktop and mobile layouts. + +## 14. Container Build + +Use a multi-stage Docker build: + +1. Node stage installs locked frontend dependencies. +2. Node stage runs `astro build`. +3. Rust stage builds a release Axum binary. +4. Minimal runtime stage receives the binary and `web/dist`. +5. The container starts only the Rust process. + +Add: + +- Non-root runtime user +- Read-only application files +- Health check against `/api/v1/health` +- No WMATA key in image layers +- Graceful shutdown +- Production request logging + +## 15. Delivery Milestones + +### Milestone 1: Project Foundation + +- Scaffold Astro with React and TypeScript. +- Scaffold Axum. +- Add the production static-file fallback. +- Add local proxying and fixture mode. +- Create CI-quality build, lint, and test commands. + +Exit criterion: one container serves an Astro page and Rust health endpoint. + +### Milestone 2: WMATA Data Spike + +- Implement configuration and WMATA authentication. +- Add train, route, circuit, prediction, and incident adapters. +- Record sanitized fixtures. +- Add caching and stale-data behavior. + +Exit criterion: `/api/v1/world` returns a stable application model. + +### Milestone 3: Functional Map + +- Draw the original SVG schematic. +- Generate circuit mappings. +- Display live train positions. +- Add station predictions and incident panels. +- Complete responsive and keyboard behavior. + +Exit criterion: the map is useful without the pet. + +### Milestone 4: Pet Vertical Slice + +- Add hidden discovery and settings toggle. +- Add one pet with four needs. +- Add care interactions and IndexedDB persistence. +- Add vacation and offline modes. + +Exit criterion: a pet can be raised using fixture data. + +### Milestone 5: Live Pet Events + +- Add deterministic world events. +- Connect arrivals and line activity to animations. +- Add exploration journeys and fallback timing. +- Add event cooldowns and consumed-event persistence. + +Exit criterion: transit data enriches play without controlling health. + +### Milestone 6: Progression and PWA + +- Add three evolutions. +- Add two minigames. +- Add collectibles and line badges. +- Add service worker, installation, and offline shell. +- Complete accessibility and legal copy. + +Exit criterion: feature-complete release candidate. + +### Milestone 7: Production Readiness + +- Load-test caching and request coalescing. +- Run browser and mobile tests. +- Verify no secrets enter frontend assets. +- Review WMATA terms and attribution. +- Deploy the single container with monitoring. + +## 16. Definition of Done + +- Astro and Axum build reproducibly from lockfiles. +- One container serves the entire application. +- The WMATA key never reaches the browser. +- Live requests are cached and coalesced. +- The map works independently of Companion Mode. +- The pet works when WMATA is offline. +- Transit disruptions never reduce pet health. +- Repeated polling cannot duplicate rewards. +- Pet state survives reloads and PWA updates. +- Keyboard, touch, screen-reader, and reduced-motion paths are tested. +- The application includes timestamps, stale indicators, attribution, and non-endorsement language. + +## References + +- [Astro framework component hydration](https://docs.astro.build/en/guides/framework-components/#hydrating-interactive-components) +- [Astro client directives](https://docs.astro.build/en/reference/directives-reference/#client-directives) +- [Astro environment variables](https://docs.astro.build/en/guides/environment-variables/)