Files
life/README.md

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.