Add smart alert profiles and recording timeline (Tasks 4-5)
Task 4 — Alert Profiles: presence-aware + time-of-day alert routing. Profiles match household state (EMPTY/KIDS_HOME/ADULTS_HOME/ALL_HOME) and time windows (sleep hours). Per-detection-type rules control push/record/quiet behavior with role-based recipients (all vs adults). Task 5 — Recording Timeline: canvas-based 24h timeline per camera with color-coded segments (person=red, vehicle=blue, motion=gray). Click-to-play, date picker, detection type filters, hour markers. Timeline API endpoint returns segments for a camera+date. All 5 daily-use feature tasks complete. 140 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,11 +4,52 @@
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0"><i class="bi bi-camera-video me-2"></i>Recordings</h5>
|
||||
<select class="form-select form-select-sm" id="filter-camera" style="width: auto;">
|
||||
<option value="">All Cameras</option>
|
||||
</select>
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<input type="date" class="form-control form-control-sm bg-dark text-light border-secondary"
|
||||
id="timeline-date" style="width: auto;">
|
||||
<select class="form-select form-select-sm bg-dark text-light border-secondary"
|
||||
id="filter-camera" style="width: auto;">
|
||||
<option value="">All Cameras</option>
|
||||
</select>
|
||||
<div class="btn-group btn-group-sm" id="filter-type">
|
||||
<button class="btn btn-outline-light active" data-type="all">All</button>
|
||||
<button class="btn btn-outline-danger" data-type="person">Person</button>
|
||||
<button class="btn btn-outline-primary" data-type="vehicle">Vehicle</button>
|
||||
<button class="btn btn-outline-secondary" data-type="motion">Motion</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline canvases (one per camera) -->
|
||||
<div id="timeline-container">
|
||||
<div class="card bg-dark border-secondary mb-3" id="timeline-placeholder">
|
||||
<div class="card-body text-center text-muted py-3">
|
||||
Select a camera to view the recording timeline
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Video player area -->
|
||||
<div class="card bg-dark border-secondary mb-3 d-none" id="player-card">
|
||||
<div class="card-header border-secondary d-flex justify-content-between align-items-center">
|
||||
<span id="player-title">Recording</span>
|
||||
<button class="btn btn-sm btn-outline-light" id="player-close">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<video id="player-video" class="w-100" controls></video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="d-flex gap-3 mb-3 small text-muted">
|
||||
<span><span class="badge" style="background:#dc3545"> </span> Person</span>
|
||||
<span><span class="badge" style="background:#0d6efd"> </span> Vehicle</span>
|
||||
<span><span class="badge" style="background:#6c757d"> </span> Motion</span>
|
||||
</div>
|
||||
|
||||
<!-- Fallback table for recordings list -->
|
||||
<div class="card bg-dark border-secondary">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
@@ -47,4 +88,71 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/timeline.js') }}"></script>
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const dateInput = document.getElementById('timeline-date');
|
||||
const cameraSelect = document.getElementById('filter-camera');
|
||||
const container = document.getElementById('timeline-container');
|
||||
const placeholder = document.getElementById('timeline-placeholder');
|
||||
const playerCard = document.getElementById('player-card');
|
||||
const playerVideo = document.getElementById('player-video');
|
||||
const playerTitle = document.getElementById('player-title');
|
||||
const playerClose = document.getElementById('player-close');
|
||||
|
||||
// Default to today
|
||||
dateInput.value = new Date().toISOString().split('T')[0];
|
||||
|
||||
playerClose.addEventListener('click', () => {
|
||||
playerCard.classList.add('d-none');
|
||||
playerVideo.pause();
|
||||
playerVideo.src = '';
|
||||
});
|
||||
|
||||
function handleSegmentClick(seg) {
|
||||
playerCard.classList.remove('d-none');
|
||||
playerTitle.textContent = `Recording #${seg.id} (${seg.type || 'motion'})`;
|
||||
playerVideo.src = `/recordings/${seg.id}/download`;
|
||||
playerVideo.play().catch(() => {});
|
||||
}
|
||||
|
||||
function loadTimeline() {
|
||||
const cameraId = cameraSelect.value;
|
||||
const date = dateInput.value;
|
||||
|
||||
if (!cameraId) {
|
||||
placeholder.classList.remove('d-none');
|
||||
// Remove any existing timeline canvases
|
||||
container.querySelectorAll('.timeline-row').forEach(el => el.remove());
|
||||
return;
|
||||
}
|
||||
|
||||
placeholder.classList.add('d-none');
|
||||
container.querySelectorAll('.timeline-row').forEach(el => el.remove());
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'card bg-dark border-secondary mb-2 timeline-row';
|
||||
row.innerHTML = `
|
||||
<div class="card-header border-secondary py-1 small">${cameraId}</div>
|
||||
<div class="card-body p-2">
|
||||
<canvas data-height="48" style="width:100%;cursor:pointer"></canvas>
|
||||
</div>`;
|
||||
container.appendChild(row);
|
||||
|
||||
const canvas = row.querySelector('canvas');
|
||||
const tl = new VigilarTimeline(canvas, {
|
||||
cameraId: cameraId,
|
||||
date: date,
|
||||
onClick: handleSegmentClick,
|
||||
});
|
||||
tl.load();
|
||||
}
|
||||
|
||||
cameraSelect.addEventListener('change', loadTimeline);
|
||||
dateInput.addEventListener('change', loadTimeline);
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user