diff --git a/PROJECT.md b/PROJECT.md new file mode 100644 index 0000000..2e941ec --- /dev/null +++ b/PROJECT.md @@ -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 `
`. + +### 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 `` 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`.