# chart FastAPI backend + Vue 3 (from CDN, no build step) served at . The app charts Yahoo's `ES=F` feed, builds CME-session-aware timeframes and daily moving averages, and alerts on confluence zones. The full spec lives in [`docs/IMPLEMENTATION_PLAN.md`](docs/IMPLEMENTATION_PLAN.md) — read it before writing code; it records decisions and verified API facts that are expensive to rediscover. ## Local development ```bash docker compose up --build ``` Then open . Override the host port with, for example, `PORT=8020 docker compose up`. The port binds to all host interfaces, so another machine can connect at `http://HOST_IP:8010`. The source tree is bind-mounted and uvicorn runs with `--reload`, so edits to `main.py` or `static/` take effect without a rebuild. Rebuild only when `requirements.txt` changes. The Compose stack also includes Playwright for browser screenshots. It reaches the app over the internal Compose network and writes ignored artifacts locally: ```bash docker compose exec playwright playwright screenshot \ --lang en-US --wait-for-timeout 5000 http://api:8000 /artifacts/chart.png ``` Without Docker: ```bash python3 -m venv .venv && . .venv/bin/activate pip install -r requirements.txt uvicorn main:app --reload ``` Copy settings from `.env.example` as needed. To recalibrate the alert threshold against Yahoo's current eight-day minute tape: ```bash python3 -m scripts.calibrate_alerts ``` The M4 calibration on 2026-08-09 replayed 8,065 minute bars across seven sessions. Threshold `12` generated 210 alerts from lone daily MAs; `24` and the selected `28` generated none. The selected threshold deliberately requires at least three clustered daily MAs (score `36`) and should be revisited as more varied tapes are recorded. ## Layout | Path | Purpose | |---|---| | `main.py` | FastAPI lifespan and app wiring; JSON under `/api`, SPA at `/` | | `app/` | Market sources, aggregation, analysis, alerts, and API | | `static/` | `index.html`, `app.js`, `style.css` — Vue 3 loaded from unpkg | | `requirements.txt` | Python deps | | `Procfile` | Start command; **nixpacks needs this** or the deploy has nothing to run | | `bin/wait-deploy` | Blocks until the live site serves your latest commit | | `Dockerfile.dev`, `docker-compose.yml` | Local dev only — production does not use them | ## Deployment Push to `main` → Forgejo webhook → Coolify rebuilds with nixpacks → live. Rebuild time depends on whether Docker's build cache is warm: measured at **~20s warm** (you pushed recently) and **~90s cold** (the cache goes stale after an idle hour or so, which is the usual case). Nothing changes on the site until the new container swaps in at the very end, so the old version keeps serving for the whole build. To know when your commit is actually live, rather than guessing: ```bash git push && bin/wait-deploy ``` It polls `/api/version` (which returns the `SOURCE_COMMIT` Coolify bakes into the container) until it matches your local `HEAD`, then exits. Runs from any machine — no Coolify token, no SSH tunnel. Check by hand any time with: ```bash curl -s https://chart.amow.com/api/version ``` One more trap worth naming: if your change only touches an `/api` endpoint, the HTML is byte-identical and a browser refresh looks like nothing happened even after a successful deploy. Verify the endpoint, not the page. Two things to know before changing the deploy config: - The Coolify domain is registered as `https://chart.amow.com:8000`. The port suffix is how Coolify decides the Traefik target port; drop it and every request 502s while the container looks perfectly healthy. It does not appear in the public URL. - Ignore a 502 in the first minute after a deploy — that's the rolling container swap, and it clears on its own. Manual redeploy: ```bash curl -X POST -H "Authorization: Bearer $(cat ~/.coolify-token)" \ "http://127.0.0.1:8000/api/v1/deploy?uuid=dgvch0xqv8uvjfor7dl8bwl9&force=true" ``` ## Access token `CHART_AUTH_TOKEN` guards everything under `/api` plus the `/ws` stream. Leave it blank and the app is wide open, which is what you want locally — nothing prompts. Set it and every request needs the token, as the `X-Chart-Token` header or a `?token=` query parameter (WebSocket handshakes can't carry headers, hence the second form). In production the token lives in **Coolify's environment variables**, not in this repo and not in `.env` — that file is gitignored and never exists in the built container. Coolify re-injects its env vars into every container it builds, so the token survives redeploys and reboots. The browser asks for it once on the first 401 and keeps it in `localStorage`. To clear it: `localStorage.removeItem('chart-token')`. `/api/health` and `/api/version` deliberately stay open — `bin/wait-deploy` polls the latter from whatever machine you pushed from, and neither reveals anything about the market data or the configuration. ## Saved trendlines Manual trendlines are written to `MANUAL_LINES_PATH` (`./data/manual_lines.json`). In production `/app/data` is a **Coolify persistent volume** — without it the container filesystem is ephemeral and every deploy would silently wipe every line you've drawn.