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

17 KiB
Raw Blame History

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
  • tini for 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_order setting (random shuffled each cycle, sequential, newest first)
  • Respects slideshow_speed setting (3 s 15 s)
  • Next photo preloaded while current is displayed
  • Live clock overlay — large thin numerals, responsive font size (clamp)
  • Clock respects time_format setting (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_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

# 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.