chart.amow.com — FastAPI + Vue3 app
Find a file
Chris Amow 45802c1a2f Fix trendline deletion while typing, audio leak, prefs drift, cluster payload
Five defects found by exercising the running app rather than reading it:

- Backspace inside the sidebar rename field deleted the trendline instead of
  a character. The window keydown handler never checked what was focused, so
  correcting a typo in a line's name destroyed the line.
- playAlert() built a new AudioContext per alert and never closed it. Browsers
  cap a document at roughly six, after which alerts stop making any sound.
  One shared context now, with nodes released on end and a resume() for the
  autoplay policy.
- Stored layer preferences were used verbatim, so any key added to
  defaultPrefs later would be missing for existing visitors. A missing
  enabled.ma is a crash rather than a cosmetic gap. They are now deep-merged
  onto the defaults, and unparseable state falls back instead of throwing.
- The alert log keyed rows on a second-resolution timestamp, so two alerts in
  the same second collided.
- Clusters embedded whole Level objects, including a moving average's entire
  point history — hundreds of entries reaching back years. Because clusters
  are re-sent on every closed 1m bar, this shipped the whole levels payload
  once a minute. Members are now compact summaries and the client joins on
  id; Cluster.to_dict() also stops round-tripping through asdict(), which was
  deep-copying those arrays before discarding them.

/api/confluence drops from 61,838 to 1,245 bytes with five clusters live.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 00:17:40 -05:00
app Fix trendline deletion while typing, audio leak, prefs drift, cluster payload 2026-08-10 00:17:40 -05:00
bin Add /api/version and bin/wait-deploy so deploys are observable from any machine 2026-08-10 01:04:58 +00:00
docs Add implementation plan for /ES multi-timeframe confluence chart 2026-08-09 20:31:40 -05:00
scripts Implement M4 confluence alerts 2026-08-09 20:51:32 -05:00
static Fix trendline deletion while typing, audio leak, prefs drift, cluster payload 2026-08-10 00:17:40 -05:00
tests Fix trendline deletion while typing, audio leak, prefs drift, cluster payload 2026-08-10 00:17:40 -05:00
.env.example Enforce CHART_AUTH_TOKEN on /api and /ws; restore /api/version for wait-deploy 2026-08-10 04:38:36 +00:00
.gitignore Improve local chart development 2026-08-09 22:18:36 -05:00
docker-compose.yml trendlines namable deleteable 2026-08-09 23:00:46 -05:00
Dockerfile.dev Scaffold FastAPI + Vue3 placeholder app with local docker compose stack 2026-08-09 23:34:01 +00:00
main.py Enforce CHART_AUTH_TOKEN on /api and /ws; restore /api/version for wait-deploy 2026-08-10 04:38:36 +00:00
Procfile Scaffold FastAPI + Vue3 placeholder app with local docker compose stack 2026-08-09 23:34:01 +00:00
README.md Merge main into feat/chart-engine 2026-08-10 04:43:51 +00:00
requirements-dev.txt Implement M0 Yahoo market data and replay 2026-08-09 20:36:13 -05:00
requirements.txt Implement M0 Yahoo market data and replay 2026-08-09 20:36:13 -05:00

chart

FastAPI backend + Vue 3 (from CDN, no build step) served at https://chart.amow.com.

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 — read it before writing code; it records decisions and verified API facts that are expensive to rediscover.

Local development

docker compose up --build

Then open http://localhost:8010. 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:

docker compose exec playwright playwright screenshot \
  --lang en-US --wait-for-timeout 5000 http://api:8000 /artifacts/chart.png

Without Docker:

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:

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:

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:

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:

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.