This commit is contained in:
390
PROJECT.md
Normal file
390
PROJECT.md
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
# 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 3–5)
|
||||||
|
- 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`.
|
||||||
Reference in New Issue
Block a user