Post-hand Texas Hold'em training analyzer for screenshots from online poker tables. Poker Hero extracts table state, lets the user verify uncertain fields, and produces an educational recommendation through a configurable local or external provider.
The project is for study and post-hand review. It is not live-play automation, covert real-time assistance, or a tool for taking actions in a poker client.
Prerequisites: Node 24, pnpm 11+, Python 3.11+, Rust 1.85+, and Docker.
git clone <repo-url> && cd poker-hero
pnpm bootstrapThen start the two apps in separate terminals:
pnpm backend:dev
pnpm pwa:devOpen http://localhost:5173. The API runs at http://localhost:8000.
poker-hero/
├── apps/
│ ├── backend/ FastAPI API, OCR parsers, solvers, storage, tests
│ └── pwa/ React/Vite product app and Cloudflare Worker proxy
├── packages/
│ ├── openapi/ Deterministic backend OpenAPI document and tooling
│ └── openapi-client/ Generated TypeScript wire contracts for the PWA
├── infra/
│ └── docker/ Local Compose and deployment env example
├── solver-plugins/
│ └── postflop/ Rust heads-up postflop solver adapter
├── docs/
│ ├── specs/ Canonical product specification and archive
│ ├── reference/ Architecture reference
│ └── process/ Deployment and operational procedures
├── scripts/ Development automation
└── .github/workflows/ App-scoped CI and deployment pipelines
| Command | Description |
|---|---|
pnpm bootstrap |
Install dependencies and build the local postflop solver |
pnpm backend:dev |
Start FastAPI with reload on port 8000 |
pnpm backend:mcp |
Start the environment-fixed local MCP gateway over stdio |
pnpm backend:benchmark <dataset.zip> |
Benchmark a parser against an exported labeled dataset |
pnpm backend:recommendation-benchmark <dataset.json> |
Benchmark a recommendation provider against trusted references |
pnpm backend:backup <command> |
Initialize, export, verify, or restore-drill application backups |
pnpm backend:test |
Run the backend pytest suite |
pnpm api:generate |
Regenerate the committed OpenAPI document and TypeScript client |
pnpm api:check |
Verify that committed OpenAPI artifacts are tracked and current |
pnpm pwa:dev |
Start Vite on port 5173 |
pnpm pwa:test |
Run PWA tests |
pnpm pwa:build |
Build the production PWA |
pnpm pwa:performance |
Build and enforce production bundle budgets |
pnpm monitor:test |
Test the deployment uptime probe |
pnpm test:e2e |
Run browser workflow tests with isolated test providers |
pnpm docker:up |
Build and start both apps with Docker Compose |
pnpm docker:down |
Stop the Compose stack |
The browser workflow command starts temporary FastAPI, HTTP provider stub, and
Vite servers on ports 8010, 8011, and 4174. Install Chromium once with
pnpm -C apps/pwa exec playwright install chromium before the first local
run. Its backend job store is removed when the test server exits.
Backend settings use the POKER_ prefix. pnpm bootstrap copies
apps/backend/.env.example to apps/backend/.env when needed.
The main provider switches are:
POKER_PARSER_PROVIDER:mock,llm_vision,ocr_cv, orauto. Automatic recognition uses local OCR for explicit calibrated client layouts and external vision forgeneric, other layouts, or local OCR rejection; it requiresPOKER_EXTERNAL_PARSER_URLPOKER_PARSER_LAYOUT_PROFILE: the default layout ID. Fixed-regionocr_cvsupportsgeneric,fortuna,nations, andfortuna_nations; external vision may use deployment-defined IDs such aspokerstarsPOKER_PARSER_ENABLED_PROVIDERSandPOKER_PARSER_ENABLED_LAYOUT_PROFILES: JSON lists of additional installed parsers and layout IDs a user may select for each new upload or live capture; the deployment defaults are always enabled and the UI shows only compatible parser/layout combinationsPOKER_RECOMMENDATION_PROVIDER:rule_based,mock,local_solver,external_solver, orllm_advicePOKER_RECOMMENDATION_ENABLED_PROVIDERS: JSON list of additional installed recommendation plugins exposed for per-screenshot selectionPOKER_LOCAL_SOLVER_ENGINE:postflop_solver(default) orlocal_evPOKER_LOCAL_SOLVER_ENABLED_ENGINES: JSON list of additional local solver engines exposed whenlocal_solveris selectedPOKER_POSTFLOP_SOLVER_RANGE_MODE: derive ranges from a complete supported heads-up preflop history withcontextual(default), or always use the configured OOP/IP ranges withconfiguredPOKER_EXTERNAL_PARSER_BEARER_TOKEN: optional bearer token forllm_visionPOKER_EXTERNAL_PROVIDER_BEARER_TOKEN: optional bearer token forexternal_solverPOKER_LLM_ADVICE_BEARER_TOKEN: optional bearer token forllm_advicePOKER_EXTERNAL_REQUEST_TIMEOUT_SECONDS: timeout shared by external parser and recommendation requests (default 60 seconds)POKER_DEPLOYMENT_ENVIRONMENT:local,staging, orproduction; MCP gateways verify this identity before accessing jobs
For example, a private deployment can route Fortuna/Nations through local OCR and PokerStars through external vision behind one automatic recognition option:
POKER_PARSER_PROVIDER=auto
POKER_PARSER_LAYOUT_PROFILE=fortuna_nations
POKER_PARSER_ENABLED_LAYOUT_PROFILES=["pokerstars"]
POKER_EXTERNAL_PARSER_URL=https://vision.example.com/parseThe automatic parser routes pokerstars directly to external vision. The local
ocr_cv option remains unavailable for that layout until a labeled corpus and
calibrated coordinates/templates are added.
POKER_DATA_DIR: file-backed jobs and uploaded screenshotsPOKER_DATA_VOLUME_ID: stable deployment identity required only by the operational backup CLIPOKER_MAX_DATASET_UPLOAD_BYTES: maximum parser dataset ZIP size for benchmark selection, export, and import (default 100 MiB)POKER_MAX_BACKUP_UPLOAD_BYTES: maximum full application backup ZIP size for export and restore (default 100 MiB)POKER_API_RATE_LIMIT_ENABLED: enable bounded per-client limits for uploads, recommendations, benchmark runs, and archive transfers (defaulttrue)POKER_API_RATE_LIMIT_*_PER_MINUTE: tune each expensive-operation budget; defaults are120for uploads/recommendations and6for benchmarks/transfersPOKER_CORS_ORIGINS: JSON list of direct browser originsPOKER_PROXY_SHARED_SECRET: optional Worker-to-backend credential, at least 32 characters; leave empty for local developmentPOKER_MCP_ENABLEDandPOKER_MCP_PUBLIC_URL: hosted MCP kill switch and exact public HTTPS/mcpendpoint; hosted access defaults offPOKER_MCP_ALLOW_WRITES: staging-only server write gate, independent of credential scope and Codex client approvalPOKER_MCP_ALLOWED_ORIGINSandPOKER_MCP_*_CALLS_PER_MINUTE: exact browser origins and per-principal hosted read/write limitsPOKER_SENTRY_DSN: optional HTTPS Sentry DSN for scrubbed unhandled backend exception reports; leave empty to disablePOKER_SENTRY_ENVIRONMENT,POKER_SENTRY_RELEASE, andPOKER_SENTRY_ERROR_SAMPLE_RATE: optional error-report attribution and sampling controls
See apps/backend/.env.example for the complete local contract and infra/docker/backend.env.example for container-oriented values.
Bearer tokens are masked by the settings model and sent only in the standard
Authorization: Bearer ... header. Any external URL paired with a token must
use HTTPS. Keep deployed token values in Coolify secrets.
Poker Hero includes a curated MCP gateway for post-hand training agents. It can run locally over stdio or be hosted by the deployed backend over authenticated Streamable HTTP. It calls the same FastAPI contract as the browser, so parser evidence, explicit approval, provider routing, rate limits, request IDs, and persisted job state remain authoritative. It never reads providers or poker stores directly.
Create a separate client configuration for each environment using
apps/backend/mcp.env.example. Every process
requires POKER_MCP_ENVIRONMENT and POKER_MCP_API_BASE_URL. Before any data
operation, it checks /api/health and refuses a backend whose
POKER_DEPLOYMENT_ENVIRONMENT does not match. Production is always read-only.
Staging write tools appear only with POKER_MCP_ALLOW_WRITES=true, and local
screenshot paths must resolve under an explicitly configured
POKER_MCP_IMAGE_ROOT.
After exporting the selected configuration into the MCP process environment, use this command in an MCP client that supports local stdio servers:
pnpm backend:mcpThe read surface covers environment status, processing jobs, individual jobs, history, training progress, and parser benchmarks. The staging write profile adds screenshot submission, reviewed-state approval, pre-reveal decisions, recommendations, and lesson reviews. Backup restore, dataset import, benchmark execution, and bulk archival are intentionally not exposed.
Cloudflare Access service credentials are the preferred authentication path
through a protected Worker. POKER_MCP_API_PROXY_SECRET exists only for a
trusted gateway deployment that calls the backend directly; do not give the
Worker-to-backend shared secret to an untrusted agent.
For the Nexcue-style hosted workflow, enable the staging route with
POKER_MCP_ENABLED=true and an exact HTTPS POKER_MCP_PUBLIC_URL ending in
/mcp. Configure the deployment's separate MCP_ADMIN_TOKEN, unlock
About → Agent access with it, create an environment-bound credential, store
the one-time token in an environment variable, and configure Codex:
[mcp_servers.poker_staging]
url = "https://<staging-agent-origin>/mcp"
bearer_token_env_var = "POKER_MCP_STAGING_TOKEN"
default_tools_approval_mode = "writes"Hosted credentials are stored only as hashes and can be rotated or revoked.
The hosted endpoint omits local screenshot submission; upload the hand in the
app first. See
docs/reference/mcp-agent-access.md
for rollout and incident steps.
With POKER_RECOMMENDATION_PROVIDER=local_solver, the default
postflop_solver engine runs the pinned Rust Discounted CFR adapter. It accepts
heads-up flop, turn, and river states when hero_position identifies IP,
OOP, or button, or when reviewed hero and opponent seats establish an
unambiguous postflop order. A small-blind versus big-blind pair still requires
explicit IP/OOP review unless an exact called big-blind isolation, called
limp-reraise, squeeze, or cold 4-bet history resolves it. The called-isolation
and limp-reraise routes use the six-max chart contract when reviewed seats are
canonical blinds. On the limp-reraise route, a reviewed dealer/button alias
instead identifies the heads-up small blind and preserves its postflop IP
order. The other routes prove a multi-seat preflop line. Its ranges, bet tree,
iteration target, rake,
timeout, and memory ceiling are configurable through the
POKER_POSTFLOP_SOLVER_* variables in the example env files.
In the default contextual range mode, an exact two-player state with a
single 1 BB limp checked by the big blind, the same limp followed by a 2-5 BB
big-blind isolation raise and matching call by the original limper, a 1 BB
limp followed by a 2-5 BB isolation raise, original-limper reraise, and
isolator call, an open-and-call, ordinary open/3-bet/call, cold-caller
open/3-bet/call, open/call/squeeze/call, or
open/3-bet/4-bet/call preflop history, including a later cold 4-bettor after the
opener folds, selects transparent ranges from the same position-aware chart
boundaries used by the preflop trainer. The limped route
uses the limper's stack-adjusted first-in range as an explicit proxy and the
complement of the big blind's isolation-raise band as the checked range.
The called-isolation route uses that big-blind isolation band as the raiser's
range and the limper's adjusted continue band after excluding limp-reraises as
the calling range. Concrete reviewed seats or a consistent opposing OOP/IP
pair identify which represented actor owns each range. Non-big-blind isolation
raises retain configured ranges because the bundled chart does not define
their initial isolation band.
The called limp-reraise route uses the limper's adjusted reraise band and the
isolator's ratio- and stack-adjusted continue band after excluding 4-bets.
Unlike the called-isolation route, it supports every legal limper/isolator pair
because the chart defines both sides of those matchups.
Dead-money routes require the original opener to be absent
from the two reviewed survivors and keep that player's opening contribution in
the reconstructed root pot. The squeeze route applies the chart's existing
single-caller adjustment to the squeezer and its named squeeze-response policy
to the caller. The cold 4-bet route uses the cold player's adjusted 4-bet band
and the original 3-bettor's continue band after excluding 5-bets. The reviewed
sizes and a provable starting effective stack apply the remaining chart
response adjustments, and the backend verifies that the reconstructed
flop-root pot agrees with every represented commitment. A turn
additionally requires one terminal completed-flop history; a river requires
terminal completed-flop and completed-turn histories in order. Each completed
history ends in check-check or a matching call and records each wager as the
actor's total BB committed on that street.
When current-street money cannot be reconciled to the reviewed visible stacks,
the range context retains a labeled 100 BB stack assumption. Unsupported,
incomplete, contradictory, and other preflop trees retain the configured OOP/IP
ranges. Recommendation evidence records the selected source, policy context,
decision street, and completed-street verification. When terminal prior-street
histories and both visible stacks reconstruct the flop root, the adapter first
solves a memory-bounded flop-root tree, replays the reviewed actions and actual
turn/river cards, and uses the resulting posterior hand weights in the normal
current-street solve. The conditioning tree preserves observed prior-street
wagers, allows one configured bet size downstream, and omits unobserved raises;
the decision tree retains the configured bet and raise sizes. Evidence reports
whether conditioning was applied, its simplified tree profile, line reach,
active combinations, memory, and exploitability. Set the mode to configured
to disable contextual preflop selection; reviewed prior-street conditioning can
still refine those configured ranges.
Facing-bet trees also require the visible hero stack so the adapter can
reconstruct whether hero or the bettor was covered before the wager. The
facing action must identify the outstanding wager. Raised heads-up decisions
also require the opponent's visible stack and ordered current-street action
history. Each history wager is the player's total BB committed on that street;
the adapter validates the actors, pot, call amount, stacks, and final hero turn
before replaying the line. Incomplete or contradictory histories use fallback.
Completed-street history is separate from the replayed current-street list. It
both verifies later-street range and stack assumptions and, when the exact root
can be reconstructed within the memory limit, conditions each player's range
through the reviewed prior-street line.
pnpm bootstrap builds the adapter into
solver-plugins/postflop/target/release/poker-postflop-solver; the local backend
commands add that directory to PATH. Re-run bootstrap after pulling changes
to the Rust plugin.
Preflop hands with a recognized six-max position and an unambiguous supported
context use the bundled position-aware training chart. Supported contexts
include an unopened pot, one to five 1 BB limps to the big blind, a single open,
one open followed by one to four callers, a hero 1 BB limp facing one
later-position isolation raise after action returns heads-up,
a single opponent limp followed by hero's isolation raise and a reraise by the
original limper after action returns heads-up,
a hero open facing a 3-bet, a bounded opponent-open/opponent-3-bet sequence, and
a hero 3-bet facing either an opener 4-bet or a later cold 4-bet after the
opener folds. It also supports hero cold-calling an open, facing a later
squeeze, and acting heads-up after the opener folds. The chart uses
position-matchup boundaries plus
transparent adjustments for 2-4 BB opening sizes, supported 3-bet ratios, and
short, medium, standard, or deep effective stacks.
First-in ranges and sizing also adjust by stack depth. The chart reports its
hand-class ranking, base and adjusted policy, assumptions, and action frequencies
without presenting the result as a solved preflop tree. Approved states may
provide the opener position and total opening size as structured fields. An
ordered preflop history can represent a single open, one open followed by one
to four calls before hero, one to five ordered 1 BB limps before hero's
big-blind option, exactly
one hero open followed by one later-position 3-bet, or one opponent open followed
by one opponent 3-bet before hero. A complete
three-raise history can also represent an opponent open, hero 3-bet, and opener
4-bet with action returning to hero, or a later-position cold 4-bet after the
opener folds and action returns heads-up to hero. An open, matching hero call,
and later squeeze can represent the same heads-up return after the opener folds.
A hero 1 BB call followed by one later-position 2-5 BB raise can represent a
heads-up isolation response after every other player folds. This route requires
known hero and effective stacks, validates the call amount and reconstructed
pot, and exposes its position, size-band, stack, response-range, and raise-cap
adjustments as evidence.
One opponent 1 BB call, a 2-5 BB hero isolation raise, and a bounded reraise by
that same limper can represent the corresponding heads-up limp-reraise response.
This route requires the limper to act before hero, validates a full reraise up
to 4x the isolation total, and applies dedicated limper-versus-isolator,
size-band, and stack-depth continue/four-bet boundaries. Its evidence retains
the original limper, hero isolation total, limp-reraise total and ratio, named
policy, adjusted range, and reconstructed all-in cap.
The called-open routes require exactly three through six active players,
matching open/call totals, and legal seat order. They apply explicit conservative
range multipliers and 4x-open through 7x-open squeeze targets for one through
four callers, respectively. The cold 3-bet route also requires exactly three
active players and legal opener-3-bettor-hero order, then applies explicit
three-seat continue/four-bet boundaries. The chart validates position order,
total action sizes, amount to call, pot composition, and stack availability
before routing. The heads-up 4-bet route applies explicit matchup and size-band
continue/five-bet boundaries, with five-bets modeled as capped all-ins. The
cold 4-bet route uses narrower three-seat policies and keeps the folded
opener's commitment in pot validation. The squeeze-response route likewise
uses explicit three-seat policies, retains hero's prior call, and validates the
folded opener's dead money.
The heads-up limp route requires exactly one active limper and hero in the big
blind, validates the pot against blinds and the limp, then uses explicit
limper-position and stack-depth isolation ranges with a capped target size. The
multi-limper routes similarly require exactly one more active player than the
two through five distinct ordered 1 BB calls, plus hero's big-blind option. They
use explicit policies for every legal limper pair, triple, four-seat group, or
full-table sequence, progressively tighter stack-adjusted isolation ranges,
1.5x-pot targets of at least 5 BB through 8 BB, and the same effective-total cap.
Action-text parsing remains for older saved single-open hands.
Positionless and unsupported limped spots, including mismatched active-player
counts, action pending behind hero, or an isolation raise with another active
player, caller histories beyond the terminal six-max ordering, unsupported
squeezes with an active opener, another caller, or action behind, cold 4-bets
with action behind or another active player, unsupported limp-reraises, longer
preflop trees, multiway postflop states, and
incomplete, ambiguous-position, oversized, or failed trees use local_ev when
fallback is enabled. Recommendations preserve
the requested engine and routing or fallback reason in raw metadata. Set
POKER_LOCAL_SOLVER_ENGINE=local_ev to bypass both CFR and the preflop chart
and run the range/EV engine directly. Select external_solver at the provider
boundary for a future licensed service.
When a user locks an answer before revealing guidance, training progress tracks action and exact-line accuracy. The answer may include a low, medium, or high self-rated certainty, allowing progress to compare accuracy and available EV loss by how sure the player felt before reveal. Rated certainty groups with at least two hands compare equal recent and previous windows for those metrics. Unrated hands remain in overall progress and are excluded only from certainty calibration. A rated certainty group or the unrated bucket can filter Recent decisions to its newest matching hands without changing global progress or the pending-review queue. Street groups with at least two hands expose the same equal-window performance comparison and drill-down behavior. Approved hero positions are normalized into common six-max and IP/OOP labels so the same metrics can be compared by position; hands without a position remain counted separately. A normalized position or the unpositioned bucket can apply the same bounded Recent decisions filter. Position groups with at least two hands also expose those equal-window comparisons. Position rows and the unpositioned bucket also expose unresolved counts and can open a focused needs-review queue. That review position composes with street, certainty, action-pattern, and ordering controls while remaining separate from the Recent decisions position filter. Solver responses also contribute engine coverage by street, including unattributed legacy hands and recorded fallback frequency and reasons. Each attributed engine route and fallback reason also reports the player's action and exact-line accuracy plus average EV loss when candidate EVs are available. Groups with at least two hands also compare equal recent and previous windows for those performance metrics. Intentional routing, such as using the preflop chart for a supported preflop hand, is not counted as fallback. An engine route, the unattributed legacy bucket, or a fallback reason can also filter Recent decisions to its newest matching hands without changing global progress or the pending-review queue. Once two reviewed hands are available, coverage also compares equal recent and previous windows, showing whether engine attribution and recorded fallback use are moving in the desired direction. Responses with complete candidate EV metadata also report the selected line's EV loss in BB plus aggregate and street-level averages; providers without comparable EVs remain ungraded for that metric. The needs-review queue can stay newest-first or prioritize the highest available EV losses while retaining ungraded hands afterward. It can also focus on one street; filtering happens before ordering and the bounded queue limit. Training progress suggests an actionable focus street from the pending queue, preferring the highest graded average EV loss and otherwise the lowest action accuracy. Street rows show their unresolved count and can open that street queue directly. Reviews can also be scoped to low, medium, or high self-rated certainty, or to legacy hands with no rating; certainty combines with street and action-pattern filters before ordering and limiting. Rated calibration rows show their unresolved count and can open that certainty queue directly. Progress also suggests which rated certainty backlog to review first, preferring the highest graded average EV loss and otherwise the lowest action accuracy. The Unrated backlog is suggested only when no rated certainty group has pending work. Legacy hands appear in a separate Unrated row with no fabricated accuracy or EV metrics and the same review shortcut. Progress also suggests a normalized position backlog using the same EV-loss and action-match ranking. Unpositioned hands are suggested only when no scored position has pending work. Progress also compares equal recent and previous windows, capped at ten hands each, so action, exact-line, and available EV-loss movement is visible without letting a larger period skew the trend. Common unsupported action choices are grouped by the player's action and the solver's headline action, with available average EV loss, to make repeated differences easier to study. Progress suggests the unresolved pattern with the highest comparable average EV loss, or the largest pending backlog when every pattern is ungraded. Each pattern shows its unresolved count and can open a review queue scoped to that exact action pair; position, street, certainty, and EV-loss ordering remain available within the focused queue. Completed patterns show a clear state instead of a disabled action. Completing a hand opened from that queue reloads the same filters and opens the next matching hand, then returns to the empty queue when the session is complete. A review can include a short lesson note. Notes stay attached to the hand in progress history and return to the editor when a review is reopened. Completed notes also appear in a dedicated Lessons view, newest first by default and kept independently from the shorter recent-decisions list. A completed lesson note can be edited or removed in place without reopening the review. The Lessons view can filter the full saved set by street and case-insensitive note text, then order it by recency or highest available EV loss before applying its bounded display limit. Ungraded lessons remain after graded lessons in EV-loss order. The active study set can be downloaded as Markdown in the same order without the display limit. Changing the approved state, training answer, or recommendation clears the note because the comparison it described is no longer current. Clearing completed processing items persists an archive timestamp on each backend job. The history rail restores the latest archived hands once per browser session, keeps a small local cache for immediate rendering and fallback, can load older archived hands in bounded pages, and can be refreshed explicitly after another device archives work. Saving changes to a reopened archived hand updates its history card and bounded browser cache immediately. Server-backed search can find older hands by filename, cards, table context, recommendation, lesson text, screenshot title, notes, or tags without replacing that newest-page cache. Every queue and history item exposes screenshot details where those metadata fields can be edited. The same dialog can permanently remove the job, original image, analysis, training record, and benchmark-corpus membership, so an incomplete parse never has to remain stuck in processing. Unarchived upload and capture jobs also survive reloads: the browser renders a bounded local queue cache immediately, then reconciles the complete oldest-first processing projection from the backend. Dataset-only benchmark imports stay out of that operational queue. Bounded browser-session mutation leases keep uncertain writes, uploads, and batch archives unsynchronized across a same-tab reload until the backend projection proves the operation completed or the recovery window expires. Benchmark dataset imports additionally carry a client-generated request ID. Before parsing or changing the corpus, the backend atomically publishes a pending journal containing the size-bounded archive. Validation failures and completed results are persisted in that journal, while interrupted or partial imports can resume idempotently from the same archive. A dropped response or same-tab reload can therefore recover newly created benchmark-only cases that intentionally appear in neither operational projection.
The app information dialog can export a versioned full-data backup containing every job record, original screenshot, screenshot title, notes and tags, training decision, review and lesson note, recommendation, history timestamp, benchmark selection, and benchmark report. Restore validates the complete ZIP, member paths, Pydantic records, image payloads, limits, and SHA-256 checksums before writing. Missing records are merged, exact records are reused, and a divergent existing job, image, or report rejects the restore without overwriting current data. Provider credentials, environment configuration, and transient import journals are intentionally excluded.
Operators can create and validate the same archive format without the browser:
pnpm backend:backup init-volume
pnpm backend:backup export ./backups --retain 14
pnpm backend:backup verify ./backups/<archive>.zip
pnpm backend:backup drill ./backups/<archive>.zipThe drill restores only into temporary isolated storage, verifies a repeated restore is idempotent, and compares a re-export with the source. Operational exports coordinate with live API mutations through a shared data-volume lock, then durably publish and rotate archives under a destination lock. See the deployment runbook for Coolify mounts, scheduling, off-host copies, and recovery procedure.
Export approved ground-truth hands from the Parser benchmark dialog, then run the same field-level evaluation without importing them into the configured data directory:
pnpm backend:benchmark ./poker-hero-parser-dataset.zip \
--parser-provider ocr_cv \
--layout-profile fortuna_nations \
--minimum-cases 25 \
--minimum-accuracy 0.90 \
--minimum-field-cases hero_cards=25 \
--minimum-field-accuracy hero_cards=0.98 \
--minimum-field-accuracy board_cards=0.98 \
--minimum-field-accuracy pot_size=0.90 \
--minimum-field-accuracy current_bet=0.90The command prints overall and per-field accuracy plus cases needing review. Add
--json for a complete machine-readable report. It exits with status 1 when
any case fails or a configured corpus, label-count, overall-accuracy, or
field-accuracy threshold is missed. Repeat --minimum-field-cases and
--minimum-field-accuracy for cards, street, pot, bets, stacks, player count,
and position. Keep a separately labeled dataset and threshold command for each
client/layout so a strong result on one table cannot hide a regression on
another. Evaluation uses temporary storage and never changes the configured
POKER_DATA_DIR or the source ZIP.
Capture a trusted full report and use it to gate later runs from the same parser, layout, and labeled corpus:
pnpm backend:benchmark ./poker-hero-parser-dataset.zip \
--parser-provider ocr_cv \
--layout-profile fortuna_nations \
--json > ./fortuna-ocr-baseline.json
pnpm backend:benchmark ./poker-hero-parser-dataset.zip \
--parser-provider ocr_cv \
--layout-profile fortuna_nations \
--baseline-report ./fortuna-ocr-baseline.json \
--maximum-accuracy-drop 0.01 \
--maximum-field-accuracy-drop hero_cards=0 \
--maximum-field-accuracy-drop board_cards=0 \
--maximum-field-accuracy-drop pot_size=0.02Drop values are accuracy ratios, so 0.01 permits one percentage point. The
human report prints overall and per-field changes; --json keeps stdout as the
current full report while regression failures remain on stderr. A baseline with
a different parser, layout, or corpus fingerprint is rejected. The fingerprint
includes selected hand IDs, approved labels, and screenshot contents. After
changing selected hands, screenshots, or approved labels, review the new corpus
and deliberately capture a replacement baseline.
When multiple compatible parser plugins are enabled, the in-app benchmark dialog compares each plugin's latest saved run for the selected layout. Reports are fingerprinted against their labeled corpus; changed labels, selected hands, and legacy reports without a fingerprint are marked for rerun rather than shown as current. A comparison row switches both the active recognition plugin and the scoped report history without loading full case details for the other plugins. Run comparison benchmarks every available compatible parser in sequence; a provider failure is reported without discarding successful runs from the same comparison.
Run the configured recommendation provider against a versioned JSON corpus of canonical hands and trusted reference policies:
pnpm backend:recommendation-benchmark ./recommendation-benchmark.json \
--provider local_solver \
--minimum-action-accuracy 0.90 \
--minimum-conditioning-accuracy 1.00 \
--minimum-conditioning-coverage 1.00 \
--minimum-range-source-accuracy 1.00 \
--minimum-range-source-coverage 1.00 \
--maximum-ev-loss 0.05The report measures supported-action and exact sizing-line agreement, mixed
policy distance, reference EV loss, provider failures, and recorded fallback
use. Version-3 corpora can require turn/river range-conditioning evidence;
version-4 postflop cases can additionally require the exact configured or
contextual range source. Both report separate agreement and evidence coverage.
Optional thresholds make the command suitable for local regression gates and
CI; --json emits the complete case report. It reads no screenshots and does
not change application data. Reference
policy frequencies and EVs must come from a trusted solver or reviewed strategy
source rather than the provider being evaluated. See
the recommendation benchmark format.
Capture a trusted report and gate direction-aware metric changes against the same provider and normalized reference corpus:
pnpm backend:recommendation-benchmark ./recommendation-benchmark.json \
--provider local_solver \
--json > ./local-solver-baseline.json
pnpm backend:recommendation-benchmark ./recommendation-benchmark.json \
--provider local_solver \
--baseline-report ./local-solver-baseline.json \
--maximum-metric-regression action_accuracy=0.01 \
--maximum-metric-regression average_policy_distance=0.02 \
--maximum-metric-regression average_ev_loss=0.05 \
--maximum-metric-regression fallback_rate=0 \
--maximum-street-metric-regression river:average_ev_loss=0.02 \
--maximum-tag-metric-regression facing-bet:action_accuracy=0.01 \
--maximum-case-metric-regression action_accuracy=0Street and tag gates use the same direction-aware metric keys and catch scoped
regressions that aggregate results can hide. Case gates apply a metric threshold
to each trusted hand independently and fail when previously available evidence
disappears. Ratio deltas use 0.01 for one percentage point; EV-loss deltas are
BB. JSON stdout remains a reusable current report, while gate failures use
stderr.
Run the full local stack:
pnpm docker:upThe PWA is available at http://localhost:8080, the backend at
http://localhost:8000, and job data is kept in the poker-data volume.
The backend image compiles and includes the pinned Rust postflop solver plugin.
Build either image directly from the repository root:
docker build -f apps/backend/Dockerfile -t poker-hero-backend .
docker build -f apps/pwa/Dockerfile -t poker-hero-pwa .Each staging and production deployment uses two services:
apps/pwadeploys to Cloudflare Workers Static Assets. Its Worker proxies same-origin/api/*requests to the configured backend.apps/backenddeploys as a Docker service in Coolify with persistent storage mounted at/app/data.
For each Coolify application, use repository-root build context and Dockerfile
path apps/backend/Dockerfile. Expose port 8000 and mount a separate
persistent volume at /app/data. Pushes to main deploy staging, v* tags
deploy production, and manual workflows can select either environment.
The deployment workflows require repository secrets CLOUDFLARE_API_TOKEN and
COOLIFY_API_TOKEN, plus these repository variables:
CLOUDFLARE_ACCOUNT_IDAPP_WORKERS_SUBDOMAIN, for examplestudio81COOLIFY_API_BASE_URL
Each staging and production GitHub environment requires:
BACKEND_URL, the environment's backend origin used by the Worker proxyCOOLIFY_BACKEND_UUID, the environment's Coolify applicationAPP_WORKER_NAME, distinct between environmentsAPI_PROXY_SECRET, matching that backend'sPOKER_PROXY_SHARED_SECRET
Staging, and any other environment before enabling hosted MCP, also requires
MCP_ADMIN_TOKEN, the separate operator bearer for principal management.
For a deployed backend, set the same random value in the Cloudflare
API_PROXY_SECRET secret and Coolify POKER_PROXY_SHARED_SECRET environment
variable, and use an https:// BACKEND_URL. The Worker replaces any incoming
copy of its private header before proxying. FastAPI then rejects direct requests
to application API routes that do not carry the configured value; /api/health
remains available to platform health probes.
Backend responses carry X-Request-ID, which is also written to structured
container access logs for tracing requests through the Worker and Coolify.
Runtime error monitoring is disabled by default. Set POKER_SENTRY_DSN in
Coolify for backend failures and the public VITE_SENTRY_DSN environment
variable for browser failures. The PWA deployment
uses its commit SHA as the release. Both adapters remove poker state, request
bodies and metadata, user context, breadcrumbs, local variables, and free-form
exception text before sending an event. Browser tracing and replay are off.
Errors retain stack locations, exception type, environment/release, component,
and backend request correlation tags.
The Uptime Monitor workflow checks the deployed SPA, proxied health route,
and protected queue route every hour. Scheduled runs derive the staging URL from
APP_WORKER_NAME and APP_WORKERS_SUBDOMAIN, or use the optional
UPTIME_MONITOR_URL environment variable. Manual runs can select staging or
production. A failed probe opens one environment-specific GitHub outage issue;
the next successful probe closes it. Set optional UPTIME_ISSUE_ASSIGNEE in
each environment to a GitHub login that should receive the incident assignment.
Leave VITE_API_BASE_URL unset for the deployed Worker so browser requests use
same-origin /api/*.
See docs/process/deployment.md for the complete deployment checklist and docs/reference/architecture.md for the runtime topology.
GET /api/healthPOST /api/jobsGET /api/jobs/{job_id}PUT /api/jobs/{job_id}/metadataDELETE /api/jobs/{job_id}GET /api/jobs/{job_id}/imageGET /api/historyPUT /api/historyPOST /api/jobs/{job_id}/approvePUT /api/jobs/{job_id}/decisionPOST /api/jobs/{job_id}/recommendPUT /api/jobs/{job_id}/training-reviewDELETE /api/jobs/{job_id}/training-reviewPUT /api/jobs/{job_id}/benchmarkGET /api/training/progressGET /api/training/lessons/exportGET /api/benchmarks[?parser_provider=...&parser_layout_profile=...]GET /api/backups/exportPOST /api/backups/restoreGET /api/benchmarks/exportPOST /api/benchmarks/importGET /api/benchmarks/{report_id}POST /api/benchmarks/run
Private - all rights reserved, except solver-plugins/postflop, which links the
AGPL-3.0-or-later postflop-solver project and is distributed under that
license.