17 KiB
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
- pnpm monorepo (
apps/client+apps/server) - Multi-stage Dockerfile — client build, server build, minimal Alpine runtime
- Sequential migration runner — aborts startup on failure
- Gitea Actions CI — builds and pushes Docker image on push to
main - Unraid install guide (GUI template + CLI script)
- PUID/PGID support via
docker-entrypoint.sh tinifor correct PID 1 signal handling
Design System
- CSS custom property token system (surface, border, text, accent)
- Light / dark mode with smooth transitions
- 5 accent colours (Indigo, Teal, Rose, Amber, Slate)
- Collapsible sidebar with animated labels
- Mobile overlay drawer
- Primitive component library:
Button,Input,Textarea,Select,Modal,Avatar,Badge,ThemeToggle - Page entry/exit animations via Framer Motion
Members
- Create, edit, delete family members
- Custom display colour per member
- Avatar field
- Member assignment used throughout app (events, chores, shopping items)
Settings
- Theme mode + accent colour (persisted to DB and applied via CSS tokens)
- Photo folder path (absolute path or Docker bind-mount override)
- Slideshow speed (3 s – 15 s)
- Slideshow order (random / sequential / newest first)
- Idle timeout (1 min – 10 min, or disabled)
- Time format (12h / 24h)
- Date format (MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD)
- Weather API key, location, units (stored — widget pending)
Calendar
- Month grid view with event dots
- Per-day event list modal
- Create / edit / delete events
- All-day events
- Member colour coding
- Custom event colour override
- Recurrence field (stored, display only)
- Month navigation with slide animation
Shopping
- Multiple named lists
- Add / edit / delete items with optional quantity
- Check / uncheck items (checked items grouped separately)
- Clear all checked items
- Member assignment per item
- Keyboard shortcut: Enter to add
- Create / delete lists
Chores
- Create / edit / delete chores
- Title, description, due date, recurrence label, member assignment
- Status: pending / done
- Filter by status or by member
- Completion count badge
- Completion history recorded in
chore_completions
Photos
- Batch upload via browser (drag & drop anywhere on page, or file picker)
- Multi-file selection — up to 200 files per upload, 50 MB each
- Server saves to configured photo folder root (multer disk storage)
- Filenames sanitised + timestamp-suffixed to prevent collisions
- Responsive photo grid (2 – 6 columns)
- Hover overlay: filename label + delete button
- Click to open full-screen lightbox
- Lightbox: prev / next navigation, keyboard arrows, Esc to close, photo counter
- Permanent delete with confirmation modal
- Graceful "not configured" state with link to Settings
- Empty state with prominent upload drop zone
- Recursive folder scan (pre-existing subdirectory photos appear in slideshow)
- Path traversal protection on all file-serving and delete endpoints
Screensaver
- Activates automatically after configurable idle timeout
- Idle detection:
mousemove,mousedown,keydown,touchstart,scroll - Ken Burns effect — 8 presets alternating zoom-in / zoom-out with diagonal, vertical, and horizontal pan
- Crossfade between photos (1.2 s,
AnimatePresence mode="sync") - Respects
slideshow_ordersetting (random shuffled each cycle, sequential, newest first) - Respects
slideshow_speedsetting (3 s – 15 s) - Next photo preloaded while current is displayed
- Live clock overlay — large thin numerals, responsive font size (
clamp) - Clock respects
time_formatsetting (12h with AM/PM, 24h) - Date line below clock
- "Tap to dismiss" hint fades out after 3.5 s
- Dismiss on click, touch, or any keypress
- No-photo fallback: dark gradient + clock only
- Gradient scrim over photos for clock legibility
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_urlis 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
pendingon 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.dband 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
# 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
# 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.