My garden CCTV system started with a simple loop: Frigate detects movement, a Python script sends a Telegram notification, and I can look at a snapshot or short clip from my phone.
That was good enough for real-time alerts. It was not good enough for history.
Telegram is a terrible archive browser. It is excellent at interrupting me when something happens, but bad at answering questions like:
- What happened in the last two hours?
- Did I miss a person detection yesterday?
- Which clips exist for this camera?
- Can I open the full event without digging through chat history?
So the next small project was obvious: keep Telegram as the notification surface, but build a proper CCTV history portal on the Raspberry Pi itself.
This post documents the first working version.
Why Host It on the Pi?
The CCTV stack already runs on a Raspberry Pi 5:
- camera feed
- Frigate
- MQTT
- Telegram notification script
- video storage
Putting the portal somewhere else would create work for no real benefit. A cloud-hosted portal would need video uploads, sync logic, authentication, and another failure mode. The clips already live on the Pi. Frigate already has an API. The browser only needs a thin layer on top.
The MVP architecture is deliberately boring:
Browser / phone
↓
CCTV history portal on raspi5 :8097
↓
Frigate API on 127.0.0.1:5000
↓
Frigate event clips and snapshots
Telegram stays in the loop, but only as a launcher:
Frigate MQTT event
↓
frigate-notify.py
↓
Telegram alert with buttons
↓
Open event / Clip
↓
CCTV portal on the Pi
That split is important. Telegram should tell me something happened. The web portal should help me browse, filter, and play back history.
Finding the Existing Data Source
The useful thing about Frigate is that it already knows about events. There was no need to create a new database for the first version.
On the Pi, Frigate exposes events through its local API:
curl -fsS "http://127.0.0.1:5000/api/events?limit=3"
That response includes the pieces the portal needs:
- event id
- camera name
- label
- start and end timestamps
- whether a snapshot exists
- whether a clip exists
- zones and score metadata
Clips are also available directly:
curl -I "http://127.0.0.1:5000/api/events/<event-id>/clip.mp4"
So the portal does not own the archive. Frigate remains the source of truth.
Building the Smallest Useful Portal
I first checked what Python dependencies were already available on the Pi. jinja2 was installed, but fastapi, uvicorn, and httpx were not.
For an MVP, adding a framework felt unnecessary. The portal only needed:
- one timeline page
- one event detail page
- a snapshot proxy
- a clip proxy
- a health check endpoint
So I built it with Python’s standard library: http.server, urllib, and a small amount of HTML rendering.
The first version exposes:
GET / timeline
GET /event/<id> event detail page
GET /media/<id>/snapshot.jpg Frigate snapshot proxy
GET /media/<id>/clip.mp4 Frigate clip proxy
GET /healthz health check
The timeline has simple filters:
- last 2 hours
- last 6 hours
- today
- yesterday
- all
- camera
- label
- search text
That is enough for the job I actually need: open a browser on the home network and quickly answer “what happened recently?”
Installing It as a Service
The portal lives on the Pi at:
/home/timur/cctv-history-portal/portal.py
I installed it with a systemd unit:
sudo install -d -o timur -g timur /home/timur/cctv-history-portal
sudo install -m 0755 -o timur -g timur /tmp/portal.py /home/timur/cctv-history-portal/portal.py
sudo install -m 0644 -o root -g root /tmp/cctv-history-portal.service /etc/systemd/system/cctv-history-portal.service
sudo systemctl daemon-reload
sudo systemctl enable --now cctv-history-portal.service
The service listens on port 8097:
http://192.168.0.23:8097/
For now this is LAN-only. I deliberately did not punch a public hole into the Pi. The right next step is to put it behind an OpenClaw-authenticated route, not expose a home CCTV archive directly to the internet.
Telegram as the Remote Control
The existing notification bot already posted Frigate alerts into my Telegram group. I changed it so new notifications include an inline keyboard:
def event_keyboard(event_id):
if not event_id:
return None
return {
"inline_keyboard": [[
{"text": "Open event", "url": f"{PORTAL_URL}/event/{event_id}"},
{"text": "Clip", "url": f"{PORTAL_URL}/media/{event_id}/clip.mp4"},
]]
}
The result is simple:
Open eventopens the event page in the portalClipopens the MP4 directly
This is much better than trying to make Telegram itself into the archive UI. Telegram does what it is good at: timely notifications. The browser does what it is good at: navigation and playback.
One caveat: old alerts do not magically get buttons. Only new notifications sent after the bot update include the portal links.
The iPhone Playback Bug
The first version looked fine on desktop and still failed on my phone.
The portal rendered the event page. The video element was there. The MP4 endpoint returned data. But mobile Safari would not play it.
The bug was in the clip proxy.
The first implementation streamed the MP4 from Frigate to the browser, but it did not behave like a real static MP4 endpoint. Safari expects byte-range support for video playback. If the browser asks for part of a file, the server needs to answer with 206 Partial Content, not just dump the whole file as a stream.
The fix was to make the portal’s clip route support:
HTTP/1.1Accept-Ranges: bytesRangerequests from the browser206 Partial Content- correct
Content-Range - correct
Content-Length HEADrequests for media probes
The verification command looked like this:
curl --http1.1 -D - -o /tmp/range-body \
-H "Range: bytes=0-1023" \
"http://127.0.0.1:8097/media/<event-id>/clip.mp4"
And the important response was:
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/1079008
Content-Length: 1024
Accept-Ranges: bytes
After that, the clip endpoint behaved like something a mobile browser could trust.
This is the kind of bug that makes “just proxy the file” slightly less true than it sounds.
Verification
After deployment I checked:
systemctl is-active cctv-history-portal.service
curl -fsS http://127.0.0.1:8097/healthz
curl -fsS http://127.0.0.1:8097/
curl -fsS http://127.0.0.1:8097/event/<event-id>
curl -fsS -o /tmp/portal-snapshot.jpg http://127.0.0.1:8097/media/<event-id>/snapshot.jpg
curl --http1.1 -r 0-1023 -o /tmp/portal-clip.part http://127.0.0.1:8097/media/<event-id>/clip.mp4
The service was active, the health check returned ok, the timeline rendered, the event page rendered, snapshots came back as real JPEGs, and clips supported byte-range playback.
The Telegram notification bot was also restarted and reconnected to MQTT after the inline-button change.
What I Did Not Build Yet
There is no separate database in this version.
That is intentional. A database becomes useful when the portal needs state that Frigate does not have:
- starred clips
- hidden clips
- notes
- richer search
- joins back to Telegram message ids
- manual classifications
Until then, Frigate’s API is enough.
There is also no remote access yet. This is a local home-network tool. Remote access needs authentication first.
The Useful Pattern
The pattern here is broader than CCTV:
- Let the existing system remain the source of truth.
- Add a very thin browsing layer close to the data.
- Keep chat as the notification and command surface.
- Move real inspection into a web UI.
That is a good shape for personal automation.
Chat is brilliant for interruption. It is mediocre for browsing. A tiny local web app can fill that gap without turning the whole project into a platform.
This CCTV portal is not finished, but it is already useful: Frigate keeps recording, Telegram keeps alerting, and now there is a proper place to browse the history.