Deployment
make docker-* targets. The systemd / SQLite material below describes
the legacy Oracle setup and the still-present make deploy/sync/restart targets.
Package entry points
# All three are equivalent — pick whichever you prefer
python run.py
python -m arb_bot
# On Linux server the systemd service uses:
ExecStart=venv/bin/python run.py
Makefile shortcuts (from local machine)
Requires .env.deploy with DEPLOY_SSH_KEY, DEPLOY_USER (root), DEPLOY_HOST (187.127.181.8), DEPLOY_REMOTE_DIR (/opt/trading-bot). See Infra & Services for the full Docker reference.
| Command | What it does |
|---|---|
make docker-deploy | Build HTML+frontend → rsync → DB migrate → rebuild & restart bot services (and re-resolve nginx) |
make docker-deploy-code | Code-only deploy (skip the frontend rebuild) |
make docker-status | docker compose ps on the server |
make docker-logs | Tail live container logs (Ctrl+C to stop) |
make docker-restart | Restart arb-bot dashboard token-refresher |
make docker-ssh | SSH into the Hostinger server |
make docker-setup | One-shot VPS provisioning (Docker + Node + ufw) |
make ssl-init | Issue the Let's Encrypt certificate (after DNS points here) |
make build-html | Rebuild public/index.html + React app locally (run after editing any section file) |
dashboard container gives it a new IP. nginx caches the
dashboard upstream at startup, so docker_deploy.sh restarts nginx after
every deploy — otherwise the proxy serves 502s (and OTP/login stop working) until nginx
re-resolves.
public/sections/, then run make build-html.
The generated public/index.html is what gets deployed to Netlify. Never edit index.html directly.
make fetch-metrics → downloads legacy metrics/*.json files locally →
run python backtest_recovery.py to analyse partial-fill recovery performance, or
use /analyze-trading-metrics in Claude Code for Config tuning recommendations.
Architecture Overview
The dashboard follows a modern decoupled architecture for better performance and interactivity:
- Frontend: A React Single Page Application (SPA) built with Vite and Tailwind CSS. Located in
frontend/, built intopublic/dist/. - Backend: A FastAPI server (
dashboard_server.py) that provides real-time PnL, position tracking, and documentation metadata via REST APIs. - Nginx Proxy: Acts as the entry point, serving static files and proxying API requests to the FastAPI backend.
Systemd Services
Two separate services run on the production server:
| Service | Purpose | Entry Point |
|---|---|---|
arb-bot.service | The core trading engine | run.py |
token-refresher.service | Dhan token renewal independent of the bot loop | token_refresher.py --interval-sec 300 |
arb-dashboard.service | FastAPI backend for dashboard | dashboard_server.py |
The token refresher unit template lives at scripts/token-refresher.service. The dashboard shows its last stored token health through /api/token/health.
Nginx Configuration
Nginx redirects all port 80 traffic, including direct IP requests, to https://portfolioplanner.online. The HTTPS server serves the SPA from /var/www/html/dist/ and proxies /api/* requests to localhost:8080. Configuration is maintained in scripts/nginx_dashboard.conf.
The dashboard server block includes 92.4.66.95, portfolioplanner.online, and www.portfolioplanner.online as accepted host names so domain traffic does not fall through to the default Nginx site.
Deployed documentation routes are not public. Requests to /docs/* use Nginx auth_request against /api/auth/nginx-check, which accepts only a valid dashboard session cookie. Unauthenticated users are redirected to /login before any docs route is served.
Dashboard Session Policy
Dashboard login is per-user Telegram OTP with an HttpOnly session cookie identifying a persisted, revocable session row (not a process-global singleton). Multiple users, and multiple sessions for the same user, remain valid concurrently — logging in again never invalidates another session, and logout revokes only the current one. See Multi-User Auth & RBAC for the full login flow, role model, and admin bootstrap.
MCP & Google linking
The self-service link flow is default-off. Runtime values belong only to the
dashboard service; the Google OAuth credentials are
bootstrap-time inputs and are deliberately blanked in every application
container. Other services also receive blank/pinned-safe values so a shared
env_file cannot accidentally enable linking or fail while
parsing numeric TTLs.
| Variable | Scope | Contract |
|---|---|---|
MCP_GOOGLE_LINKING_ENABLED | dashboard | Default false; enable only after bootstrap. |
DASHBOARD_MCP_LINK_CLIENT_ID | dashboard + bootstrap | Defaults to dashboard-mcp-link. |
DASHBOARD_MCP_LINK_CLIENT_SECRET | dashboard + bootstrap | At least 32 characters; never log or expose to the SPA. |
DASHBOARD_COOKIE_SECURE | dashboard | Must remain enabled for an HTTPS callback so the step-up grant is never sent over plaintext HTTP. |
DASHBOARD_MCP_LINK_OIDC_ISSUER | dashboard | HTTPS Keycloak realm issuer; must equal AGENT_OAUTH_ISSUER when both are set. |
DASHBOARD_MCP_LINK_OIDC_TOKEN_ENDPOINT | dashboard | Optional override; when set it must have the same origin as the issuer. |
DASHBOARD_MCP_LINK_OIDC_JWKS_URI | dashboard | Optional override; when set it must have the same origin as the issuer. |
DASHBOARD_MCP_LINK_REDIRECT_URI | dashboard + bootstrap | HTTPS URL with exact path /api/auth/mcp-link/callback; must exactly match Keycloak. |
DASHBOARD_MCP_LINK_LINK_TTL_MINUTES | dashboard | Positive integer; default 10. |
DASHBOARD_MCP_LINK_STEP_UP_TTL_MINUTES | dashboard | Positive integer; default 7. |
DASHBOARD_MCP_LINK_MCP_URL | dashboard | HTTPS MCP endpoint copied by the user; default https://agents.portfolioplanner.online/mcp. |
BROKER_SECRET_KEY | dashboard | Stable valid Fernet key used to encrypt the PKCE verifier at rest; generate once, store securely, and preserve across restarts. |
KEYCLOAK_GOOGLE_CLIENT_ID | bootstrap only | Google OAuth Web Application client id. |
KEYCLOAK_GOOGLE_CLIENT_SECRET | bootstrap only | Google OAuth Web Application secret; never injected into the dashboard. |
The dashboard startup validator requires non-placeholder HMAC and client secrets (at least 32 characters), a valid stable Fernet key, secure cookies for HTTPS, secure URLs (localhost HTTP is the development exception), exact callback path, positive TTLs, issuer agreement, and issuer-origin token/JWKS overrides. It reports variable names only and never echoes values.
docker compose cp scripts/keycloak_bootstrap.sh keycloak:/tmp/bootstrap.sh
docker compose exec \
-e AGENT_KEYCLOAK_ADMIN \
-e AGENT_KEYCLOAK_ADMIN_PASSWORD \
-e AGENT_PUBLIC_BASE_URL \
-e KEYCLOAK_GOOGLE_CLIENT_ID \
-e KEYCLOAK_GOOGLE_CLIENT_SECRET \
-e DASHBOARD_MCP_LINK_CLIENT_ID \
-e DASHBOARD_MCP_LINK_CLIENT_SECRET \
-e DASHBOARD_MCP_LINK_REDIRECT_URI \
keycloak bash /tmp/bootstrap.sh
The bootstrap always reconciles the base Agent Gateway realm. Linking
resources use an all-or-none group: when all of
KEYCLOAK_GOOGLE_CLIENT_ID,
KEYCLOAK_GOOGLE_CLIENT_SECRET, and
DASHBOARD_MCP_LINK_CLIENT_SECRET are absent, the Google IdP and
dashboard client are skipped; partial configuration fails immediately. With
all three present, re-running reconciles the Google IdP, rotates both client
secrets, enforces PKCE S256, disables direct grants, service accounts, and
token exchange, and installs a client protocol mapper that copies the
identity_provider session note to the ID-token
broker claim.
BROKER_SECRET_KEY in the dashboard runtime
secret store. The script does not print secret values.
MCP_GOOGLE_LINKING_ENABLED=false
and restart the dashboard. API routes return 404 and existing mapping rows
remain for a later re-enable. The identity-link audit table is append-only;
challenge and mapping tables are retained history, not described as
append-only. Fully removing the Google IdP or the dashboard client is a
deliberate Keycloak admin operation; an all-empty bootstrap run skips these
resources and does not delete them.