Fix spec review findings: data model gaps, API completeness, missing sections
- Add contributed_by_user_id + research_job_id to location_schedules (give-to-get audit trail) - Add reentry type field (reentry/rebuy/addon distinction) - Consistent minor currency unit annotations on all monetary columns - Add exchange_rate_to_home to sessions for cross-currency stats - Add timestamps to venue_profiles and location_schedules - Add missing API endpoints: GET session detail, DELETE endpoints, GET snapshots/reentries - Document 202 Accepted exceptions (auth, research approval are synchronous) - Add multipart/form-data upload flow documentation - Add tilt guard response contract - Add session status state machine diagram - Add currency conversion strategy - Add offline sync strategy (localStorage queue, last-write-wins) - Add API error response format and cursor pagination format - Add rate limiting on auth endpoints - Resolve Zustand vs Jotai → Zustand - Align Go version across docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fef6f5318e
commit
38c7877765
3 changed files with 134 additions and 24 deletions
|
|
@ -26,7 +26,7 @@ Personal poker tracker for tournament players. Track ad-hoc home sessions, plan
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Go 1.22+
|
- Go (latest stable)
|
||||||
- Node.js 20+
|
- Node.js 20+
|
||||||
- PostgreSQL 16+
|
- PostgreSQL 16+
|
||||||
- SearXNG instance (for AI research web search)
|
- SearXNG instance (for AI research web search)
|
||||||
|
|
|
||||||
|
|
@ -94,12 +94,14 @@ When tapping "Re-entry", the app calculates and displays: how many BBs you'll ge
|
||||||
| venue_id | UUID | FK → venues |
|
| venue_id | UUID | FK → venues |
|
||||||
| day_of_week | INT | 0=Sunday, 1=Monday, etc. (nullable for "any day" default) |
|
| day_of_week | INT | 0=Sunday, 1=Monday, etc. (nullable for "any day" default) |
|
||||||
| default_game_type | TEXT | NLH, PLO, etc. |
|
| default_game_type | TEXT | NLH, PLO, etc. |
|
||||||
| default_buyin | INT | in minor currency unit |
|
| default_buyin | INT | minor currency unit (cents/øre) |
|
||||||
| default_currency | TEXT | DKK, PHP, USD, EUR, etc. |
|
| default_currency | TEXT | DKK, PHP, USD, EUR, etc. |
|
||||||
| default_gtd | INT | nullable |
|
| default_gtd | INT | nullable |
|
||||||
| starting_stack | INT | chips |
|
| starting_stack | INT | chips |
|
||||||
| blind_structure | JSONB | array of {level, small, big, ante, duration_min} |
|
| blind_structure | JSONB | array of {level, small, big, ante, duration_min} |
|
||||||
| notes | TEXT | |
|
| notes | TEXT | |
|
||||||
|
| created_at | TIMESTAMPTZ | |
|
||||||
|
| updated_at | TIMESTAMPTZ | |
|
||||||
|
|
||||||
### Trips
|
### Trips
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
|
|
@ -111,7 +113,7 @@ When tapping "Re-entry", the app calculates and displays: how many BBs you'll ge
|
||||||
| country | TEXT | "Philippines" |
|
| country | TEXT | "Philippines" |
|
||||||
| start_date | DATE | |
|
| start_date | DATE | |
|
||||||
| end_date | DATE | |
|
| end_date | DATE | |
|
||||||
| planned_budget | INT | soft ceiling |
|
| planned_budget | INT | soft ceiling, minor currency unit (cents/øre) |
|
||||||
| currency | TEXT | |
|
| currency | TEXT | |
|
||||||
| status | TEXT | planning / active / completed |
|
| status | TEXT | planning / active / completed |
|
||||||
| created_at | TIMESTAMPTZ | |
|
| created_at | TIMESTAMPTZ | |
|
||||||
|
|
@ -141,9 +143,9 @@ When tapping "Re-entry", the app calculates and displays: how many BBs you'll ge
|
||||||
| time | TIME | |
|
| time | TIME | |
|
||||||
| event_code | TEXT | "#29A" etc. |
|
| event_code | TEXT | "#29A" etc. |
|
||||||
| name | TEXT | |
|
| name | TEXT | |
|
||||||
| buyin | INT | |
|
| buyin | INT | minor currency unit (cents/øre) |
|
||||||
| currency | TEXT | |
|
| currency | TEXT | |
|
||||||
| guarantee | INT | nullable |
|
| guarantee | INT | nullable, minor currency unit |
|
||||||
| format | TEXT | NLH, PLO, NLH PKO, etc. |
|
| format | TEXT | NLH, PLO, NLH PKO, etc. |
|
||||||
| notes | TEXT | |
|
| notes | TEXT | |
|
||||||
| multi_day | BOOLEAN | |
|
| multi_day | BOOLEAN | |
|
||||||
|
|
@ -151,7 +153,10 @@ When tapping "Re-entry", the app calculates and displays: how many BBs you'll ge
|
||||||
| starting_stack | INT | nullable |
|
| starting_stack | INT | nullable |
|
||||||
| blind_structure | JSONB | nullable |
|
| blind_structure | JSONB | nullable |
|
||||||
| source | TEXT | "ai_research" / "manual" / "scraped" |
|
| source | TEXT | "ai_research" / "manual" / "scraped" |
|
||||||
|
| contributed_by_user_id | UUID | FK → users, never exposed via API (for audit/rollback) |
|
||||||
|
| research_job_id | UUID | FK → research_jobs, nullable (traces provenance) |
|
||||||
| created_at | TIMESTAMPTZ | |
|
| created_at | TIMESTAMPTZ | |
|
||||||
|
| updated_at | TIMESTAMPTZ | |
|
||||||
|
|
||||||
### Sessions (the core table — every tournament/cash game played)
|
### Sessions (the core table — every tournament/cash game played)
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
|
|
@ -166,15 +171,16 @@ When tapping "Re-entry", the app calculates and displays: how many BBs you'll ge
|
||||||
| end_time | TIMESTAMPTZ | nullable (set when session ends) |
|
| end_time | TIMESTAMPTZ | nullable (set when session ends) |
|
||||||
| game_type | TEXT | NLH, PLO, etc. |
|
| game_type | TEXT | NLH, PLO, etc. |
|
||||||
| session_type | TEXT | tournament / cash |
|
| session_type | TEXT | tournament / cash |
|
||||||
| buyin | INT | |
|
| buyin | INT | minor currency unit (cents/øre) |
|
||||||
| currency | TEXT | |
|
| currency | TEXT | |
|
||||||
| payout | INT | nullable |
|
| payout | INT | nullable, minor currency unit |
|
||||||
| finish_position | INT | nullable |
|
| finish_position | INT | nullable |
|
||||||
| field_size | INT | nullable |
|
| field_size | INT | nullable |
|
||||||
| status | TEXT | registered / playing / busted / cashed / day2 / skipped |
|
| status | TEXT | registered / playing / busted / cashed / day2 / skipped |
|
||||||
| late_reg | BOOLEAN | default false |
|
| late_reg | BOOLEAN | default false |
|
||||||
| entry_level | INT | nullable — what level you joined at |
|
| entry_level | INT | nullable — what level you joined at |
|
||||||
| entry_blinds | TEXT | nullable — e.g. "100/200" |
|
| entry_blinds | TEXT | nullable — e.g. "100/200" |
|
||||||
|
| exchange_rate_to_home | NUMERIC | nullable, snapshot of exchange rate at session creation for cross-currency stats |
|
||||||
| notes | TEXT | |
|
| notes | TEXT | |
|
||||||
| created_at | TIMESTAMPTZ | |
|
| created_at | TIMESTAMPTZ | |
|
||||||
| updated_at | TIMESTAMPTZ | |
|
| updated_at | TIMESTAMPTZ | |
|
||||||
|
|
@ -188,7 +194,8 @@ When tapping "Re-entry", the app calculates and displays: how many BBs you'll ge
|
||||||
| level | INT | nullable |
|
| level | INT | nullable |
|
||||||
| blinds | TEXT | nullable — e.g. "200/400" |
|
| blinds | TEXT | nullable — e.g. "200/400" |
|
||||||
| stack_received | INT | nullable — chips received |
|
| stack_received | INT | nullable — chips received |
|
||||||
| cost | INT | re-entry cost (may differ from original buyin) |
|
| cost | INT | minor currency unit, inherits session's currency |
|
||||||
|
| type | TEXT | reentry / rebuy / addon |
|
||||||
|
|
||||||
### Session Snapshots
|
### Session Snapshots
|
||||||
| Column | Type | Notes |
|
| Column | Type | Notes |
|
||||||
|
|
@ -236,6 +243,79 @@ When tapping "Re-entry", the app calculates and displays: how many BBs you'll ge
|
||||||
| purpose | TEXT | research / snapshot |
|
| purpose | TEXT | research / snapshot |
|
||||||
| created_at | TIMESTAMPTZ | |
|
| created_at | TIMESTAMPTZ | |
|
||||||
|
|
||||||
|
### Monetary Values
|
||||||
|
|
||||||
|
All monetary amounts are stored as integers in **minor currency units** (cents, øre, centavos). This avoids floating-point precision issues. The `currency` column on the same row (or parent row for re-entries) determines the unit. Examples: 500 DKK = 50000, ₱8000 = 800000, $100 = 10000.
|
||||||
|
|
||||||
|
### Currency Conversion Strategy
|
||||||
|
|
||||||
|
Sessions are stored in their original currency. For cross-currency aggregation (stats, overall P&L), each session records the currency it was played in. Stats views aggregate within the same currency by default. A "home currency" user setting (e.g. DKK) enables a converted view where an exchange rate snapshot is stored at session creation time (`exchange_rate_to_home NUMERIC` on sessions, nullable). This is a best-effort convenience — not financial-grade precision.
|
||||||
|
|
||||||
|
### Session Status State Machine
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┐
|
||||||
|
│ upcoming │ (default for trip schedule entries)
|
||||||
|
└────┬─────┘
|
||||||
|
┌──────┼──────┐
|
||||||
|
▼ │ ▼
|
||||||
|
┌──────────┐ │ ┌─────────┐
|
||||||
|
│registered│ │ │ skipped │
|
||||||
|
└────┬─────┘ │ └─────────┘
|
||||||
|
▼ │
|
||||||
|
┌──────────┐ │
|
||||||
|
│ playing │◄┘ (Home sessions start here)
|
||||||
|
└──┬──┬──┬─┘
|
||||||
|
│ │ │
|
||||||
|
┌────┘ │ └────┐
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────┐┌──────┐┌──────┐
|
||||||
|
│ busted ││cashed││ day2 │
|
||||||
|
└────────┘└──────┘└──┬───┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────┐
|
||||||
|
│ playing │ (day 2 resumes as playing)
|
||||||
|
└──┬──┬───┘
|
||||||
|
│ │
|
||||||
|
┌────┘ └────┐
|
||||||
|
▼ ▼
|
||||||
|
┌────────┐ ┌──────┐
|
||||||
|
│ busted │ │cashed│
|
||||||
|
└────────┘ └──────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Any terminal state (busted, cashed, skipped) can be reset back to upcoming/playing via explicit user action (undo).
|
||||||
|
|
||||||
|
### Offline Sync Strategy
|
||||||
|
|
||||||
|
Single-user tool: last write wins. Mutations made while offline are queued in localStorage (ordered array of API calls). On reconnect (`navigator.onLine` event), the queue is replayed in order. If a replayed mutation fails (409 conflict), the client fetches fresh state from the server and reconciles. For the personal tool this is sufficient. SaaS multi-device sync would need vector clocks or CRDTs — documented in saas-considerations.md.
|
||||||
|
|
||||||
|
### API Error Response Format
|
||||||
|
|
||||||
|
All error responses use a consistent envelope:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_ERROR",
|
||||||
|
"message": "buyin must be a positive integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
HTTP status codes: 400 (validation), 401 (auth), 403 (forbidden), 404 (not found), 409 (conflict), 429 (rate limited), 500 (server error).
|
||||||
|
|
||||||
|
### API Pagination Format
|
||||||
|
|
||||||
|
All list endpoints use cursor-based pagination:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v1/sessions?cursor=<opaque_string>&limit=50
|
||||||
|
```
|
||||||
|
|
||||||
|
Response includes `next_cursor` (null if no more results). Cursor is an opaque base64-encoded value (typically `created_at + id` for stable ordering).
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -280,13 +360,17 @@ The UI is always ahead of the database. No lag, ever.
|
||||||
|
|
||||||
## API Design
|
## API Design
|
||||||
|
|
||||||
All endpoints versioned under `/api/v1/`. All write endpoints return `202 Accepted`. All list endpoints support cursor pagination + query param filters.
|
All endpoints versioned under `/api/v1/`. All list endpoints support cursor pagination + query param filters.
|
||||||
|
|
||||||
|
**Write semantics:** Most write endpoints return `202 Accepted` (fire-and-forget, processed via River). Exceptions that return synchronously: auth endpoints (must return tokens), and `POST /research/:id/approve` (returns `200` after writing, so the UI can immediately show approved data).
|
||||||
|
|
||||||
|
**Uploads:** Endpoints that accept images (`/sessions/:id/snapshot`, `/research`, `/research/:id/refine`) accept `multipart/form-data`. Images are stored via the storage interface and an `uploads` record is created. The response includes the upload URL.
|
||||||
|
|
||||||
```
|
```
|
||||||
/api/v1/auth
|
/api/v1/auth (synchronous — returns tokens directly)
|
||||||
POST /register
|
POST /register → 201 Created, returns JWT + refresh token
|
||||||
POST /login → JWT access token + refresh token (httpOnly cookie)
|
POST /login → 200 OK, JWT access token + refresh token (httpOnly cookie)
|
||||||
POST /refresh → rotate tokens
|
POST /refresh → 200 OK, rotate tokens
|
||||||
|
|
||||||
/api/v1/me
|
/api/v1/me
|
||||||
GET / → user profile + settings
|
GET / → user profile + settings
|
||||||
|
|
@ -296,44 +380,69 @@ All endpoints versioned under `/api/v1/`. All write endpoints return `202 Accept
|
||||||
GET / → list (public + user's private)
|
GET / → list (public + user's private)
|
||||||
POST /
|
POST /
|
||||||
PATCH /:id
|
PATCH /:id
|
||||||
|
DELETE /:id → soft delete (private) or remove user's reference (public)
|
||||||
|
|
||||||
/api/v1/venue-profiles
|
/api/v1/venue-profiles
|
||||||
GET / → user's venue defaults
|
GET / → user's venue defaults
|
||||||
PUT /:venue_id/:dow → set/update default
|
PUT /:venue_id → set/update default (body includes day_of_week, nullable for "any day")
|
||||||
|
DELETE /:venue_id/:dow → remove a specific day's profile ("all" for the any-day default)
|
||||||
|
|
||||||
/api/v1/sessions
|
/api/v1/sessions
|
||||||
GET / → filterable: venue, trip, date range, buyin range, game type, status
|
GET / → filterable: venue, trip, date range, buyin range, game type, status
|
||||||
|
GET /:id → single session with nested reentries + snapshots + tilt guard data
|
||||||
POST /
|
POST /
|
||||||
PATCH /:id → status changes, payout, notes, end time
|
PATCH /:id → status changes, payout, notes, end time
|
||||||
POST /:id/reentry → add re-entry (triggers tilt guard response)
|
DELETE /:id
|
||||||
POST /:id/snapshot → upload tournament screen photo + stack size
|
POST /:id/reentry → add re-entry/rebuy/addon (returns tilt guard calculation)
|
||||||
|
GET /:id/reentries → list re-entries for a session
|
||||||
|
POST /:id/snapshot → upload tournament screen photo + stack size (multipart/form-data)
|
||||||
|
GET /:id/snapshots → list snapshots for a session (for trajectory chart)
|
||||||
|
|
||||||
/api/v1/trips
|
/api/v1/trips
|
||||||
GET /
|
GET /
|
||||||
POST /
|
POST /
|
||||||
PATCH /:id
|
PATCH /:id
|
||||||
|
DELETE /:id
|
||||||
GET /:id/schedule → trip's tournament schedule
|
GET /:id/schedule → trip's tournament schedule
|
||||||
|
|
||||||
/api/v1/schedule
|
/api/v1/schedule
|
||||||
GET / → location schedules (query by location_key, date range)
|
GET / → location schedules (query by location_key, date range)
|
||||||
POST / → manually add schedule entry
|
POST / → manually add schedule entry
|
||||||
|
DELETE /:id
|
||||||
|
|
||||||
/api/v1/research
|
/api/v1/research
|
||||||
POST / → start AI research job
|
POST / → start AI research job (multipart/form-data for images)
|
||||||
GET /:id → job status + results
|
GET /:id → job status + results
|
||||||
POST /:id/refine → iterate with additional context/images
|
POST /:id/refine → iterate with additional context/images (multipart/form-data)
|
||||||
POST /:id/approve → accept results → writes to venues + location_schedules
|
POST /:id/approve → 200 OK (synchronous) — writes to venues + location_schedules
|
||||||
|
|
||||||
/api/v1/global-events
|
/api/v1/global-events
|
||||||
GET / → browse scraped series (filterable by date, location)
|
GET / → browse scraped series (filterable by date, location)
|
||||||
|
|
||||||
/api/v1/events
|
/api/v1/events
|
||||||
GET /stream → SSE (research progress, sync confirmations)
|
GET /stream → SSE (research progress, sync confirmations, snapshot OCR results)
|
||||||
|
|
||||||
/api/v1/settings/models
|
/api/v1/settings/models
|
||||||
GET / → quick picks + full model list from provider API
|
GET / → quick picks + full model list from provider API
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Tilt guard response** (returned by `POST /sessions/:id/reentry`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"reentry_id": "uuid",
|
||||||
|
"tilt_guard": {
|
||||||
|
"cost": 50000,
|
||||||
|
"stack_received": 10000,
|
||||||
|
"current_blinds": "200/400",
|
||||||
|
"bb_count": 25,
|
||||||
|
"total_invested": 150000,
|
||||||
|
"entry_count": 3,
|
||||||
|
"break_even_cash": 150000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## AI Research System
|
## AI Research System
|
||||||
|
|
||||||
### Research Flow
|
### Research Flow
|
||||||
|
|
@ -405,7 +514,7 @@ The in-the-moment tool. Speed and touch-friendliness above all.
|
||||||
- Today's venue suggestion (from venue profiles for this day of week)
|
- Today's venue suggestion (from venue profiles for this day of week)
|
||||||
- Last 3-5 sessions as quick history
|
- Last 3-5 sessions as quick history
|
||||||
- Tap "Log" → pick venue (favorites first, search others) → pre-filled from venue profile → confirm → live session
|
- Tap "Log" → pick venue (favorites first, search others) → pre-filled from venue profile → confirm → live session
|
||||||
- Active session: timer, big thumb-friendly action buttons (Rebuy, Add-on, Bust, Cash, Made Day 2, Snapshot)
|
- Active session: timer, big thumb-friendly action buttons (Re-entry, Rebuy, Add-on, Bust, Cash, Made Day 2, Snapshot) — re-entry/rebuy/addon all tracked as separate types in session_reentries
|
||||||
- Re-entry tilt guard modal before confirming re-entry
|
- Re-entry tilt guard modal before confirming re-entry
|
||||||
- "Cash" → input payout + finish position → end session
|
- "Cash" → input payout + finish position → end session
|
||||||
|
|
||||||
|
|
@ -468,6 +577,7 @@ The analysis and planning tool.
|
||||||
- JWT access token (15 min) + refresh token (30 days, httpOnly cookie)
|
- JWT access token (15 min) + refresh token (30 days, httpOnly cookie)
|
||||||
- All queries scoped by user_id
|
- All queries scoped by user_id
|
||||||
- No email verification (single user)
|
- No email verification (single user)
|
||||||
|
- Basic rate limiting on auth endpoints: 5 attempts/min per IP (in-memory counter, even for personal tool — it's exposed to the internet)
|
||||||
|
|
||||||
### Data Visibility Rules
|
### Data Visibility Rules
|
||||||
|
|
||||||
|
|
@ -503,7 +613,7 @@ The analysis and planning tool.
|
||||||
- **Framer Motion** (animations)
|
- **Framer Motion** (animations)
|
||||||
- **Recharts** (stats/charts)
|
- **Recharts** (stats/charts)
|
||||||
- **Workbox** (PWA/service worker)
|
- **Workbox** (PWA/service worker)
|
||||||
- **Zustand** or **Jotai** (lightweight state)
|
- **Zustand** (lightweight state management)
|
||||||
|
|
||||||
### Infrastructure
|
### Infrastructure
|
||||||
- **Postgres** (10.5.0.109, database: pokertrip)
|
- **Postgres** (10.5.0.109, database: pokertrip)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Read these before planning:
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
- **Backend**: Go, Chi (router), pgx (Postgres driver), sqlc (codegen), goose (migrations), River (Postgres-backed job queue), golang-jwt, bcrypt
|
- **Backend**: Go, Chi (router), pgx (Postgres driver), sqlc (codegen), goose (migrations), River (Postgres-backed job queue), golang-jwt, bcrypt
|
||||||
- **Frontend**: React + TypeScript, Vite, Tailwind CSS (Catppuccin Mocha palette), Framer Motion, Recharts, Workbox (PWA), Zustand or Jotai (state)
|
- **Frontend**: React + TypeScript, Vite, Tailwind CSS (Catppuccin Mocha palette), Framer Motion, Recharts, Workbox (PWA), Zustand (state)
|
||||||
- **Database**: PostgreSQL at `10.5.0.109:5432/pokertrip` (credentials in `.env`)
|
- **Database**: PostgreSQL at `10.5.0.109:5432/pokertrip` (credentials in `.env`)
|
||||||
- **AI**: OpenAI-compatible endpoint (OpenRouter/Requesty), self-hosted SearXNG for web search
|
- **AI**: OpenAI-compatible endpoint (OpenRouter/Requesty), self-hosted SearXNG for web search
|
||||||
- **Deploy**: Single Go binary with embedded React frontend via `embed.FS`
|
- **Deploy**: Single Go binary with embedded React frontend via `embed.FS`
|
||||||
|
|
@ -38,7 +38,7 @@ The design spec covers everything in detail, but the major systems are:
|
||||||
|
|
||||||
## Architecture Principles
|
## Architecture Principles
|
||||||
|
|
||||||
- Every write is fire-and-forget from the client (202 Accepted, River processes async)
|
- Most writes are fire-and-forget from the client (202 Accepted, River processes async) — exceptions: auth (synchronous, returns tokens) and research approval (synchronous, returns 200 after write)
|
||||||
- UI is always optimistic — never wait for DB confirmation
|
- UI is always optimistic — never wait for DB confirmation
|
||||||
- All data is user-scoped (ready for multi-tenant)
|
- All data is user-scoped (ready for multi-tenant)
|
||||||
- Storage interface abstraction (local filesystem now, S3-compatible later)
|
- Storage interface abstraction (local filesystem now, S3-compatible later)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue