diff --git a/README.md b/README.md index 2d3a01c..3806cf0 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Personal poker tracker for tournament players. Track ad-hoc home sessions, plan ### Prerequisites -- Go 1.22+ +- Go (latest stable) - Node.js 20+ - PostgreSQL 16+ - SearXNG instance (for AI research web search) diff --git a/docs/2026-03-18-pokertrip-design.md b/docs/2026-03-18-pokertrip-design.md index d4dcb4a..4648709 100644 --- a/docs/2026-03-18-pokertrip-design.md +++ b/docs/2026-03-18-pokertrip-design.md @@ -94,12 +94,14 @@ When tapping "Re-entry", the app calculates and displays: how many BBs you'll ge | venue_id | UUID | FK → venues | | day_of_week | INT | 0=Sunday, 1=Monday, etc. (nullable for "any day" default) | | 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_gtd | INT | nullable | | starting_stack | INT | chips | | blind_structure | JSONB | array of {level, small, big, ante, duration_min} | | notes | TEXT | | +| created_at | TIMESTAMPTZ | | +| updated_at | TIMESTAMPTZ | | ### Trips | 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" | | start_date | DATE | | | end_date | DATE | | -| planned_budget | INT | soft ceiling | +| planned_budget | INT | soft ceiling, minor currency unit (cents/øre) | | currency | TEXT | | | status | TEXT | planning / active / completed | | created_at | TIMESTAMPTZ | | @@ -141,9 +143,9 @@ When tapping "Re-entry", the app calculates and displays: how many BBs you'll ge | time | TIME | | | event_code | TEXT | "#29A" etc. | | name | TEXT | | -| buyin | INT | | +| buyin | INT | minor currency unit (cents/øre) | | currency | TEXT | | -| guarantee | INT | nullable | +| guarantee | INT | nullable, minor currency unit | | format | TEXT | NLH, PLO, NLH PKO, etc. | | notes | TEXT | | | 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 | | blind_structure | JSONB | nullable | | 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 | | +| updated_at | TIMESTAMPTZ | | ### Sessions (the core table — every tournament/cash game played) | 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) | | game_type | TEXT | NLH, PLO, etc. | | session_type | TEXT | tournament / cash | -| buyin | INT | | +| buyin | INT | minor currency unit (cents/øre) | | currency | TEXT | | -| payout | INT | nullable | +| payout | INT | nullable, minor currency unit | | finish_position | INT | nullable | | field_size | INT | nullable | | status | TEXT | registered / playing / busted / cashed / day2 / skipped | | late_reg | BOOLEAN | default false | | entry_level | INT | nullable — what level you joined at | | 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 | | | created_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 | | blinds | TEXT | nullable — e.g. "200/400" | | 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 | 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 | | 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=&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 ``` @@ -280,13 +360,17 @@ The UI is always ahead of the database. No lag, ever. ## 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 - POST /register - POST /login → JWT access token + refresh token (httpOnly cookie) - POST /refresh → rotate tokens +/api/v1/auth (synchronous — returns tokens directly) + POST /register → 201 Created, returns JWT + refresh token + POST /login → 200 OK, JWT access token + refresh token (httpOnly cookie) + POST /refresh → 200 OK, rotate tokens /api/v1/me 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) POST / PATCH /:id + DELETE /:id → soft delete (private) or remove user's reference (public) /api/v1/venue-profiles 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 GET / → filterable: venue, trip, date range, buyin range, game type, status + GET /:id → single session with nested reentries + snapshots + tilt guard data POST / PATCH /:id → status changes, payout, notes, end time - POST /:id/reentry → add re-entry (triggers tilt guard response) - POST /:id/snapshot → upload tournament screen photo + stack size + DELETE /:id + 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 GET / POST / PATCH /:id + DELETE /:id GET /:id/schedule → trip's tournament schedule /api/v1/schedule GET / → location schedules (query by location_key, date range) POST / → manually add schedule entry + DELETE /:id /api/v1/research - POST / → start AI research job + POST / → start AI research job (multipart/form-data for images) GET /:id → job status + results - POST /:id/refine → iterate with additional context/images - POST /:id/approve → accept results → writes to venues + location_schedules + POST /:id/refine → iterate with additional context/images (multipart/form-data) + POST /:id/approve → 200 OK (synchronous) — writes to venues + location_schedules /api/v1/global-events GET / → browse scraped series (filterable by date, location) /api/v1/events - GET /stream → SSE (research progress, sync confirmations) + GET /stream → SSE (research progress, sync confirmations, snapshot OCR results) /api/v1/settings/models 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 ### 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) - Last 3-5 sessions as quick history - 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 - "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) - All queries scoped by user_id - 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 @@ -503,7 +613,7 @@ The analysis and planning tool. - **Framer Motion** (animations) - **Recharts** (stats/charts) - **Workbox** (PWA/service worker) -- **Zustand** or **Jotai** (lightweight state) +- **Zustand** (lightweight state management) ### Infrastructure - **Postgres** (10.5.0.109, database: pokertrip) diff --git a/docs/gsd-kickoff.md b/docs/gsd-kickoff.md index 1ba787d..e56ff45 100644 --- a/docs/gsd-kickoff.md +++ b/docs/gsd-kickoff.md @@ -15,7 +15,7 @@ Read these before planning: ## Tech Stack - **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`) - **AI**: OpenAI-compatible endpoint (OpenRouter/Requesty), self-hosted SearXNG for web search - **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 -- 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 - All data is user-scoped (ready for multi-tenant) - Storage interface abstraction (local filesystem now, S3-compatible later)