Files
family-planner/PROJECT.md
jason ba2a76f7dd
All checks were successful
Build and Push Docker Image / build (push) Successful in 7s
ROJECT.md
2026-03-30 13:55:58 -05:00

391 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Family Planner — Project Reference
A self-hosted family dashboard designed for always-on display (wall tablet, TV, Unraid server). Manages calendars, chores, shopping, meals, a message board, countdowns, and a photo screensaver — all in one Docker container with zero external services required.
---
## Tech Stack
| Layer | Technology |
|---|---|
| **Runtime** | Node.js 22, Docker (Alpine) |
| **Backend** | Express 4, TypeScript 5 |
| **Database** | Node.js built-in SQLite (`node:sqlite`), WAL mode |
| **File uploads** | Multer |
| **Frontend** | React 18, TypeScript 5, Vite 5 |
| **Styling** | Tailwind CSS 3, CSS custom properties (theme tokens) |
| **Animation** | Framer Motion 11 |
| **State / data** | TanStack Query 5, Zustand 4 |
| **Routing** | React Router 6 |
| **Icons** | Lucide React |
| **Date utils** | date-fns 3 |
| **Package manager** | pnpm (workspaces monorepo) |
| **CI/CD** | Gitea Actions → Docker build & push to private registry |
| **Deployment target** | Unraid (Community Applications / CLI install) |
---
## Repository Structure
```
family-planner/
├── apps/
│ ├── client/ # React frontend
│ │ └── src/
│ │ ├── components/
│ │ │ ├── layout/
│ │ │ │ └── AppShell.tsx # Sidebar, mobile drawer, page wrapper
│ │ │ ├── screensaver/
│ │ │ │ └── Screensaver.tsx # Idle screensaver w/ Ken Burns slideshow
│ │ │ └── ui/ # Design-system primitives
│ │ │ ├── Avatar.tsx
│ │ │ ├── Badge.tsx
│ │ │ ├── Button.tsx
│ │ │ ├── Input.tsx
│ │ │ ├── Modal.tsx
│ │ │ ├── Select.tsx
│ │ │ ├── Textarea.tsx
│ │ │ └── ThemeToggle.tsx
│ │ ├── features/ # Feature-scoped sub-components
│ │ │ ├── calendar/
│ │ │ ├── chores/
│ │ │ └── shopping/
│ │ ├── hooks/
│ │ │ └── useMembers.ts
│ │ ├── lib/
│ │ │ └── api.ts # Axios instance + all API types
│ │ ├── pages/ # One file per route
│ │ │ ├── Calendar.tsx ✅ complete
│ │ │ ├── Chores.tsx ✅ complete
│ │ │ ├── Photos.tsx ✅ complete
│ │ │ ├── Settings.tsx ✅ complete
│ │ │ ├── Members.tsx ✅ complete
│ │ │ ├── Shopping.tsx ✅ complete
│ │ │ ├── Dashboard.tsx 🔲 stub
│ │ │ ├── Meals.tsx 🔲 stub
│ │ │ ├── Board.tsx 🔲 stub
│ │ │ └── Countdowns.tsx 🔲 stub
│ │ └── store/
│ │ ├── settingsStore.ts # Zustand settings (synced from DB)
│ │ └── themeStore.ts # Zustand theme + CSS token application
│ └── server/
│ └── src/
│ ├── db/
│ │ ├── db.ts # node:sqlite wrapper + transaction helper
│ │ ├── runner.ts # Sequential migration runner
│ │ └── migrations/
│ │ └── 001_initial.ts # Full schema + seed data
│ ├── routes/ # One router file per domain
│ │ ├── members.ts ✅ full CRUD
│ │ ├── settings.ts ✅ key/value GET + PATCH
│ │ ├── events.ts ✅ full CRUD + date-range filter
│ │ ├── shopping.ts ✅ full CRUD (lists + items)
│ │ ├── chores.ts ✅ full CRUD + completions
│ │ ├── meals.ts ✅ full CRUD (upsert by date)
│ │ ├── messages.ts ✅ full CRUD + pin/expiry
│ │ ├── countdowns.ts ✅ full CRUD + event link
│ │ └── photos.ts ✅ list, upload, serve, delete, slideshow
│ └── index.ts # Express app + static client serve
├── .gitea/workflows/
│ └── docker-build.yml # Push to main → build + push Docker image
├── Dockerfile # Multi-stage: client build → server build → runtime
├── docker-compose.yml
├── docker-entrypoint.sh # PUID/PGID ownership fix + app start
├── UNRAID.md # Full Unraid install guide (GUI + CLI)
├── INSTALL.md
└── PROJECT.md # ← this file
```
---
## Database Schema
All tables live in a single SQLite file at `$DATA_DIR/family.db`.
| Table | Purpose |
|---|---|
| `members` | Family member profiles (name, color, avatar) |
| `settings` | Key/value app configuration store |
| `events` | Calendar events (all-day, timed, recurrence field) |
| `shopping_lists` | Named shopping lists |
| `shopping_items` | Line items (quantity, checked state, member assignment) |
| `chores` | Chore definitions (recurrence, due date, status) |
| `chore_completions` | Completion history per chore per member |
| `meals` | One dinner entry per calendar date (upsert pattern) |
| `messages` | Board messages (color, emoji, pin, optional expiry) |
| `countdowns` | Countdown timers (linked to calendar event or standalone) |
Photos are stored as files on disk — no database table. The configured folder path lives in `settings.photo_folder` (or `$PHOTOS_DIR` env var in Docker).
---
## Completed Features
### Infrastructure
- [x] pnpm monorepo (`apps/client` + `apps/server`)
- [x] Multi-stage Dockerfile — client build, server build, minimal Alpine runtime
- [x] Sequential migration runner — aborts startup on failure
- [x] Gitea Actions CI — builds and pushes Docker image on push to `main`
- [x] Unraid install guide (GUI template + CLI script)
- [x] PUID/PGID support via `docker-entrypoint.sh`
- [x] `tini` for correct PID 1 signal handling
### Design System
- [x] CSS custom property token system (surface, border, text, accent)
- [x] Light / dark mode with smooth transitions
- [x] 5 accent colours (Indigo, Teal, Rose, Amber, Slate)
- [x] Collapsible sidebar with animated labels
- [x] Mobile overlay drawer
- [x] Primitive component library: `Button`, `Input`, `Textarea`, `Select`, `Modal`, `Avatar`, `Badge`, `ThemeToggle`
- [x] Page entry/exit animations via Framer Motion
### Members
- [x] Create, edit, delete family members
- [x] Custom display colour per member
- [x] Avatar field
- [x] Member assignment used throughout app (events, chores, shopping items)
### Settings
- [x] Theme mode + accent colour (persisted to DB and applied via CSS tokens)
- [x] Photo folder path (absolute path or Docker bind-mount override)
- [x] Slideshow speed (3 s 15 s)
- [x] Slideshow order (random / sequential / newest first)
- [x] Idle timeout (1 min 10 min, or disabled)
- [x] Time format (12h / 24h)
- [x] Date format (MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD)
- [x] Weather API key, location, units (stored — widget pending)
### Calendar
- [x] Month grid view with event dots
- [x] Per-day event list modal
- [x] Create / edit / delete events
- [x] All-day events
- [x] Member colour coding
- [x] Custom event colour override
- [x] Recurrence field (stored, display only)
- [x] Month navigation with slide animation
### Shopping
- [x] Multiple named lists
- [x] Add / edit / delete items with optional quantity
- [x] Check / uncheck items (checked items grouped separately)
- [x] Clear all checked items
- [x] Member assignment per item
- [x] Keyboard shortcut: Enter to add
- [x] Create / delete lists
### Chores
- [x] Create / edit / delete chores
- [x] Title, description, due date, recurrence label, member assignment
- [x] Status: pending / done
- [x] Filter by status or by member
- [x] Completion count badge
- [x] Completion history recorded in `chore_completions`
### Photos
- [x] Batch upload via browser (drag & drop anywhere on page, or file picker)
- [x] Multi-file selection — up to 200 files per upload, 50 MB each
- [x] Server saves to configured photo folder root (multer disk storage)
- [x] Filenames sanitised + timestamp-suffixed to prevent collisions
- [x] Responsive photo grid (2 6 columns)
- [x] Hover overlay: filename label + delete button
- [x] Click to open full-screen lightbox
- [x] Lightbox: prev / next navigation, keyboard arrows, Esc to close, photo counter
- [x] Permanent delete with confirmation modal
- [x] Graceful "not configured" state with link to Settings
- [x] Empty state with prominent upload drop zone
- [x] Recursive folder scan (pre-existing subdirectory photos appear in slideshow)
- [x] Path traversal protection on all file-serving and delete endpoints
### Screensaver
- [x] Activates automatically after configurable idle timeout
- [x] Idle detection: `mousemove`, `mousedown`, `keydown`, `touchstart`, `scroll`
- [x] Ken Burns effect — 8 presets alternating zoom-in / zoom-out with diagonal, vertical, and horizontal pan
- [x] Crossfade between photos (1.2 s, `AnimatePresence mode="sync"`)
- [x] Respects `slideshow_order` setting (random shuffled each cycle, sequential, newest first)
- [x] Respects `slideshow_speed` setting (3 s 15 s)
- [x] Next photo preloaded while current is displayed
- [x] Live clock overlay — large thin numerals, responsive font size (`clamp`)
- [x] Clock respects `time_format` setting (12h with AM/PM, 24h)
- [x] Date line below clock
- [x] "Tap to dismiss" hint fades out after 3.5 s
- [x] Dismiss on click, touch, or any keypress
- [x] No-photo fallback: dark gradient + clock only
- [x] Gradient scrim over photos for clock legibility
- [x] `z-[200]` — renders above all modals and UI
---
## In Progress / Stubs
These pages have **complete backend APIs** but their frontend is a placeholder `<div>`.
### Meals `🔲`
**Backend:** `GET /api/meals`, `PUT /api/meals/:date` (upsert), `DELETE /api/meals/:date`
**Planned UI:**
- Weekly grid (Mon Sun) showing the current week's dinner plan
- Click any day to open a quick-edit modal (title, description, recipe URL)
- Week navigation (prev / next)
- "No meal planned" empty state per day
- Recipe link button when `recipe_url` is set
### Message Board `🔲`
**Backend:** `GET /api/messages`, `POST`, `PATCH /:id`, `DELETE /:id`
**Planned UI:**
- Sticky-note card grid
- Custom card background colour + emoji
- Pin important messages to the top
- Optional expiry (auto-removed after date)
- Member attribution
- Quick compose at the bottom
### Countdowns `🔲`
**Backend:** `GET /api/countdowns`, `POST`, `PUT /:id`, `DELETE /:id`
**Planned UI:**
- Card grid showing days remaining to each target date
- Large number + label per card
- Custom colour and emoji per countdown
- Optional link to a calendar event
- Flag to show on Dashboard
- Completed countdowns hidden (target date in the past is filtered server-side)
### Dashboard `🔲`
**Planned UI (depends on all modules above being complete):**
- Today's date + time (respects `time_format` / `date_format`)
- Weather widget (OpenWeatherMap — API key + location already in Settings)
- Upcoming calendar events (next 35)
- Today's meal plan
- Active chore count / completion summary
- Shopping list item count
- Pinned messages preview
- Countdowns flagged `show_on_dashboard`
- Quick-action buttons to each module
---
## Roadmap
### Near-term (next sessions)
- [ ] **Meals page** — weekly dinner grid with modal editor
- [ ] **Message Board page** — sticky-note UI with compose, pin, expiry
- [ ] **Countdowns page** — day-count cards with create/edit/delete
- [ ] **Dashboard** — wire up all modules once the above are complete
- [ ] **Weather widget** — OpenWeatherMap fetch on the Dashboard using stored credentials
### Medium-term
- [ ] **Recurring chore automation** — auto-reset status to `pending` on schedule instead of just storing the recurrence label
- [ ] **Calendar recurrence expansion** — expand recurring events into visible instances on the grid
- [ ] **Meal recipe import** — paste a URL, scrape title from `<title>` or Open Graph
- [ ] **Shopping list reorder** — drag-and-drop reorder items (sort_order column already in schema)
- [ ] **Member avatar upload** — upload image instead of text initials
- [ ] **Screensaver burn-in mitigation** — slowly drift clock position across OLED panels
### Future / Nice-to-have
- [ ] **PWA manifest + service worker** — installable on tablet home screen, offline cache for static assets
- [ ] **Push notifications** — chore reminders, upcoming events (requires service worker)
- [ ] **Multi-user auth** — PIN-per-member or password gate (currently open LAN access only)
- [ ] **Backup / restore UI** — download `family.db` and restore from file in the app
- [ ] **Community Applications XML template** — publish official Unraid CA template
- [ ] **Dark mode auto-schedule** — switch theme at sunrise/sunset based on location
- [ ] **Grocery store integration** — import a shared list from a URL or barcode scan
---
## API Reference
All endpoints are under `/api`. The server also serves the built React client at `/` (catch-all `index.html`).
| Method | Path | Description |
|---|---|---|
| GET | `/api/members` | List all members |
| POST | `/api/members` | Create member |
| PUT | `/api/members/:id` | Update member |
| DELETE | `/api/members/:id` | Delete member |
| GET | `/api/settings` | Get all settings as flat object |
| PATCH | `/api/settings` | Update one or more settings keys |
| GET | `/api/events` | List events (optional `?start=&end=` ISO range) |
| POST | `/api/events` | Create event |
| PUT | `/api/events/:id` | Update event |
| DELETE | `/api/events/:id` | Delete event |
| GET | `/api/shopping/lists` | List all shopping lists |
| POST | `/api/shopping/lists` | Create list |
| DELETE | `/api/shopping/lists/:id` | Delete list + cascade items |
| GET | `/api/shopping/lists/:id/items` | List items for a list |
| POST | `/api/shopping/lists/:id/items` | Add item |
| PATCH | `/api/shopping/lists/:id/items/:itemId` | Update item (check, rename, etc.) |
| DELETE | `/api/shopping/lists/:id/items/:itemId` | Delete item |
| DELETE | `/api/shopping/lists/:id/items/checked` | Clear all checked items |
| GET | `/api/chores` | List chores with member info + completion count |
| POST | `/api/chores` | Create chore |
| PUT | `/api/chores/:id` | Update chore |
| PATCH | `/api/chores/:id/complete` | Record a completion |
| DELETE | `/api/chores/:id` | Delete chore |
| GET | `/api/meals` | List meals (optional `?start=&end=` date range) |
| PUT | `/api/meals/:date` | Upsert meal for a date (YYYY-MM-DD) |
| DELETE | `/api/meals/:date` | Remove meal for a date |
| GET | `/api/messages` | List non-expired messages (pinned first) |
| POST | `/api/messages` | Create message |
| PATCH | `/api/messages/:id` | Update message |
| DELETE | `/api/messages/:id` | Delete message |
| GET | `/api/countdowns` | List future countdowns (ordered by date) |
| POST | `/api/countdowns` | Create countdown |
| PUT | `/api/countdowns/:id` | Update countdown |
| DELETE | `/api/countdowns/:id` | Delete countdown |
| GET | `/api/photos` | List all photos recursively `{ configured, count, photos }` |
| GET | `/api/photos/slideshow` | Same list, optimised for screensaver use |
| GET | `/api/photos/file/*` | Serve a photo by relative path (path traversal protected) |
| POST | `/api/photos/upload` | Upload photos (`multipart/form-data`, field `photos`) |
| DELETE | `/api/photos/file/*` | Delete a photo by relative path |
---
## Environment Variables
| Variable | Default | Description |
|---|---|---|
| `PORT` | `3001` | HTTP port the server listens on |
| `DATA_DIR` | `../../data` (dev) / `/data` (Docker) | SQLite database directory |
| `PHOTOS_DIR` | *(unset)* | Override photo folder path (ignores DB setting when set) |
| `CLIENT_ORIGIN` | `http://localhost:5173` | CORS allowed origin (dev only) |
| `PUID` | `99` | User ID for file ownership in container |
| `PGID` | `100` | Group ID for file ownership in container |
| `TZ` | `UTC` | Container timezone |
| `NODE_NO_WARNINGS` | `1` | Suppresses experimental SQLite API warning |
---
## Local Development
```bash
# Install deps
pnpm install
# Start both client (port 5173) and server (port 3001) with hot reload
pnpm dev
# Type-check client
pnpm --filter client exec tsc --noEmit
# Build for production
pnpm build
```
The Vite dev server proxies `/api/*` to `localhost:3001`, so you can develop against the live server without CORS issues.
---
## Docker Build
```bash
# Build image locally
docker build -t family-planner .
# Run locally
docker run -p 3001:3001 \
-v $(pwd)/data:/data \
-v /path/to/photos:/photos \
family-planner
```
The Gitea Actions workflow builds and pushes automatically on every push to `main`.