Separate people with their own drawings, alerts and notifications, behind OIDC against a self-hosted Authentik that can federate Google. Written as phases that each pay for themselves while the app is still single-user, so none of it is scaffolding waiting on a decision. The ordering conclusion worth stating plainly: do not build local accounts. Going to OIDC means the app never stores or hashes a password, so building that first means deleting it later. Shared password to OIDC subject, with nothing in between. One thing to fix regardless: the JWT signing key is sha256 of the password. Today that is merely weak, since anyone holding a cookie can brute-force the password offline. With several users it cannot work at all — either everyone shares a signing key, or the key varies per user and a token cannot be verified without already knowing who sent it. Added to the risk register. The fork that decides the architecture is not an engineering one: whose market data. One shared feed is redistribution, which Schwab's agreement and CME's beneath it generally prohibit; each user bringing their own brokerage account avoids the question entirely but means a stream, a token and a weekly re-auth each, and the shared bar store stops being shared. That answer is only needed before the last phase, which is why it is not a blocker on starting. AGENTS.md points at both planning documents, because the cheapest moment to know whether new state is shared or per-user is while it is being written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.9 KiB
Working on this repo
Read current context first
Before planning work, read docs/IMPLEMENTATION_PLAN.md
for verified decisions and the dated session log, then
docs/NEXT_STEPS.md for current recommendations and known
deferred fixes. Mobile interaction work also has its own detailed plan in
docs/mobile_enhance.md.
Tests earn their place by catching a real bug
When a bug is found, ask whether a unit test could reasonably have caught it. If yes, write that test with the fix. If no — a rendering artefact, a browser quirk, a data-source oddity — say so and don't add one.
The bar is "would this have failed before the fix, and would it fail again if someone reintroduced it". Tests that restate the implementation, assert constructor defaults, or exercise paths nothing depends on are noise; they make the suite slow to run and expensive to change, which is how a suite stops being trusted.
What has actually paid off here: bar aggregation and bucket boundaries, the store's replace-vs-append rules, level and alert arithmetic, parsing real market-data payloads (fixtures are trimmed real responses, not invented), and the invariants that would otherwise be silent — a comment must never become a level, a tick must never overwrite a settled bar, volume must be counted once.
Name the test after the failure, not the function: test_a_tick_cannot_overwrite _a_settled_bar beats test_put.
Where things run
The agent works on a remote machine over SSH. The user's browser runs on a different machine. Consequences, all learned the hard way:
- You cannot see the user's screen, console, or cursor. Screenshots and pasted console output are the only window into it. Browser extensions that drive "your" Chrome do not help — they attach to the machine the browser is on.
- Headless Chromium here renders on server hardware: different screen, window
size and device pixel ratio from the user's. "Works in my headless run" is not
evidence that it works for them. When a UI bug will not reproduce, match their
viewport and
deviceScaleFactorexplicitly before concluding anything. - The dev stack is served to them over the network (e.g.
hera.local:8010), which is the same app the headless browser reaches ashttp://api:8000.
When a visual bug resists reproduction, prefer putting the numbers on screen in the app over asking for another console paste — one screenshot then carries the whole diagnosis.
Verify UI in a real browser
Chart bugs are invisible from the outside — the API, the socket and the frontend source can each be correct while the screen is wrong. Drive the Playwright container against the dev stack:
docker exec -i chart-playwright-1 node - <<'EOF'
const { chromium } = require('/usr/lib/node_modules/playwright');
// launch with args:['--lang=en-US'] — see below
EOF
Always launch Chromium with args: ['--lang=en-US']. The container has no
usable locale, so Chromium reports en-US@posix, Intl throws, and the chart
renders as a blank canvas that looks exactly like a broken app.
window.__chart is a deliberate debug handle. Querying it separates "the data
is missing" from "the data is off-screen" — which is how a viewport bug that
three passing API checks had missed was finally found.
Diagnostic mode
Chart geometry bugs live in the browser, which is usually on a different machine from whoever is debugging them. Rather than asking for console pastes:
open the chart with ?diag=1 # remembered until ?diag=0
docker compose logs api | grep SNAPDBG
With it on, every snap the trendline tool computes is posted to
/api/debug/snap and logged server-side — the cursor's time, price and x, the
snapped time and price, how many bars were held, the first and last bar, and the
chart's width. Throttled to about one a second. It reads the client's own
numbers, which is exactly what "works in my headless run" cannot tell you.
Extend it when the next geometry puzzle appears; the endpoint takes whatever
fields SnapReport declares.
Direction of travel
Two live planning documents, both written to be refactored toward rather than implemented in one go:
docs/async_refactor.md— nothing blocks the event loop. P0 and P1 are done;/api/statusreportsloop_lag_ms, and a rise there is the signal.docs/multi_user.md— separate people with their own drawings and alerts, authenticated by OIDC. Read it before adding state toRuntime: new state is either genuinely shared (market data) or belongs to a user, and knowing which now is much cheaper than untangling it later.
Do not build local user accounts. The destination is OIDC, so password storage would be written and then deleted.
Running tests
docker exec chart-api-1 sh -c "cd /app && python -m pytest -q"
pytest + pytest-asyncio, declared in requirements-dev.txt. Tests live in
tests/, import from app.*, and use tmp_path for anything that persists.
Async paths are driven with asyncio.run(...) directly rather than async test
markers.
Things that will cost you an hour
- Never write scratch
.pyfiles into the repo root. It is bind-mounted, so--reloadrestarts the app, and startup takes ~82 seconds. Pipe throwaway scripts over stdin instead:docker exec -i chart-api-1 python - <<'EOF'. Screenshots intoartifacts/are safe; only.pytriggers the reloader. - Dev and production keep separate drawing stores. Dev writes
data/manual_lines.json; production has its own Coolify volume. A fix that "didn't land" is often the other store. - Rebuild the image after touching
requirements.txt. The bind mount makes source edits look live while an added dependency is simply absent. - A deploy resets alert cooldowns, so production may re-alert on whatever price is sitting on. There is no durable state yet.
- Times are epoch seconds, UTC, everywhere. Only the display is localised — never shift the stored values.