227 lines
7.1 KiB
Markdown
227 lines
7.1 KiB
Markdown
# 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 <Routes>:
|
|
<Route path="/clothing" element={<ProtectedRoute><Clothing /></ProtectedRoute>} />
|
|
```
|
|
|
|
**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.
|