SPF5000 V1 consists of:
- a Python FastAPI backend
- a React + TypeScript + Vite frontend
- a minimal
spf5000.tomlruntime config for host/port/paths/logging/session secret - local-files provider runtime credentials and sync cadence in
spf5000.toml - a cached weather and alert subsystem with a National Weather Service provider
- DecentDB for metadata, settings, display profiles, and import job history
- DecentDB-backed bootstrap state plus a single local admin user
- admin-protected ZIP backup/restore and collection media export workflows
- filesystem-backed originals and generated image variants
- provider-backed offline sync metadata for local import state
- configurable background presentation modes for display, with cached color metadata plus render-time image-based treatments
- a fullscreen
/displayroute optimized for kiosk playback on Raspberry Pi
The architecture follows the accepted ADR set in design/adr/0001 through 0015 plus 0018.
- bootstrap runtime directories and DecentDB schema at startup
- load startup/runtime settings from
spf5000.toml - expose REST endpoints for setup, auth/session, health, status, settings, sources, collections, assets, uploads/imports, backup/export operations, and display state
- expose an authenticated sleep-schedule time-reference API so admin clients can compare server UTC, Pi-local timezone, and configured display timezone
- expose weather settings, weather status, weather alert, and display-facing weather APIs
- expose doctor/health diagnostics APIs for subsystem status and remediation guidance
- keep routes thin and place orchestration in services
- persist state through explicit repository SQL over the DecentDB DB-API binding
- manage local import, admin uploads, duplicate detection, original-file storage, and derivative generation
- manage database snapshot/export, validated database restore, collection media export, and the runtime coordination those workflows require
- manage scheduled weather and alert refresh into local cached state
- protect admin APIs with signed session cookies while keeping display APIs public
- serve built frontend assets from
frontend/distwhen available
- provide a browser-based admin shell for configuration and diagnostics
- provide
/setupand/loginflows before the protected/adminshell - provide a dedicated fullscreen
/displayroute with no admin chrome - provide a dedicated Backups page for database backup/restore and collection ZIP export
- provide a dedicated Doctor page for health diagnostics and troubleshooting
- consume backend API endpoints through typed helpers under
frontend/src/api/ - keep display playback independent from the admin shell layout
- render configurable background presentation behind slideshow images using cached playlist metadata plus the display variant when needed
- render a configurable weather widget plus alert badge/banner/fullscreen overlays from cached backend data
- DecentDB stores structured state and metadata
- DecentDB also stores bootstrap state and the single local admin record
- the filesystem stores original image binaries, generated display derivatives, generated thumbnails, staging data, and fallback assets
- database backup/restore operates on the DecentDB file, while collection export reads original media from the filesystem
- Raspberry Pi OS boots into a lightweight graphical session.
- The SPF5000 backend starts locally.
- Backend startup reads
spf5000.toml, initializes logging, directories, the DecentDB schema, and default records. - Chromium opens the local
/displayroute in kiosk/fullscreen mode. - Administrators use a browser on the LAN to access
/setup,/login, and/admin.
backend/app/main.py uses a FastAPI lifespan hook to:
- configure logging
- initialize storage directories
- create the fallback idle asset
- create missing DecentDB tables and indexes
- ensure default settings, default local source, default collections, the default display profile, and auth/bootstrap tables exist
If the DecentDB binding is unavailable, the app preserves the existing NullConnection fallback path instead of crashing during import time.
spf5000.toml is intentionally limited to startup/runtime concerns:
- bind host and port
- runtime storage paths
- log level
- optional session-cookie signing secret
Application settings such as slideshow timing, transition behavior, selected collection, sleep schedule, optional display timezone, bootstrap completion, and admin users remain in DecentDB.
Default backend-managed paths:
backend/data/
spf5000.ddb
fallback/
empty-display.jpg
sources/
local-files/
import/
staging/
imports/
storage/
originals/
variants/
display/
thumbnails/
- imported originals are copied into managed storage
- display playback uses generated display derivatives rather than full originals
- admin pages use generated thumbnail derivatives
- deterministic filenames/paths are derived from asset metadata and checksums
- admin backup/restore workflows may create temporary files under
staging/backup-restore/andstaging/exports/
The V1 schema bootstraps these primary tables:
Key/value device settings, including:
frame_namedisplay_variant_widthdisplay_variant_heightthumbnail_max_sizeslideshow_interval_secondstransition_modetransition_duration_msfit_modeshuffle_enabledselected_collection_idactive_display_profile_idbackground_fill_modedisplay_timezonesleep_schedule_enabledsleep_start_local_timesleep_end_local_timeweather_enabledweather_providerweather_locationweather_unitsweather_positionweather_scaleweather_refresh_minutesweather_show_precipitationweather_show_humidityweather_show_windweather_alerts_enabledweather_alert_fullscreen_enabledweather_alert_minimum_severityweather_alert_repeat_enabledweather_alert_repeat_interval_minutesweather_alert_repeat_display_seconds
Configured provider sources. V1 seeds a default local_files source.
Logical groupings of imported assets. V1 seeds a default collection for local media. Each collection may have an optional storage_path that overrides where its assets are stored under the global storage/originals/ directory.
Canonical imported image records, including:
- source ownership
- filename/original filename
- checksum
- dimensions and file size
- imported-from path
- managed original path
- metadata JSON
- cached color metadata derived from display variants for color-based background modes
- imported timestamps
- active flag
Generated derivatives keyed by asset and kind. V1 creates:
displaythumbnail
Join table mapping assets into collections with stable sort order.
Scan/import job history with discovered/imported/duplicate/skipped/error counters plus sample filenames and completion status.
Persisted slideshow behavior, including:
- selected collection
- slideshow interval seconds
- transition mode
- transition duration milliseconds
- fit mode
- shuffle flag
- idle message
- playlist refresh interval seconds
Persistence model: The DisplayService.update_config() method intentionally splits writes between two repositories:
settings_repo— fields that affect the display pipeline (slideshow_interval_seconds,transition_mode,transition_duration_ms,fit_mode,shuffle_enabled,selected_collection_id,background_fill_mode,shuffle_bag_enabled). These are kept insettingsbecause display pipeline code reads them fromSettingsRepositoryat render time, avoiding a separate display profile lookup on every frame.display_repo— fields that are purely administrative or rarely changed (name,idle_message,refresh_interval_seconds). These belong in the display profile table.
This split is historical rather than logical; consolidating all display fields into display_profiles would simplify the service code but requires careful migration planning. See ADR 0013 for tracking.
The single local admin record, including:
- username
- password hash
- enabled flag
- last login timestamp
Small key/value runtime metadata such as:
- bootstrap completion marker
- other system-level state that should stay in DecentDB
Current provider health and refresh metadata, including:
- provider status (
ready,degraded,disabled,unconfigured) - last attempted and last successful weather refresh timestamps
- last attempted and last successful alert refresh timestamps
- current provider error text
Cached normalized current conditions for the configured location, including:
- condition summary and icon token
- canonical temperature value
- humidity, wind, and precipitation-chance details
- observation and fetch timestamps
Cached normalized active alerts for the configured location, including:
- event, severity, certainty, and urgency
- headline, area, description, and instruction
- escalation mode and alert priority metadata
- issue/effective/expiry timestamps
Refresh history for weather and alert cache updates, including:
- refresh kind (
weatheroralerts) - trigger (
scheduledormanual) - completion status and error text
- start and completion timestamps
Providers implement the protocol in backend/app/providers/base.py. SPF5000 ships with LocalFilesProvider.
POST /api/import/local/scanscans the configured import directory recursively.- Supported image extensions are filtered using backend config.
POST /api/import/local/runimports discovered images.- SHA-256 checksum comparison prevents duplicate asset creation.
- Managed original files are written to the filesystem.
- Pillow extracts image metadata and generates display/thumbnail derivatives.
- DecentDB records the asset, variants, collection membership, and job history.
Import failures do not stop the display route from continuing to run with the existing library.
GET /api/backup/database/exportcheckpoints and snapshots the active DecentDB file into a ZIP that includesspf5000.ddbplusbackup-manifest.json.POST /api/backup/database/importvalidates the uploaded ZIP structure, verifies that the bundled database looks like an SPF5000 database, pauses background coordinators, swaps the active.ddb, re-runs runtime initialization, resets connection state, and clears the current admin session.GET /api/backup/collections/{collection_id}/exportpackages exportable original media files pluscollection-export-manifest.json, skips missing or out-of-bounds originals, and fails only when no exportable originals remain.- Database restore does not restore original media files or generated variants; collection export is the media-moving path in V1.
- The admin configures a weather location, widget settings, and alert behavior from the Weather page.
- A dedicated weather coordinator refreshes cached current conditions and active alerts on a schedule.
- The provider normalizes remote payloads into SPF5000 weather and alert models before persistence.
/api/display/weatherand/api/display/alertsexpose only cached normalized state to the public display route./displayrenders weather and alert overlays without waiting on live provider requests.
/displayis intentionally independent from the admin shell- the display route renders with a hidden cursor and a black fallback background
- the display route shows a calm idle state when no assets are available
- the display route can intentionally render a solid black fullscreen sleep state during the configured sleep window
- the display route can render a cached weather widget and alert overlays without changing slideshow layer ownership
- the display route can render
black,dominant_color,gradient,soft_vignette,palette_wash,blurred_backdrop,mirrored_edges, oradaptive_autobackground presentation behind the slideshow image when configured
The slideshow uses two persistent absolutely positioned layers:
- one visible layer presents the current image
- one hidden layer preloads and decodes the next image
- motion begins only after the next image is ready
- the outgoing slide moves to the right while the incoming slide enters from the left
- the backing black background never becomes the intended transition state
Background treatment is rendered in separate persistent layers behind the slideshow image layers. Cached display-variant metadata remains the source for color-based modes such as dominant_color, gradient, soft_vignette, and palette_wash, and that metadata is persisted with asset records and lazily backfilled for older assets that predate the feature. Image-based treatments such as blurred_backdrop and mirrored_edges may reuse the display variant directly at render time.
adaptive_auto is a display-behavior policy. It chooses among supported treatments based on the current asset's aspect mismatch and the cached metadata available for that asset, favoring richer treatments when the data is ready and falling back to simpler supported options when it is not.
This preserves the ADR 0008 requirement to avoid a visible full-black frame between slides.
Scheduled sleep is the only intentional full-black display state. Entering or leaving sleep mode is separate from normal image-to-image transitions and does not relax the ADR 0008 transition rule.
- the sleep schedule is stored in DecentDB-backed application settings, not in
spf5000.toml,systemd, cron, or Chromium flags - authenticated administrators manage the schedule and optional display timezone from the admin UI through dedicated sleep-schedule settings APIs
/api/display/playlistincludes the effective sleep schedule plus the configured display timezone so the public/displayroute can enforce it without requiring admin auth- the display evaluates the schedule against the configured display timezone and falls back to the Pi-local timezone when no explicit display timezone is set
- sleep start time is inclusive and sleep end time is exclusive, so the frame wakes at the configured end time
- overnight windows are supported
- when sleep is active, the display renders a solid black fullscreen overlay, pauses slideshow timers and transitions, and resumes playback automatically after the sleep window ends
- when the schedule is enabled, identical start and end times are rejected as invalid
V1 supports these end-to-end settings:
- display duration in seconds
- transition duration in milliseconds
- transition type (
slidetoday) - fit mode (
containorcover) - background fill mode (
black,dominant_color,gradient,soft_vignette,palette_wash,blurred_backdrop,mirrored_edges, oradaptive_auto) - shuffle enabled/disabled
- selected collection
- idle message
- playlist refresh interval seconds
- sleep schedule enabled/disabled
- display timezone selection with Pi-local fallback
- sleep start and end times evaluated in the configured display timezone
- weather widget enabled/disabled
- weather widget position, scale, and units
- weather detail toggles for precipitation, humidity, and wind
- alert minimum severity, fullscreen allowance, and repeat cadence
- the weather widget is a persistent overlay separate from slideshow layers, with configurable corner or vertical side placement and scale
- banner and badge alerts stay outside the slideshow transition machinery
- fullscreen alerts pause slideshow timers instead of changing the dual-layer renderer itself
fullscreen_repeatreturns to a banner between repeated fullscreen takeovers- sleep mode remains the highest-precedence display state
The React admin shell currently includes:
/setupfor first-run bootstrap when no admin exists/loginfor returning administrators/adminas the protected shell rootDashboardfor system/library statusSettingsfor device and derivative defaultsLibraryfor batch uploading, filtering, and browsing imported assets and variantsCollectionsfor collection managementBackupsfor database backup/restore plus collection media exportSourcesfor local source configuration plus scan/import actionsDisplay Settingsfor slideshow behavior, the sleep schedule, display timezone selection, and Pi-local/configured display-time clarityWeatherfor weather widget settings, provider/cache status, and alert visibility
Frontend API access stays under frontend/src/api/ and uses relative /api/... paths so the Vite proxy works in development and the same routes work when FastAPI serves the production build.
Admin routing behavior:
/redirects to/setup,/login, or/adminbased on session/bootstrap state/admin/*requires an authenticated admin session/setupbecomes unavailable once an enabled admin exists/displayremains intentionally public and independent from the admin shell
GET /api/healthGET /api/status(authenticated admin)GET /api/system/status(authenticated admin)
POST /api/setupGET /api/auth/sessionPOST /api/auth/loginPOST /api/auth/logout
GET /api/settings(authenticated admin)PUT /api/settings(authenticated admin)GET /api/settings/sleep-schedule(authenticated admin)PUT /api/settings/sleep-schedule(authenticated admin)
GET /api/weather/settings(authenticated admin)PUT /api/weather/settings(authenticated admin)GET /api/weather/status(authenticated admin)GET /api/weather/alerts(authenticated admin)POST /api/weather/refresh(authenticated admin)
GET /api/collections(authenticated admin)GET /api/collections/{collection_id}(authenticated admin)POST /api/collections(authenticated admin)PUT /api/collections/{collection_id}(authenticated admin)
GET /api/assets(authenticated admin)GET /api/assets/{asset_id}(authenticated admin)POST /api/assets/upload(authenticated admin)GET /api/assets/{asset_id}/variants/{kind}
GET /api/backup/database/export(authenticated admin)POST /api/backup/database/import(authenticated admin)GET /api/backup/collections/{collection_id}/export(authenticated admin)
GET /api/sources(authenticated admin)PUT /api/sources/{source_id}(authenticated admin)POST /api/import/local/scan(authenticated admin)POST /api/import/local/run(authenticated admin)
GET /api/display/config(authenticated admin)PUT /api/display/config(authenticated admin)GET /api/display/playlistGET /api/display/weatherGET /api/display/alerts
All admin endpoints require an authenticated session. Obtain one via login, then include the session cookie in subsequent requests.
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"your-password"}' \
-c cookies.txtResponse (200):
{"username":"admin","bootstrapped":true,"user":{"username":"admin","is_admin":true}}Error (401):
{"detail":"Invalid username or password"}curl http://localhost:8000/api/auth/session -b cookies.txtcurl -X POST http://localhost:8000/api/auth/logout -b cookies.txtcurl http://localhost:8000/api/healthResponse: {"ok":true,"app":"SPF5000","version":"...","database_available":true}
curl http://localhost:8000/api/health/deep -b cookies.txtResponse includes disk space, cache size, sync status, weather status, and asset count.
curl http://localhost:8000/api/settings -b cookies.txtcurl -X PUT http://localhost:8000/api/settings \
-H "Content-Type: application/json" \
-d '{"slideshow_seconds_per_image":10}' \
-b cookies.txtcurl http://localhost:8000/api/collections -b cookies.txtcurl http://localhost:8000/api/collections/{collection_id} -b cookies.txtcurl http://localhost:8000/api/assets -b cookies.txtcurl -X POST http://localhost:8000/api/assets/upload \
-F "file=@photo.jpg" \
-b cookies.txtcurl http://localhost:8000/api/display/playlistResponse:
{
"items": [
{
"id": "uuid",
"filename": "photo.jpg",
"variant_url": "/api/assets/{id}/display"
}
]
}curl http://localhost:8000/api/display/weather| Code | Meaning |
|---|---|
| 400 | Invalid request body or parameters |
| 401 | Missing or invalid session |
| 403 | Operation not permitted |
| 404 | Resource not found |
| 409 | Conflict (e.g., duplicate asset) |
| 422 | Validation error (Pydantic) |
| 429 | Rate limited |
| 500 | Internal server error |
| 503 | Service unavailable (no assets, etc.) |
Rate limit: 60 requests/minute per session on admin endpoints (enforced when SPF5000_RATE_LIMIT=true).
- backend launched with
cd backend && .venv/bin/python -m app - frontend Vite dev server on port
5173 - Vite proxies
/apito the backend
- build frontend assets into
frontend/dist - let FastAPI serve
frontend/dist - run the backend locally on the Pi
- provide a stable
spf5000.toml(orSPF5000_CONFIG) for runtime deployment settings - point Chromium kiosk mode at
http://127.0.0.1:8000/display
scripts/install-pi.shis the first-pass Pi-specific installer for Raspberry Pi OS Desktop- the installer assumes an existing SPF5000 checkout, creates or refreshes
backend/.venv, installs the DecentDB Python binding from the matching source archive, downloads the DecentDB native library from the matching release bundle, buildsfrontend/dist, generates a runtimespf5000.toml, installs thesystemdunit, and installs the Chromium autostart entry for the selected non-root user - default Pi runtime paths are
/opt/spf5000,/var/lib/spf5000,/var/cache/spf5000, and/var/lib/spf5000/spf5000.toml - the generated
systemdunit runscd backend && .venv/bin/python -m appwithSPF5000_CONFIGpointing at the generated runtime config andDECENTDB_NATIVE_LIBpointing at the staged DecentDB C API library from the downloaded release bundle - the generated Chromium autostart entry launches the local
/displayroute in kiosk mode after a short startup delay scripts/uninstall-pi.shremoves the service and kiosk autostart while preserving config, database, cache, and imported assets by defaultscripts/doctor.shchecks runtime prerequisites, service state, filesystem paths, local health endpoints, display playlist/sleep state, first-slide asset reachability, and kiosk wiring
Admin sessions are protected by signed HTTP-only cookies. The following configurations apply:
- Development / local Pi:
https_only = false(default). Sessions work over plain HTTP. Appropriate for the localhost/LAN trust boundary of a home photo frame. - Production with reverse proxy: Set
security.session_https_only = trueinspf5000.tomlwhen the app runs behind a TLS-terminating reverse proxy (nginx, Caddy, etc.). This marks cookies asSecure, preventing transmission over plain HTTP.
The admin API uses signed session cookies with allow_credentials=True in CORS configuration. No explicit CSRF token is issued.
Threat model: SPF5000 is a LAN-only appliance. The admin interface is expected to be used from the same browser on the same device as the photo frame. Cross-site request forgery attacks require a malicious site visited by the same browser to forge requests to the local admin API — a low-probability scenario on a home network behind a router's NAT.
Acceptance rationale: CSRF protection adds complexity (token issuance, storage, validation) for a threat scenario that does not apply to this deployment model. The risk is accepted given the appliance's LAN-only, single-user nature. If the product is ever exposed beyond the LAN (e.g., via cloud sync), CSRF protection becomes mandatory before deployment.
All API inputs are validated by Pydantic schemas. File uploads are validated for image MIME type and extension before processing. DecentDB enforces a UNIQUE constraint on assets.checksum_sha256 to prevent duplicate assets.
The current implementation has been validated with:
cd backend && .venv/bin/python -m pytestcd frontend && npm run build
- local-files provider is implemented
- weather currently supports a single configured location and the National Weather Service provider
- admin batch uploads into local collections are supported, while destructive library management remains out of scope
- database backup/restore remains DB-only; moving original media still requires collection export or another filesystem-aware migration step
- v1 supports only a single local admin account with cookie-backed sessions