- TypeScript 88.9%
- CSS 10.5%
- Dockerfile 0.3%
- HTML 0.3%
| backend | ||
| config | ||
| docs | ||
| frontend | ||
| photos | ||
| shared | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .prettierignore | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| VALIDATION.md | ||
| vite.config.ts | ||
At home — family dashboard v0.1
A self-hosted portrait wall dashboard with independent clock, photo, weather, alert and monthly calendar widgets. React + TypeScript + Vite frontend; Node + TypeScript + Express backend. Weather and samples require no account or API key. Calendars can display samples or connect to personal Outlook through your own Microsoft app registration. Weather now comes from Open-Meteo and U.S. alerts from the National Weather Service; these require internet access from the Docker host. Photos are real local files.
Start with Docker
Requires Docker Engine with the Compose plugin (or Docker Desktop using Linux containers).
- Open this project on your Docker host.
- Put JPG/JPEG, PNG or WebP images directly in
photos/. The empty folder is included; without photos, a useful empty state appears. - Optionally copy
.env.exampleto.envand change the host port or mounted paths. - From the project directory run:
docker compose up -d --build
Open http://localhost:3000 on the Docker host, or http://:3000 from another computer on the LAN. Use the host's actual IP, not localhost, on another computer. Allow inbound TCP 3000 on the host's private network firewall if necessary. No Raspberry Pi is needed.
In Chromium developer tools enable the device toolbar, choose Responsive, set 1080 × 1920 and use a fit-to-window preview scale. The scale only changes the desktop preview; the dashboard itself renders at the configured resolution. A normal landscape desktop browser can scroll through the portrait canvas. Small screens preserve a minimum vertical grid height for legibility instead of squeezing the full calendar into one short viewport.
docker compose ps
docker compose logs -f dashboard
docker compose down
The service binds to 0.0.0.0:3000, runs as the unprivileged Node user and mounts photos read-only and the configuration directory read-write. It has a health check and restarts unless explicitly stopped. There is intentionally no dashboard authentication: keep this service on your trusted LAN.
Photos
Default host folder: ./photos, mounted at /photos inside the container. For a NAS directory already mounted on the Docker host, set:
PHOTO_DIRECTORY=/mnt/nas/family-photos
Windows example: PHOTO_DIRECTORY=C:/Pictures/Dashboard. Create the directory first. The container user needs read access to images and traverse access to the directory. This version reads the folder's immediate children; it does not recurse or follow image symlinks. Images are served by the backend, not copied into the frontend build. Names with spaces and punctuation are encoded safely. New/removed files are discovered every minute. Changed files get a modification-time URL; removed files are skipped after discovery. A failed image leaves the last successful photo visible while the slideshow tries the next entry.
In config/dashboard.json, photo controls:
| Setting | Values / behavior |
|---|---|
directory |
Local development photo path. Docker's PHOTO_DIR=/photos overrides this. |
intervalSeconds |
Seconds between slides, minimum 2; default 30. |
shuffle |
Random next photo without immediately repeating, or alphabetical order. |
mode |
cover crops to fill; contain shows the whole photo; contain-blur adds a blurred enlarged background behind a sharp full photo. |
transition |
crossfade blends photos; fade fades the old image out while the next fades in. |
transitionSeconds |
0–3 seconds, shorter than the slide interval. Reduced-motion preferences disable animation. |
Configuration
Open http://:3002/config on the deployed Pop!_OS host (use port 3000 for a default fresh installation). Forms cover clock/display, photos, calendar colors, weather locations and rotation, alerts, and widget coordinates with a layout preview. Save changes applies settings to open dashboards within five seconds. Reset edits restores the last loaded/saved version; it does not restore factory defaults. Invalid settings are rejected and conflicting saves return a reload prompt instead of overwriting another editor. Leaving with unsaved changes triggers the browser's normal warning.
Settings persist in config/dashboard.json. Each save keeps config/dashboard.json.previous and atomically replaces the active file. The entire config directory is mounted writable; the container user (UID 1000 by default) must be able to write it. Photos stay read-only. The config page shows the connected photo folder but does not change Docker mounts. No login is added: anyone who can reach this LAN service can edit its settings. Cross-origin browser writes are rejected.
.env is read by Docker Compose. It controls host PORT, PHOTO_DIRECTORY and CONFIG_DIRECTORY. Changing these requires recreating the container. When running Node directly, use shell environment variables PORT, PHOTO_DIR and CONFIG_PATH; .env is not automatically loaded by Node.
| Section | Purpose |
|---|---|
display |
IANA timezone (default America/New_York), locale, target dimensions and grid dimensions. |
clock |
format: 12 or 24; showSeconds; dateFormat: short, medium, long or full (Intl date styles). |
layout |
Independent widgets with unique id, type, zero-based x,y, and spans w,h. |
calendars |
Source IDs, display names and six-digit hex colors. |
calendar |
First weekday (0 is Sunday) and maximum displayed events per day. Available height can reduce this cap. |
weatherLocations |
IDs, names, coordinates and individual IANA timezones. Home is St. Petersburg ZIP 33709 (approximate ZIP centroid 27.81721, -82.73075). Stuttgart remains configured. |
weather |
unit: fahrenheit or celsius; automatic rotation interval; ordered { locationId, view } sequence. Views: current, hourly, 5-day, 7-day. |
alerts |
mockEnabled: set true to see a demonstration warning. emptyBehavior: collapse or hidden. |
The default grid is 12 columns × 40 rows. Clock/photos occupy rows 0–8; weather rows 9–14; alerts rows 15–16; calendar rows 17–39. With no alerts, the full-width alert band collapses and the calendar expands into it: roughly 37% top / 63% calendar. hidden leaves the allocated space empty. Only an isolated full-width alert band can collapse; custom overlapping or partial-width placements remain reserved. Avoid excessively tiny widget allocations; weather uses internal scrolling when needed. Overlap is allowed for future overlays; later entries paint over earlier ones.
Example widget entry:
{ "id": "calendar-main", "type": "calendar", "x": 0, "y": 17, "w": 12, "h": 23 }
The configuration page edits coordinates in forms; no drag/resize editor exists yet. A future editor only needs to update these coordinates. Widget internals use container sizing and a ResizeObserver, not absolute dashboard coordinates.
Calendar and weather behavior
FullCalendar renders the monthly grid, multi-day bars and +N more popovers. Date/time rendering uses the configured zone through Luxon, including daylight-saving changes. All-day event ends are exclusive. Events are ordered by priority, then start/duration/title, and capped to preserve readable text. Month arrows and Today are useful on desktop; the display returns to the current month at the next local day boundary. The clock ticks independently every second.
Calendars are separate sources; events reference calendarId. Sample data is generated around each requested month, so it remains useful after today. Weekly activities look recurring but do not use a recurrence engine. Existing sample activities reference family, rolf, sunshine, and school; adding another calendar ID creates its legend entry but needs corresponding sample events in backend/src/services.ts. Names and colors can be changed without changing events.
Weather data uses Celsius and km/h internally and is converted at render time. Each location has current conditions, ten hourly periods and seven daily periods; the five-day view takes the first five. Forecast times use the location's timezone. Buttons can pause rotation, advance or select a specific view. SVG weather icons are local React components in frontend/src/widgets/WeatherIcon.tsx; conditionFromCode is the separate provider-code adapter. Set weather.provider to open-meteo (default) for live forecasts or mock for sample data. Set alerts.provider to nws (default) for official U.S. alerts, with alerts.locationIds selecting U.S. locations (default ["home"]). Stuttgart uses live forecasts but is not queried through the U.S.-only NWS alert feed. mockEnabled applies only when the alert provider is mock. Active watches appear as soon as issued, even when hazard onset is in the future; canceled, expired and test messages are excluded. Multiple alerts rotate every 12 seconds, beginning with warnings. No active alerts means the banner collapses; failed alert requests show an unavailable message instead. Forecast requests are cached for five minutes, alerts for one minute, and concurrent requests are shared. On forecast-provider failure the backend can serve explicitly marked cached data for up to one hour; the browser labels any retained data after connection failure. No live request silently falls back to mock data. Open-Meteo attribution is displayed below the forecast.
Project structure
family-dashboard/
├── docker-compose.yml
├── Dockerfile
├── .env.example
├── config/dashboard.json # Runtime settings
├── photos/ # Your photos (git-ignored)
├── frontend/
│ ├── index.html
│ └── src/
│ ├── main.tsx # React entry point
│ ├── App.tsx # Grid composition and widget isolation
│ ├── lib.ts # API polling, clock and sizing hooks
│ ├── styles.css
│ ├── settings/ # Configuration forms and layout preview
│ └── widgets/ # Independent widget components + local icons
├── backend/
│ ├── src/
│ │ ├── index.ts # Process entry point
│ │ ├── app.ts # HTTP routes and static frontend
│ │ ├── config.ts # Runtime configuration loading
│ │ ├── services.ts # Provider interfaces, mocks and photo service
│ │ ├── settings.ts # Atomic saves, backup and conflict detection
│ │ └── weather.ts # Live Open-Meteo/NWS adapters and caching
│ └── test/api.test.ts
├── shared/
│ ├── config.ts # Validated config schema and types
│ └── models.ts # API contracts
├── package.json
├── package-lock.json
├── tsconfig.json
└── vite.config.ts
Architecture and API
One container and one origin keep setup simple: Express serves the production Vite bundle, JSON APIs and mounted photos. No database is needed in v0.1. The frontend never imports the sample provider implementations. TypeScript contracts are shared, with backend provider interfaces ready for replacement by Graph, Google and ICS integrations. Outlook uses Microsoft device-code OAuth. Camera streams, kiosk setup, scheduling and a visual editor are not included.
| Endpoint | Response |
|---|---|
GET /api/health |
Configuration health status |
GET /api/config |
Validated runtime configuration |
GET /api/photos |
{ photos: [{ id, name, url }] } |
GET /photos/:name |
An allowed image in the mounted directory |
GET /api/calendar?start=…&end=… |
{ source: "mock", events: [...] }; max 93-day range |
GET /api/weather |
{ source: "open-meteo", updatedAt, stale, locations: [...] } |
GET /api/weather/alerts |
{ source: "nws", updatedAt, alerts: [...] } |
APIs use no-store; photos use short caching and modification-time URLs. Configuration refreshes every five seconds; photos/alerts/calendar every minute; forecasts every five minutes. Polling retains the last successful data after a temporary network problem; the initial startup retries automatically. Each widget has an error boundary so a rendering failure does not remove the others.
Local development and checks
Node 24 LTS is used by Docker. Install matching Node locally, then:
npm ci
npm run dev
Open http://localhost:5173. Vite proxies API/photo requests to Node on port 3000. Production without Docker:
npm run build
npm start
Stop the development server before starting production on the same port. Validate types, API behavior and production compilation with:
npm run check
See VALIDATION.md for what was actually checked and the outstanding Docker/LAN acceptance checks. The initial visual review is complete; Outlook connection is now available.
Live-provider references: Open-Meteo API, NWS alerts API. ZIP centroid reference: 33709.
Settings API: GET /api/settings returns configuration, revision and connected photo directory; PUT /api/settings accepts a JSON object with config and revision. The dashboard GET /api/config returns effective runtime settings. When upgrading older installs, replace CONFIG_FILE in .env with CONFIG_DIRECTORY=./config and recreate the container; keep your existing config/dashboard.json and photos.
Personal Outlook calendar connection
Open /config#calendars. Register your own app in Microsoft Entra with Personal Microsoft accounts only (or organizational plus personal accounts), enable Allow public client flows, and add delegated Microsoft Graph Calendars.Read permission. App registration requires an Entra directory and permission to register apps; a personal Outlook mailbox alone does not provision that directory.
Outlook has no OAuth callback route and needs no redirect URI. This backend uses MSAL's device-code grant with the consumers authority. Leave the registration redirect URI blank; no client secret is needed. Paste the Application (client) ID into the settings page, click Connect Outlook, and enter the displayed code at the exact Microsoft verification link shown by the dashboard (confirmed as https://www.microsoft.com/link for this installation). Select calendars, click Use selected calendars, then Save changes. Selection updates Outlook, removes samples and preserves Google calendars. One personal Microsoft account with multiple calendars is supported.
Complete calendar setup guide and Google Calendar
See the complete Outlook and Google setup guide, also served at /docs/calendar-setup.md. It includes the confirmed Outlook personal-account procedure, the exact returned Microsoft verification link, troubleshooting, Google Cloud setup, credential storage and Google's seven-day Testing-mode token limitation.
Google requires a Web application OAuth client, Calendar API enabled, and this exact authorized redirect URI: https://familydashboard.eiyq41.ddnss.de/api/google/callback. Start from the public HTTPS settings page so its secure browser-binding cookie returns on the callback. Paste client ID and secret into the Google Calendar panel. The secret and tokens are encrypted in the existing persistent private volume, separate from Outlook's cache. No Google credentials belong in dashboard JSON. The Google account connection is independent of Outlook.
Google routes: GET /api/google/status, GET /api/google/callback, and JSON POST /api/google/connect, /api/google/calendars, /api/google/cancel, /api/google/disconnect. The callback validates single-use state, a Secure/HttpOnly/SameSite=Lax cookie and PKCE before storing tokens. Calendar list and event scopes are read-only; Gmail access is not requested. Calendar API source can be mock, outlook, google, or mixed.
Graph calendarView expands recurring series, exceptions and single events over the visible month range. Cancelled events are excluded; all-day end dates remain exclusive. Events refresh every minute. Failed access shows an explicit warning rather than substituting sample events. Reconnect after consent revocation or sign-in expiry. Disconnect immediately removes the dashboard's token cache; selected calendar settings remain so reconnecting the same account can resume. To revoke Microsoft's underlying consent, remove the app from your Microsoft account permissions.
MSAL renews access tokens silently. Its cache is encrypted with AES-256-GCM and stored alongside a restricted encryption key in the outlook-data Docker volume, outside public assets and dashboard settings. This protects against accidentally copying the token file alone, not a host administrator with access to the entire volume. Preserve the volume across upgrades; deleting it requires signing in again. Local Node runs use ./data or DATA_DIR. Both data and build/deployment artifacts are excluded from the Docker build context and Git.
For an HTTPS reverse proxy, set PUBLIC_URL=https://familydashboard.eiyq41.ddnss.de in the host .env and recreate the container. This permits that exact browser origin for settings and connection actions without blindly trusting forwarded headers. It does not add access control: protect the public site with your proxy's authentication or private-network access, since the dashboard and configuration page have no built-in login.
Connection API: GET /api/outlook/status; JSON POST /api/outlook/connect with clientId; POST /api/outlook/poll with the returned unpredictable session id; POST /api/outlook/calendars; POST /api/outlook/disconnect. Tokens and Microsoft's device-code secret never go to the browser. Calendar API source is mock, outlook, or mixed, with an optional warning on provider failure.
References: Microsoft device-code flow, app registration, calendarView permissions and behavior.
Device-code diagnostics: set OUTLOOK_DEVICE_DIAGNOSTICS=1 in the Docker host .env and recreate to log the exact short user_code, returned verification_uri, expiry, polling interval, configured authority, actual endpoint tenant and client ID. Device codes and tokens are never logged. SHA-256 fingerprints correlate issuing responses and token polls. Without the diagnostic flag, user codes are redacted too. Logs include callback equality and polling-parameter checks. Active sign-in sessions cannot be silently replaced by another Connect request; cancel before requesting a replacement. The UI opens the exact Microsoft-returned verification URI.