From a85dc0f4b9148d945fba6b8ef751ed2139e887ab Mon Sep 17 00:00:00 2001 From: Tyler Hoang Date: Wed, 10 Jun 2026 00:28:09 -0700 Subject: Simplify deploy script and docs --- .env.example | 2 +- AGENTS.md | 12 ++-- CLAUDE.md | 26 +++----- README.md | 153 ++++++++++++++++++++++---------------------- scripts/deploy.sh | 105 ++++++++---------------------- scripts/post-receive.sample | 32 --------- 6 files changed, 122 insertions(+), 208 deletions(-) delete mode 100755 scripts/post-receive.sample diff --git a/.env.example b/.env.example index 7d2248d..30cf825 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ FMP_API_KEY= FINNHUB_API_KEY= -NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 +NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8001 diff --git a/AGENTS.md b/AGENTS.md index eedd586..2bbaf40 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,15 +21,17 @@ cd frontend && npm install NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8001 npm run dev -- --hostname 127.0.0.1 --port 3001 # Tests + lint -pytest +backend/.venv/bin/pytest cd frontend && npm run lint && npm run build ``` -## Production +## Running And Deployment -Topology: uvicorn on `127.0.0.1:8001`, `next start` on `127.0.0.1:3001`, nginx TLS reverse-proxy (`/api/` → backend, `/` → frontend). See `README.md` for the full deploy and redeploy commands. Service user is `www-data`; code lives at `/var/www/prism-v2/`. +Local runtime: uvicorn on `127.0.0.1:8001`, Next.js on `127.0.0.1:3001`. See `README.md` for clone-to-run instructions. -Deploy flow: bare repo at `/srv/git/prism-v2.git`; pushing master triggers a post-receive hook that `git checkout -f`s the tree into `/var/www/prism-v2/` and runs `scripts/deploy.sh` (build + restart + smoke check). `/var/www/prism-v2/` has no `.git/`. Manual deploy on the server: `sudo ./scripts/deploy.sh` (add `--install` to refresh systemd units / nginx site). See README for first-time setup. +`scripts/deploy.sh` is only a build/setup helper. It creates `backend/.venv` if needed, installs backend requirements, runs `npm ci`, and builds the frontend. It does not install `systemd`, configure `nginx`, restart services, or manage git hooks. + +`systemd/` and `nginx/` are optional reference templates for users who want to wire Prism into their own server setup. ## Coding Style @@ -38,7 +40,7 @@ Deploy flow: bare repo at `/srv/git/prism-v2.git`; pushing master triggers a pos ## Testing -Backend tests are `test_*.py` under `backend/tests/`. Stub `data_service` with `monkeypatch` — never hit live yfinance. Use `tmp_path` for any test that touches SQLite. No frontend test runner; rely on `npm run lint` and `npm run build`. +Backend tests are `test_*.py` under `backend/tests/`. Run them with `backend/.venv/bin/pytest` unless you have already activated the backend virtualenv. Stub `data_service` with `monkeypatch` — never hit live yfinance. Use `tmp_path` for any test that touches SQLite. No frontend test runner; rely on `npm run lint` and `npm run build`. ## Commits & PRs diff --git a/CLAUDE.md b/CLAUDE.md index 39040f7..cbe74f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,15 +10,15 @@ Prism v2: FastAPI backend (yfinance + SQLite watchlist) + Next.js frontend (Plot ```bash # Backend (port 8001 to avoid conflicts with other local services) -backend/.venv/bin/uvicorn app.main:app --reload --host 127.0.0.1 --port 8001 -pytest # all backend tests -pytest backend/tests/test_api.py # single file +backend/.venv/bin/uvicorn app.main:app --reload --host 127.0.0.1 --port 8001 --app-dir backend +backend/.venv/bin/pytest # all backend tests +backend/.venv/bin/pytest backend/tests/test_api.py # single file backend/.venv/bin/python -m py_compile backend/app/main.py backend/app/schemas.py # Frontend (port 3001) -NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8001 npm run dev -- --hostname 127.0.0.1 --port 3001 -npm run lint -npm run build +npm --prefix frontend run lint +npm --prefix frontend run build +NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8001 npm --prefix frontend run dev -- --hostname 127.0.0.1 --port 3001 ``` `./scripts/stack.sh {start|stop|restart|status}` runs both locally; logs in `.run/logs/`. @@ -39,20 +39,14 @@ Key files: ## Production -Topology: uvicorn on `127.0.0.1:8001`, `next start` on `127.0.0.1:3001`, nginx TLS reverse-proxy at `prism.tylerhoang.xyz` (`/api/` → backend, `/` → frontend). Service user `www-data`, code at `/var/www/prism-v2/`. Full deploy instructions in `README.md`. +Topology: uvicorn on `127.0.0.1:8001`, `next start` on `127.0.0.1:3001`, optionally behind a reverse proxy of the user's choosing. Full run instructions live in `README.md`. -**Deploy flow:** bare repo at `/srv/git/prism-v2.git`; pushing master triggers a post-receive hook that `git checkout -f`s into `/var/www/prism-v2/` and runs `scripts/deploy.sh`. `/var/www/prism-v2/` has NO `.git/` — never try to `git pull` there. Manual server-side deploy: `sudo ./scripts/deploy.sh` (`--install` also refreshes systemd units + nginx site). Sample hook lives at `scripts/post-receive.sample`. - -**Logs / status:** -```bash -sudo systemctl status prismv2-backend.service prismv2-frontend.service --no-pager -sudo journalctl -u prismv2-backend.service -f -``` +**Deploy flow:** `scripts/deploy.sh` is only a build/setup helper. It installs backend dependencies, runs `npm ci`, and builds the frontend so the user can start the stack themselves. It does not manage git hooks, install `systemd`, configure `nginx`, or restart services. **Rules:** - Browser code must NOT hardcode a localhost API base. `frontend/lib/api.ts` defaults to empty so production hits same-origin `/api/*` via nginx. - Frontend runs `next start` in production — never `next dev`. -- nginx config and systemd units are in-tree at `nginx/` and `systemd/`; redeploying those requires sudo cp + reload. +- `nginx/` and `systemd/` are in-tree as optional reference templates only. ## Design System (`design-system/`) @@ -81,5 +75,5 @@ v1 cache TTLs to mirror: financials 1 h, indices 5 min, search 60 s. Data hierar `.env.example` → `.env`. Current feature set works without keys. -- `NEXT_PUBLIC_API_BASE_URL` — empty in production (same-origin via nginx); `http://127.0.0.1:8001` locally +- `NEXT_PUBLIC_API_BASE_URL` — empty behind a same-origin proxy; `http://127.0.0.1:8001` locally - `FMP_API_KEY`, `FINNHUB_API_KEY` — reserved for future enrichment diff --git a/README.md b/README.md index cf2eecb..8e35ef2 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,133 @@ # Prism v2 -Financial overview app. FastAPI backend (yfinance + SQLite watchlist) and Next.js frontend (Plotly chart, ticker search, market bar, watchlist). +Financial overview app with a FastAPI backend and a Next.js frontend. ## Stack - Backend: Python 3.14+, FastAPI, uvicorn, yfinance, SQLite -- Frontend: Node 20+, Next.js (App Router), TypeScript -- Production: systemd + nginx (TLS via certbot) +- Frontend: Node 20+, Next.js App Router, TypeScript ## Layout - `backend/app/` — FastAPI app, services, SQLite watchlist +- `backend/tests/` — pytest suite - `frontend/app/` — Next.js App Router; shared types in `frontend/types/api.ts` -- `systemd/` — service units (`prismv2-backend.service`, `prismv2-frontend.service`) -- `nginx/` — TLS server block (`/api/` → :8001, `/` → :3001) -- `scripts/stack.sh` — local dev start/stop/restart/status +- `scripts/deploy.sh` — installs dependencies and builds the app for running +- `scripts/stack.sh` — local dev start/stop/restart/status helper +- `systemd/`, `nginx/` — optional reference templates if you want to wire the app into your own server setup - `design-system/` — brand tokens + Prism UI kit (mirrored to `frontend/public/design-system/`) -## Environment +## Quick Start -Copy `.env.example` → `.env`. Current feature set works without keys. +### 1. Clone the repo -- `NEXT_PUBLIC_API_BASE_URL` — leave empty in production (same-origin `/api/*` via nginx) -- `FMP_API_KEY`, `FINNHUB_API_KEY` — reserved for future enrichment +```bash +git clone +cd prism-v2 +``` -## Local Development +### 2. Optional environment setup ```bash -# Backend -cd backend && python -m venv .venv && .venv/bin/pip install -r requirements.txt -.venv/bin/uvicorn app.main:app --reload --host 127.0.0.1 --port 8001 - -# Frontend -cd frontend && npm install -NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8001 npm run dev -- --hostname 127.0.0.1 --port 3001 +cp .env.example .env ``` -Or use `./scripts/stack.sh {start|stop|restart|status}` (logs in `.run/logs/`). +Current features work without API keys. -## API +- `NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8001` for local frontend-to-backend requests +- `FMP_API_KEY` and `FINNHUB_API_KEY` are optional -- `GET /health` -- `GET /api/search?q=` -- `GET /api/market/indices` -- `GET /api/tickers/{symbol}/overview` -- `GET /api/tickers/{symbol}/history?period=1m|3m|6m|1y|5y` -- `GET|POST|DELETE /api/watchlist[/{symbol}]` (uppercase, capped at 10) +### 3. Run the backend + +```bash +python -m venv backend/.venv +backend/.venv/bin/pip install -r backend/requirements.txt +backend/.venv/bin/uvicorn app.main:app --reload --host 127.0.0.1 --port 8001 --app-dir backend +``` + +Backend health check: -SQLite lives at `backend/data/prism.db`. Backend seeds a `default` profile on startup. +```bash +curl http://127.0.0.1:8001/health +``` -## Production Deployment +### 4. Run the frontend -Target topology: +```bash +cd frontend +npm install +NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8001 npm run dev -- --hostname 127.0.0.1 --port 3001 +``` -- Bare repo at `/srv/git/prism-v2.git` (push target) -- Working tree at `/var/www/prism-v2/` owned by `www-data` (no `.git/` — populated by post-receive hook via `git --work-tree=... checkout -f`) -- Backend on `127.0.0.1:8001`, frontend on `127.0.0.1:3001` -- nginx terminates TLS and reverse-proxies `/api/` → backend, `/` → frontend +Open `http://127.0.0.1:3001`. -Workflow: `git push prod master` on your laptop → post-receive hook checks out the tree into `/var/www/prism-v2/` and runs `scripts/deploy.sh`, which builds + restarts. +### 5. Optional one-command build prep -### First-time setup +If you want the repo to prepare dependencies and build artifacts for you: ```bash -# 1. Create bare repo -sudo mkdir -p /srv/git && sudo git init --bare /srv/git/prism-v2.git +./scripts/deploy.sh +``` -# 2. Create working tree dir -sudo install -d -o www-data -g www-data /var/www/prism-v2 +This script: -# 3. Push from your laptop so the working tree gets populated -# (add the remote on your laptop: git remote add prod user@host:/srv/git/prism-v2.git) -# git push prod master +- creates `backend/.venv` if needed +- installs backend requirements +- runs `npm ci` in `frontend/` +- runs `npm run build` in `frontend/` -# 4. On the server: install systemd units + nginx site + build + start -cd /var/www/prism-v2 -sudo ./scripts/deploy.sh --install +It does not start services, install `systemd`, configure `nginx`, or manage deployment hooks. -# 5. First-time TLS (Certbot edits the nginx server block in-place) -sudo certbot --nginx -d prism.tylerhoang.xyz -sudo systemctl reload nginx +## Running The Stack -# 6. Install the post-receive hook so future pushes auto-deploy -sudo cp /var/www/prism-v2/scripts/post-receive.sample /srv/git/prism-v2.git/hooks/post-receive -sudo chmod +x /srv/git/prism-v2.git/hooks/post-receive +### Development mode -# 7. Sudoers entry so the git-push user can run deploy.sh without a password -# (replace GIT_USER with the account that owns /srv/git or receives the push) -echo 'GIT_USER ALL=(root) NOPASSWD: /var/www/prism-v2/scripts/deploy.sh' | sudo tee /etc/sudoers.d/prism-v2-deploy -sudo chmod 0440 /etc/sudoers.d/prism-v2-deploy -``` +Run these in separate terminals: -### Subsequent deploys +```bash +backend/.venv/bin/uvicorn app.main:app --reload --host 127.0.0.1 --port 8001 --app-dir backend +NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8001 npm --prefix frontend run dev -- --hostname 127.0.0.1 --port 3001 +``` -From your laptop: +Or use: ```bash -git push prod master # post-receive hook checks out + runs deploy.sh +./scripts/stack.sh start ``` -Manual deploy on the server (e.g. retry after a failed build, or to refresh systemd/nginx): +### Production-style local run + +Build first, then run: ```bash -cd /var/www/prism-v2 -sudo ./scripts/deploy.sh # build + restart + smoke check -sudo ./scripts/deploy.sh --install # also refresh systemd units / nginx site +./scripts/deploy.sh +backend/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8001 --app-dir backend +NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8001 npm --prefix frontend run start -- --hostname 127.0.0.1 --port 3001 ``` -`deploy.sh` is idempotent: ensures ownership, builds the backend venv and `npm ci && npm run build` as `www-data` (with `HOME` + `NPM_CONFIG_CACHE` set), restarts both services, and curls `/health` and `/` before exiting. It does NOT touch git — the post-receive hook owns checkout. +If you put Prism behind a reverse proxy, leave `NEXT_PUBLIC_API_BASE_URL` empty so browser requests use same-origin `/api/*`. -### Ops +## API -```bash -sudo systemctl status prismv2-backend.service prismv2-frontend.service --no-pager -sudo journalctl -u prismv2-backend.service -f -sudo journalctl -u prismv2-frontend.service -f +- `GET /health` +- `GET /api/search?q=` +- `GET /api/market/indices` +- `GET /api/tickers/{symbol}/overview` +- `GET /api/tickers/{symbol}/history?period=1m|3m|6m|1y|5y` +- `GET|POST|DELETE /api/watchlist[/{symbol}]` -curl -i http://127.0.0.1:8001/health -curl -i https://prism.tylerhoang.xyz/api/search?q=AAPL -``` +SQLite lives at `backend/data/prism.db`. The backend seeds a `default` profile on startup. ## Verification ```bash -pytest # backend tests (pytest.ini sets pythonpath) -cd frontend && npm run lint && npm run build +backend/.venv/bin/pytest +npm --prefix frontend run lint +npm --prefix frontend run build ``` -## Production Rules +## Notes -- Do not hardcode a localhost API base in frontend browser code. `frontend/lib/api.ts` must default to empty so the browser hits same-origin `/api/*`. +- Browser code should not hardcode a localhost API base. `frontend/lib/api.ts` should default to empty so browser requests can use same-origin `/api/*` when you deploy behind your own proxy. - Never commit `.env`, `node_modules/`, `backend/.venv/`, or `backend/data/prism.db`. -- Watchlist is normalized to uppercase and capped at 10 symbols. +- Watchlist symbols are normalized to uppercase and capped at 10. diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 5eae840..5a2ac5c 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,35 +1,29 @@ #!/usr/bin/env bash # -# Production deploy script for Prism v2. +# Build/setup script for Prism v2. # -# Runs on the production server, AFTER the working tree has been updated -# (typically by a git post-receive hook that does `git checkout -f` into -# /var/www/prism-v2). This script does NOT pull — it only builds + restarts. +# Prepares backend and frontend dependencies and builds the frontend so the +# stack is ready to run. This script does NOT install systemd or nginx, restart +# services, or manage git hooks. # # Usage: -# sudo ./scripts/deploy.sh # build + restart + smoke-check -# sudo ./scripts/deploy.sh --install # also install systemd units + nginx site -# sudo ./scripts/deploy.sh --help +# ./scripts/deploy.sh +# ./scripts/deploy.sh --help set -euo pipefail -APP_DIR="${APP_DIR:-/var/www/prism-v2}" -APP_USER="${APP_USER:-www-data}" -APP_GROUP="${APP_GROUP:-www-data}" -DOMAIN="${DOMAIN:-prism.tylerhoang.xyz}" -BACKEND_SVC="prismv2-backend.service" -FRONTEND_SVC="prismv2-frontend.service" - -DO_INSTALL=0 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="${APP_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}" +PYTHON_BIN="${PYTHON_BIN:-python3}" +BACKEND_VENV="$APP_DIR/backend/.venv" usage() { - sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//' + sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//' exit 0 } for arg in "$@"; do case "$arg" in - --install) DO_INSTALL=1 ;; -h|--help) usage ;; *) echo "Unknown arg: $arg" >&2; exit 2 ;; esac @@ -37,74 +31,31 @@ done log() { printf '\n=== %s ===\n' "$*"; } -if [[ $EUID -ne 0 ]]; then - echo "deploy.sh must be run as root (use sudo)" >&2 - exit 1 -fi - if [[ ! -d "$APP_DIR/backend" || ! -d "$APP_DIR/frontend" ]]; then - echo "Expected working tree at $APP_DIR (backend/ + frontend/ not found)" >&2 + echo "Expected repo root at $APP_DIR (backend/ + frontend/ not found)" >&2 exit 1 fi -cd "$APP_DIR" - -log "Ensuring ownership of $APP_DIR" -chown -R "$APP_USER:$APP_GROUP" "$APP_DIR" -install -d -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/frontend/.npm" -install -d -o "$APP_USER" -g "$APP_GROUP" "$APP_DIR/backend/data" - -run_as_app() { - sudo -u "$APP_USER" \ - env HOME="$APP_DIR/frontend" \ - NPM_CONFIG_CACHE="$APP_DIR/frontend/.npm" \ - "$@" -} - log "Backend: venv + dependencies" -if [[ ! -x "$APP_DIR/backend/.venv/bin/pip" ]]; then - sudo -u "$APP_USER" python3 -m venv "$APP_DIR/backend/.venv" +if [[ ! -x "$BACKEND_VENV/bin/pip" ]]; then + "$PYTHON_BIN" -m venv "$BACKEND_VENV" fi -sudo -u "$APP_USER" "$APP_DIR/backend/.venv/bin/pip" install --quiet --upgrade pip -sudo -u "$APP_USER" "$APP_DIR/backend/.venv/bin/pip" install --quiet -r "$APP_DIR/backend/requirements.txt" +"$BACKEND_VENV/bin/pip" install --quiet --upgrade pip +"$BACKEND_VENV/bin/pip" install --quiet -r "$APP_DIR/backend/requirements.txt" log "Frontend: npm ci + build" -run_as_app npm --prefix "$APP_DIR/frontend" ci -run_as_app npm --prefix "$APP_DIR/frontend" run build +npm --prefix "$APP_DIR/frontend" ci +npm --prefix "$APP_DIR/frontend" run build -if [[ $DO_INSTALL -eq 1 ]]; then - log "Installing systemd units" - cp "$APP_DIR/systemd/$BACKEND_SVC" "/etc/systemd/system/$BACKEND_SVC" - cp "$APP_DIR/systemd/$FRONTEND_SVC" "/etc/systemd/system/$FRONTEND_SVC" - systemctl daemon-reload - systemctl enable "$BACKEND_SVC" "$FRONTEND_SVC" +log "Build complete" +cat </dev/null; then - echo " backend /health OK" -else - echo " backend /health FAILED" >&2 - journalctl -u "$BACKEND_SVC" -n 50 --no-pager >&2 || true - exit 1 -fi -if curl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3001/ | grep -qE '^(200|301|302|307|308)$'; then - echo " frontend / OK" -else - echo " frontend / FAILED" >&2 - journalctl -u "$FRONTEND_SVC" -n 50 --no-pager >&2 || true - exit 1 -fi +Development: + $BACKEND_VENV/bin/uvicorn app.main:app --reload --host 127.0.0.1 --port 8001 --app-dir "$APP_DIR/backend" + NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8001 npm --prefix "$APP_DIR/frontend" run dev -- --hostname 127.0.0.1 --port 3001 -log "Deploy complete" -systemctl status "$BACKEND_SVC" "$FRONTEND_SVC" --no-pager --lines=0 || true +Production-style local run: + $BACKEND_VENV/bin/uvicorn app.main:app --host 127.0.0.1 --port 8001 --app-dir "$APP_DIR/backend" + NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:8001 npm --prefix "$APP_DIR/frontend" run start -- --hostname 127.0.0.1 --port 3001 +EOF diff --git a/scripts/post-receive.sample b/scripts/post-receive.sample deleted file mode 100755 index 3ee898d..0000000 --- a/scripts/post-receive.sample +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -# -# Sample git post-receive hook for /srv/git/prism-v2.git on the VPS. -# -# Install: -# sudo cp /var/www/prism-v2/scripts/post-receive.sample /srv/git/prism-v2.git/hooks/post-receive -# sudo chmod +x /srv/git/prism-v2.git/hooks/post-receive -# -# Required sudoers entry so the git user can restart services without a password. -# Adjust GIT_USER if your git is owned by a different account. -# GIT_USER ALL=(root) NOPASSWD: /var/www/prism-v2/scripts/deploy.sh - -set -euo pipefail - -BRANCH="master" -WEB_DIR="/var/www/prism-v2" -GIT_DIR="/srv/git/prism-v2.git" -LOG="/var/log/git-deploy.log" - -exec >> "$LOG" 2>&1 - -while read -r oldrev newrev refname; do - if [ "$refname" = "refs/heads/$BRANCH" ]; then - echo "$(date -Is): Checking out $BRANCH to $WEB_DIR" - sudo -u www-data git --work-tree="$WEB_DIR" --git-dir="$GIT_DIR" checkout -f "$BRANCH" - - echo "$(date -Is): Running deploy.sh" - sudo "$WEB_DIR/scripts/deploy.sh" - - echo "$(date -Is): Done." - fi -done -- cgit v1.3-2-g0d8e