137 lines
4.3 KiB
Markdown
137 lines
4.3 KiB
Markdown
# 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
|
||
```
|