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:
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:
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:
# Local machine
git add .
git commit -m "Description of changes"
git push origin main
Deploy on VPS:
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:
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:
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, addsid, setsfrom_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
from app.models.clothing import ClothingEntry
# Add to __all__ list
4. Register the router — edit backend/app/main.py
from app.routers import clothing
app.include_router(clothing.router)
Database — 1 new migration
5. Create migration — backend/alembic/versions/002_add_clothing.py
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
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
import Clothing from "@/pages/Clothing";
// Add inside <Routes>:
<Route path="/clothing" element={<ProtectedRoute><Clothing /></ProtectedRoute>} />
9. Add sidebar link — edit frontend/src/components/layout/Shell.tsx
import { Shirt } from "lucide-react";
// Add to navItems array:
{ to: "/clothing", label: "Clothing", icon: Shirt }
Deploy
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.