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 modelbackend/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 routerbackend/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

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 migrationbackend/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 pagefrontend/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.

Description
dashboard for keeping track of life metrics like workouts, weight, health etc...
Readme 108 KiB
Languages
TypeScript 62.6%
Python 32.4%
JavaScript 1.6%
CSS 1.5%
Dockerfile 0.8%
Other 1.1%