# Life Dashboard Private personal dashboard at `dash.option.design` for tracking weight/body metrics, workouts, and finances with visual trend charts. Single-user app with password auth. ## Tech Stack - **Backend**: Python 3.12 / FastAPI / SQLAlchemy / Alembic - **Frontend**: React 18 / TypeScript / Vite / Tailwind CSS / Recharts - **UI Components**: shadcn/ui style (Radix primitives + Tailwind) - **Auth**: JWT tokens, single-user credentials via env vars - **DB**: PostgreSQL 16 - **Infra**: Docker (multi-stage build), Caddy reverse proxy ## Project Structure ``` life/ ├── backend/ │ ├── app/ │ │ ├── main.py # FastAPI entry — API routes + SPA serving │ │ ├── config.py # Settings from env vars │ │ ├── auth.py # JWT creation + validation │ │ ├── database.py # SQLAlchemy engine/session │ │ ├── models/ │ │ │ ├── weight.py # WeightEntry model │ │ │ ├── workout.py # Exercise, Workout, WorkoutSet models │ │ │ └── finance.py # FinanceTransaction, FinanceSnapshot models │ │ └── routers/ │ │ ├── auth.py # Login + token validation │ │ ├── weight.py # Weight CRUD │ │ ├── workout.py # Workout + exercise CRUD │ │ └── finance.py # Transaction + snapshot CRUD + summary │ ├── alembic/ │ │ └── versions/ # Database migrations │ ├── alembic.ini │ └── requirements.txt ├── frontend/ │ ├── src/ │ │ ├── App.tsx # Router + protected routes │ │ ├── main.tsx # React entry │ │ ├── api/ │ │ │ ├── client.ts # Fetch wrapper with JWT auth │ │ │ └── types.ts # TypeScript interfaces │ │ ├── components/ │ │ │ ├── layout/Shell.tsx # Sidebar + main content shell │ │ │ └── ui/ # Button, Input, Card, Label │ │ └── pages/ │ │ ├── Login.tsx │ │ ├── Dashboard.tsx # Overview with summary cards + sparklines │ │ ├── Weight.tsx # Weight tracker with line chart │ │ ├── Workouts.tsx # Workout logger with bar chart │ │ └── Finances.tsx # Transactions + net worth charts │ ├── package.json │ ├── tailwind.config.js │ └── vite.config.ts ├── Dockerfile # Multi-stage: Node build → Python runtime ├── docker-compose.yml # Standalone compose (references external network) └── .gitignore ``` ## Local Development **Backend:** ```bash cd backend pip install -r requirements.txt # Set DATABASE_URL, AUTH_USERNAME, AUTH_PASSWORD, JWT_SECRET as env vars alembic upgrade head uvicorn app.main:app --reload ``` **Frontend:** ```bash cd frontend npm install npm run dev # Vite proxies /api requests to localhost:8000 ``` ## Deployment The app runs as a Docker container on the VPS alongside Caddy, Postgres, and Gitea — all sharing a `web` Docker network. **Push changes:** ```bash # Local machine git add . git commit -m "Description of changes" git push origin main ``` **Deploy on VPS:** ```bash cd /opt/will-dash/life git pull cd /opt/server docker compose up --build -d life ``` The container automatically runs `alembic upgrade head` on startup, so new migrations are applied before the server starts. **Check logs:** ```bash docker compose logs -f life ``` ## Environment Variables | Variable | Description | Example | |----------|-------------|---------| | `DATABASE_URL` | Postgres connection string | `postgresql://admin:pass@postgres:5432/life` | | `AUTH_USERNAME` | Login username | `will` | | `AUTH_PASSWORD` | Login password | (set in .env) | | `JWT_SECRET` | Secret for signing tokens | (generate with `openssl rand -hex 32`) | ## How to Add a New Module Adding a new tracking module follows a consistent 9-file pattern. Here's a walkthrough using a hypothetical "Clothing" module that tracks what you wore each day (shirt, shorts, shoes). ### Backend — 2 new files, 2 edits **1. Create the model** — `backend/app/models/clothing.py` Define a SQLAlchemy model with your table schema: ```python from datetime import date, datetime from sqlalchemy import Date, Text, func from sqlalchemy.orm import Mapped, mapped_column from app.database import Base class ClothingEntry(Base): __tablename__ = "clothing_entries" id: Mapped[int] = mapped_column(primary_key=True) date: Mapped[date] = mapped_column(Date, unique=True) shirt: Mapped[str] = mapped_column(Text) shorts: Mapped[str] = mapped_column(Text) shoes: Mapped[str] = mapped_column(Text) notes: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column(server_default=func.now()) ``` **2. Create the router** — `backend/app/routers/clothing.py` Define Pydantic schemas and CRUD endpoints. Copy `routers/weight.py` as a starting point — it's the simplest existing module (flat table, basic CRUD). Key pieces: - `ClothingCreate` (Pydantic model for input) - `ClothingRead` (extends Create, adds `id`, sets `from_attributes = True`) - GET (list with optional date filters), POST, PUT, DELETE endpoints - Router uses `dependencies=[Depends(get_current_user)]` for auth **3. Register the model** — edit `backend/app/models/__init__.py` ```python from app.models.clothing import ClothingEntry # Add to __all__ list ``` **4. Register the router** — edit `backend/app/main.py` ```python from app.routers import clothing app.include_router(clothing.router) ``` ### Database — 1 new migration **5. Create migration** — `backend/alembic/versions/002_add_clothing.py` ```python revision = "002" down_revision = "001" def upgrade(): op.create_table("clothing_entries", ...) def downgrade(): op.drop_table("clothing_entries") ``` ### Frontend — 1 new page, 3 edits **6. Add TypeScript types** — edit `frontend/src/api/types.ts` ```typescript export interface ClothingEntry { id: number; date: string; shirt: string; shorts: string; shoes: string; notes: string | null; } ``` **7. Create the page** — `frontend/src/pages/Clothing.tsx` Build a React component with a form, history table, and optional chart. Copy `pages/Weight.tsx` as a template and modify the fields. **8. Add the route** — edit `frontend/src/App.tsx` ```tsx import Clothing from "@/pages/Clothing"; // Add inside : } /> ``` **9. Add sidebar link** — edit `frontend/src/components/layout/Shell.tsx` ```tsx import { Shirt } from "lucide-react"; // Add to navItems array: { to: "/clothing", label: "Clothing", icon: Shirt } ``` ### Deploy ```bash git add . && git commit -m "Add clothing module" && git push origin main # VPS: git pull, then docker compose up --build -d life ``` The new migration runs automatically on container startup.