A lightweight, self-hosted web application for browsing and managing event-based security camera recordings.
Does not do continuous recording or motion detection — those are handled by external systems (e.g. Home Assistant). HomeTimeline provides the library, timeline, and playback interface.
- Timeline — multi-camera, zoomable, date-range navigation, thumbnail preview on bars, click-to-play; the player has previous/next controls (and ← / → keys) to step through a camera's clips in time order
- Recordings — sortable table, date/camera filtering, thumbnail preview, inline playback, download
- Scanner — auto-discovers recordings on NAS; deduplicates by hash; generates thumbnails via ffmpeg; runs on a per-camera schedule (or Never for manual-only) or on demand per-camera
- Hikvision cameras — a camera can be typed Hikvision with host/credentials; the app pulls clips directly over ISAPI (per-day
YYYY-MM-DDfolders), indexes them like scanned files, shows live device details (model/firmware/RTSP/snapshot), and downloads on a per-camera schedule (or Never for manual-only) via a Download Videos button - Aqura cameras — a camera can be typed Aqura with 3 user-configured RTSP stream URLs and RTSP credentials; clips are stored by the camera itself onto a NAS in
YYYYMMDDfolders and are discovered by the scanner (no download or purge). Live view shows all 3 streams (Channel1/Channel2/Channel3) via go2rtc - Purge old videos — Hikvision cameras can auto-delete clips older than a per-camera retention window (delete older than N days, default Never); each purge removes the video file, its thumbnail, and the index entry, and runs on its own per-camera schedule (or Never for manual-only) via a Purge Old Videos button. All runs land in Activity
- Live view — real-time WebRTC video for Hikvision (main/sub) and Aqura (3 channels) cameras via an embedded go2rtc bridge, with a quality stream switch; the camera page puts the live feed on top over Timeline / Details / Commands tabs
- Live View wall — a dedicated Live View page shows every live-capable camera at once in an NVR-style grid, with a selectable cameras-per-row layout (Auto / 1× / 2× / 3× / 4×, persisted); each tile has a per-camera stream selector dropdown (Main/Sub for Hikvision, Channel1/2/3 for Aqura) and links back to its camera page
- Activity — unified feed of scan / download / purge runs; runs left unfinished by a restart are reconciled to an interrupted state on startup (rather than spinning forever)
- Dashboard — storage stats, recent recordings, health summary, bulk Download Videos (enabled Hikvision cameras with host/username/password) and Purge Videos (enabled Hikvision cameras with retention > 0) actions
- Settings — general app settings (display timezone), per-camera config (type, clip storage strategy, scan schedule, Hikvision connection + download schedule + purge retention/schedule, Aqura RTSP URLs + credentials), location management
- Timezone — all timestamps stored as UTC; displayed in any IANA timezone configured in General Settings
| Layer | Choice |
|---|---|
| Backend | FastAPI + Peewee + SQLite (WAL) |
| Frontend | React 18 + TypeScript + Vite + shadcn/ui + Tailwind |
| Video | ffmpeg (probe + thumbnails) + HTML5 range streaming; go2rtc (live WebRTC) |
| Container | Multi-stage Dockerfile (apt ffmpeg by default; optional custom FFmpeg target) |
| CI/CD | GitHub Actions — lint, test, build, push to ghcr.io on main |
Requires SSH access to a Linux server with Podman installed.
podman run -d --name hometimeline \
-p 8080:8080 \
-p 8555:8555 \
-v /opt/hometimeline/data:/opt/hometimeline/data \
-v /nas/camera:/nas/camera \
-e DATABASE_URL=sqlite:////opt/hometimeline/data/cam.db \
-e RECORDING_LOCATIONS=/nas/camera \
-e THUMBNAIL_DIR=/opt/hometimeline/data/thumbnails \
-e LOG_FILE=/opt/hometimeline/data/app.log \
-e GO2RTC_WEBRTC_CANDIDATE=<server-lan-ip>:8555 \
ghcr.io/dk307/hometimeline:latestThe recordings volume is mounted read-write (no
:ro): Hikvision cameras download clips into it. Use:roonly if you have no Hikvision cameras.Port 8555 and
GO2RTC_WEBRTC_CANDIDATE=<server-lan-ip>:8555are needed for live view (WebRTC): inside a container go2rtc can't detect the host's LAN address, so it's passed explicitly.scripts/deploy.pysets both automatically. Omit them if you don't need live view.
App served at http://server:8080. Display timezone and other app settings can be changed live from Settings → General; each camera's scan schedule is configured per-camera under Settings → Cameras.
Requires SSH key access to the server (see scripts/deploy.sh for setup).
./scripts/deploy.shThis validates locally, rsyncs source to the server, rebuilds the container via podman-compose up --build, and verifies health.
All persistent data lives on the host, mounted into the container:
| Path | Purpose |
|---|---|
data/cam.db |
SQLite database (recordings, cameras, scan events, settings) |
data/app.log |
Application log |
data/thumbnails/ |
Generated video thumbnails |
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
sqlite:///./data/cam.db |
SQLite path |
RECORDING_LOCATIONS |
/mnt/recordings |
Colon-separated list of root recording dirs |
THUMBNAIL_DIR |
./data/thumbnails |
Thumbnail output directory |
LOG_FILE |
./data/app.log |
Log file path (rotating, 5×5 MB). Point it inside the mounted data volume so logs survive container restarts. |
LOG_LEVEL |
INFO |
Logging verbosity |
Display timezone is configurable at runtime via Settings → General, and each camera's scan schedule via Settings → Cameras — no restart needed.
# Create and activate virtual environment
python3 -m venv .venv
source .venv/bin/activate
# Install project with dev dependencies
pip install -e ".[dev]"
# Run tests
pytest tests/unit tests/integration -vThe
.venv/directory is gitignored. Remember to activate it (source .venv/bin/activate) before running any Python commands. Do notpip install --break-system-packages— the venv is the only supported way to run tests locally.
# Unit + integration (isolated tmp DB, safe any time)
pytest tests/unit tests/integration -v
# E2E (needs a running container)
pytest tests/e2e -v --base-url=http://localhost:8080| Trigger | What runs |
|---|---|
| Every push / PR | Backend lint (ruff), unit + integration tests, frontend type-check + build |
| PR only | Docker build smoke test (no push) |
| Merge to main | Docker build + push to ghcr.io/dk307/hometimeline:latest and :<sha> |
app/ FastAPI backend
api/ Route handlers (cameras, recordings, timeline, settings, …)
models/ Peewee ORM models
schemas/ Pydantic request/response schemas
services/ Scanner, log buffer, timezone utilities
workers/ APScheduler background job
frontend/src/ React frontend
api/ Typed API clients
hooks/ Custom React hooks (useTimezone)
lib/ Utility modules (tz.ts — timezone-aware date formatting)
pages/ Page components (Dashboard, Timeline, Recordings, Settings)
components/ Shared components (VideoPlayer)
store/ Zustand UI state
tests/
unit/ Pure unit tests (scanner, models, tz utilities)
integration/ FastAPI TestClient against in-memory DB
e2e/ Playwright browser tests against live container
docker/Dockerfile Multi-stage: node:26-slim → python:3.14-slim + ffmpeg targets
docs/ Architecture design and agent guide
See docs/DESIGN.md for full architecture decisions.