Add sync clock entity for synchronized animation timing

Introduces Synchronization Clocks — shared, controllable time bases
that CSS sources can optionally reference for synchronized animation.

Backend:
- New SyncClock dataclass, JSON store, Pydantic schemas, REST API
- Runtime clock with thread-safe pause/resume/reset and speed control
- Ref-counted runtime pool with eager creation for API control
- clock_id field on all ColorStripSource types
- Stream integration: clock time/speed replaces source-local values
- Paused clock skips rendering (saves CPU + stops frame pushes)
- Included in backup/restore via STORE_MAP

Frontend:
- Sync Clocks tab in Streams section with cards and controls
- Clock dropdown in CSS editor (hidden speed slider when clock set)
- Clock crosslink badge on CSS source cards (replaces speed badge)
- Targets tab uses DataCache for picture/audio sources and sync clocks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 21:46:55 +03:00
parent 52ee4bdeb6
commit aa1e4a6afc
32 changed files with 1255 additions and 58 deletions

View File

@@ -9,6 +9,7 @@ import {
kcWebSockets,
ledPreviewWebSockets,
_cachedValueSources, valueSourcesCache,
streamsCache, audioSourcesCache, syncClocksCache,
} from '../core/state.js';
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isOpenrgbDevice } from '../core/api.js';
import { t } from '../core/i18n.js';
@@ -475,15 +476,17 @@ export async function loadTargetsTab() {
if (!csDevices.isMounted()) setTabRefreshing('targets-panel-content', true);
try {
// Fetch devices, targets, CSS sources, picture sources, pattern templates, and value sources in parallel
const [devicesResp, targetsResp, cssResp, psResp, patResp, valueSrcArr, asResp] = await Promise.all([
// Fetch devices, targets, CSS sources, pattern templates in parallel;
// use DataCache for picture sources, audio sources, value sources, sync clocks
const [devicesResp, targetsResp, cssResp, patResp, psArr, valueSrcArr, asSrcArr] = await Promise.all([
fetchWithAuth('/devices'),
fetchWithAuth('/picture-targets'),
fetchWithAuth('/color-strip-sources').catch(() => null),
fetchWithAuth('/picture-sources').catch(() => null),
fetchWithAuth('/pattern-templates').catch(() => null),
streamsCache.fetch().catch(() => []),
valueSourcesCache.fetch().catch(() => []),
fetchWithAuth('/audio-sources').catch(() => null),
audioSourcesCache.fetch().catch(() => []),
syncClocksCache.fetch().catch(() => []),
]);
const devicesData = await devicesResp.json();
@@ -499,10 +502,7 @@ export async function loadTargetsTab() {
}
let pictureSourceMap = {};
if (psResp && psResp.ok) {
const psData = await psResp.json();
(psData.streams || []).forEach(s => { pictureSourceMap[s.id] = s; });
}
psArr.forEach(s => { pictureSourceMap[s.id] = s; });
let patternTemplates = [];
let patternTemplateMap = {};
@@ -516,10 +516,7 @@ export async function loadTargetsTab() {
valueSrcArr.forEach(s => { valueSourceMap[s.id] = s; });
let audioSourceMap = {};
if (asResp && asResp.ok) {
const asData = await asResp.json();
(asData.sources || []).forEach(s => { audioSourceMap[s.id] = s; });
}
asSrcArr.forEach(s => { audioSourceMap[s.id] = s; });
// Fetch all device states, target states, and target metrics in batch
const [batchDevStatesResp, batchTgtStatesResp, batchTgtMetricsResp] = await Promise.all([