137 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Cyber Hybrid Hub API
Postgres-backed profile API for the Flutter app.
## Setup
1. Create the database (once):
```bash
createdb cyberhybridhub
```
2. Copy and edit environment variables:
```bash
cp .env.example .env
```
3. Install dependencies and run (from this `server/` directory):
```bash
dart pub get
dart run bin/server.dart
```
The API listens on `http://localhost:3000` by default (`PORT` in `.env`).
## Tests
From the repo root (loads `server/.env` automatically):
```bash
./scripts/test-server.sh # unit + DB integration (no live Alpaca)
./scripts/test-server-alpaca.sh # live SPY quote — requires keys in server/.env
```
Or from `server/`:
```bash
# Uses DATABASE_URL from the environment or server/.env
export DATABASE_URL=postgresql://postgres:PASSWORD@localhost:5432/cyberhybridhub
dart pub get
dart test
```
Integration tests apply migrations `001``004` on `cyberhybridhub_test` and truncate
trading tables between cases. Optional override: `TEST_DATABASE_URL`.
## Endpoints
| Method | Path | Auth |
|--------|------|------|
| `GET` | `/v1/me/profile` | `Authorization: Bearer <Firebase ID token>` |
| `PUT` | `/v1/me/profile` | same |
| `POST` | `/v1/me/incoming-question` | same — pushes a question to the client via SignalR |
| `POST` | `/v1/me/questions/bootstrap` | ensure starter question at login |
| `GET` | `/v1/me/questions` | list unanswered questions (queue order) |
| `POST` | `/v1/me/questions/{id}/answer` | submit answer (`{"answer": 0}` default) |
| `POST` | `/v1/me/questions/{id}/defer` | move question to end of queue |
## SignalR — incoming questions
Hub URL: `http://localhost:3000/hubs/questions`
The Flutter app calls `POST /v1/me/questions/bootstrap` once at login to ensure a
starter question exists (random correct answer from -10 to 10) when the user has none.
After sign-in it connects to SignalR and listens for `ReceiveQuestion`. On each new
WebSocket connection the API only delivers an existing unanswered question — it does
not create new rows.
Client payload (correct answer is not sent):
```json
{
"id": "uuid",
"assignedUserId": "firebase-uid",
"text": "...",
"sentAt": "...",
"unansweredCount": 2
}
```
`unansweredCount` is the number of unanswered rows for that user (shown in the app when greater than 1).
`questions` table: `id` (UUID), `assigned_user_id`, `question_text`, `user_response`
(nullable), `correct_answer`, `created_at`, `modified_at`.
## Background question pipeline
A background worker runs inside the API process (enabled by default). On each
interval it walks registered users, fetches data from public web APIs, and
enqueues pipeline questions when the user's queue is not full.
| Env var | Default | Purpose |
|---------|---------|---------|
| `QUESTION_WORKER_ENABLED` | `true` | Set to `false` to disable the worker |
| `QUESTION_WORKER_INTERVAL_SECONDS` | `60` | Seconds between maintenance cycles |
| `QUESTION_PIPELINE_TEST_MODE` | `false` | Use random -10..10 starter-style questions instead of API copy |
**External APIs used**
- [REST Countries](https://restcountries.com/) — population and capital facts
- [Open-Meteo](https://open-meteo.com/) — current temperature by city
**Branching flow**
1. **Track choice** — user swipes toward +10 (weather) or -10 (geography).
2. **Geography** — yes/no population threshold, then capital confirmation;
wrong population guess triggers a recovery question.
3. **Weather** — yes/no warm/cool for a random European city, then a follow-up
to continue weather or switch to geography.
When a user submits an answer (`POST .../answer`), the pipeline evaluates the
response and may immediately create the next branched question and push it over
SignalR.
Pipeline state is stored in `user_pipeline_state`; questions may include
`source_tag`, `pipeline_key`, and `pipeline_step` columns (migration
`003_question_pipeline.sql`).
Test from the shell (replace `ID_TOKEN`):
```bash
curl -s -X POST http://localhost:3000/v1/me/incoming-question \
-H "Authorization: Bearer ID_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text":"What is your preferred contact method?"}'
```
## Flutter client
Run the app with the API URL (defaults to `http://localhost:3000`):
```bash
flutter run --dart-define=API_BASE_URL=http://localhost:3000
```