// data.jsx - API client; populates window.ZAMPP_DATA from real endpoints const API = '/api/v1'; window.ZAMPP_API_PREFIX = API; // single source of truth (#115) // Gated logger (#123). Production deploys ship muted; appending ?debug=1 // to the URL (or localStorage.df_debug = '1') re-enables full console output. (function setupLogger() { let enabled = false; try { enabled = /(?:^|[?&])debug=1(?:&|$)/.test(location.search) || localStorage.getItem('df_debug') === '1' || location.hostname === 'localhost' || location.hostname === '127.0.0.1'; } catch {} const noop = () => {}; window.DF_LOG = { debug: enabled ? console.debug.bind(console) : noop, warn: enabled ? console.warn.bind(console) : noop, error: console.error.bind(console), // errors always surface }; })(); // Premiere panel releases embedded in this deployment. Bumping the version // here is the single source of truth - both the Editor download buttons and // the Settings → Capture SDKs page read from this list (#125). // // The panel is now a UXP plugin (.ccx), replacing the legacy CEP/ZXP panel. // Each entry carries a `ccx` URL; the older `zxp`/`installer` fields are gone. window.PREMIERE_RELEASES = [ { version: '2.2.2', ccx: '/downloads/dragonflight-mam-2.2.2.ccx', installer: null, notes: 'UXP plugin: one-click Export Timeline, library + conform + auto-relink, growing-file mount, runtime version chip. Replaces the legacy CEP/ZXP panel.', latest: true, }, ]; window.PREMIERE_LATEST = window.PREMIERE_RELEASES.find(r => r.latest) || window.PREMIERE_RELEASES[0]; // Teams ISO workstation installer. Placeholder slot: the .exe is not in the // repo yet, so `available` is false and the Downloads modal renders the row // disabled with a "coming soon" note. Drop the file into public/downloads/ // and flip `available: true` (set `version`) to finish it. window.TEAMS_ISO = { version: null, url: '/downloads/TeamsISO.exe', available: false }; window.ZAMPP_DATA = { PROJECTS: [], ASSETS: [], RECORDERS: [], JOBS: [], NODES: [], USERS: [], BINS: [], COMMENTS: [], ME: null, }; async function apiFetch(path, opts = {}) { const method = (opts.method || 'GET').toUpperCase(); const headers = { ...(opts.headers || {}), 'Content-Type': 'application/json', }; if (method !== 'GET' && method !== 'HEAD') headers['X-Requested-With'] = 'dragonflight-ui'; const res = await fetch(API + path, { credentials: 'include', ...opts, headers, }); // 401: hand off to AuthGate, which will re-render Login (no full-page reload). if (res.status === 401) { if (window.AuthGate && typeof window.AuthGate.bounce === 'function') { window.AuthGate.bounce('apiFetch saw 401 on ' + path); } throw new Error('Unauthenticated'); } if (!res.ok) throw new Error(res.status + ' ' + res.statusText); return res.json(); } function fmtDuration(ms) { if (!ms) return '·'; const s = Math.round(ms / 1000); const h = Math.floor(s / 3600); const m = Math.floor((s % 3600) / 60); const sec = s % 60; if (h > 0) return h + ':' + String(m).padStart(2, '0') + ':' + String(sec).padStart(2, '0'); return m + ':' + String(sec).padStart(2, '0'); } function fmtSize(bytes) { if (!bytes) return '·'; if (bytes >= 1e12) return (bytes / 1e12).toFixed(1) + ' TB'; if (bytes >= 1e9) return (bytes / 1e9).toFixed(1) + ' GB'; if (bytes >= 1e6) return Math.round(bytes / 1e6) + ' MB'; return Math.round(bytes / 1e3) + ' KB'; } function fmtRelative(iso) { if (!iso) return '·'; const diff = (Date.now() - new Date(iso)) / 1000; if (diff < 60) return 'just now'; if (diff < 3600) return Math.floor(diff / 60) + 'm ago'; if (diff < 86400) return Math.floor(diff / 3600) + 'h ago'; return Math.floor(diff / 86400) + 'd ago'; } const PROJECT_COLORS = ['#5B7CFA', '#2DD4A8', '#FF5B5B', '#F5A623', '#B57CFA', '#6B7280']; window.PROJECT_COLORS = PROJECT_COLORS; function normalizeAsset(a, projectMap) { return { ...a, name: a.display_name || a.filename || 'Untitled', type: a.media_type || 'video', duration: fmtDuration(a.duration_ms), size: fmtSize(a.file_size), res: a.resolution || '·', updated: fmtRelative(a.updated_at), project: (projectMap && projectMap[a.project_id]) || '', comments: 0, progress: 0, tc: a.start_tc || null, seed: a.id ? Array.from(a.id.replace(/-/g, '')).reduce((acc, c) => acc + c.charCodeAt(0), 0) % 6 : 1, }; } function normalizeRecorder(r) { let elapsed = '·'; if (r.status === 'recording' && r.started_at) { const s = Math.floor((Date.now() - new Date(r.started_at)) / 1000); elapsed = String(Math.floor(s / 3600)).padStart(2, '0') + ':' + String(Math.floor((s % 3600) / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0'); } const cfg = r.source_config || {}; return { ...r, source: r.source_type || '·', url: cfg.url || cfg.address || cfg.srt_url || cfg.rtmp_url || r.source_type || '·', codec: r.recording_codec || '·', res: r.recording_resolution || '·', node: r.node_id ? r.node_id.slice(0, 8) : 'primary', elapsed, bitrate: '·', health: 100, audio: false, }; } function normalizeJob(j) { const statusMap = { waiting: 'queued', active: 'running', completed: 'done', failed: 'failed' }; const kindMap = { proxy: 'Proxy', thumbnail: 'Thumbnail', conform: 'Conform', transcode: 'Transcode' }; const meta = j.metadata || {}; return { ...j, status: statusMap[j.status] || j.status, kind: kindMap[j.type] || j.type || 'Job', asset: j.asset_name || meta.filename || '·', eta: '·', node: meta.node || '·', priority: meta.priority || 'normal', error: j.error || null, progress: j.progress || 0, }; } async function refreshAssets() { const raw = await apiFetch('/assets?limit=500'); const list = Array.isArray(raw) ? raw : (raw.assets || []); const projectMap = {}; (window.ZAMPP_DATA.PROJECTS || []).forEach(function(p) { projectMap[p.id] = p.name; }); const normalized = list.map(function(a) { return normalizeAsset(a, projectMap); }); window.ZAMPP_DATA.ASSETS = normalized; if (window.ZAMPP_DATA.PROJECTS && window.ZAMPP_DATA.PROJECTS.length) { const counts = {}; list.forEach(function(a) { if (a.project_id) counts[a.project_id] = (counts[a.project_id] || 0) + 1; }); window.ZAMPP_DATA.PROJECTS = window.ZAMPP_DATA.PROJECTS.map(function(p) { return { ...p, assets: counts[p.id] || 0 }; }); } return normalized; } async function loadData() { const [projectsR, assetsR, recordersR, jobsR, clusterR, usersR, binsR, meR] = await Promise.allSettled([ apiFetch('/projects'), apiFetch('/assets?limit=500'), apiFetch('/recorders'), apiFetch('/jobs'), apiFetch('/cluster'), apiFetch('/users'), apiFetch('/bins'), apiFetch('/auth/me'), ]); const projectMap = {}; if (projectsR.status === 'fulfilled') { window.ZAMPP_DATA.PROJECTS = (projectsR.value || []).map((p, i) => ({ ...p, color: PROJECT_COLORS[i % PROJECT_COLORS.length], assets: 0, updated: fmtRelative(p.updated_at), })); window.ZAMPP_DATA.PROJECTS.forEach(p => { projectMap[p.id] = p.name; }); } if (assetsR.status === 'fulfilled') { const raw = assetsR.value; const list = Array.isArray(raw) ? raw : (raw.assets || []); window.ZAMPP_DATA.ASSETS = list.map(a => normalizeAsset(a, projectMap)); const counts = {}; list.forEach(a => { if (a.project_id) counts[a.project_id] = (counts[a.project_id] || 0) + 1; }); window.ZAMPP_DATA.PROJECTS = window.ZAMPP_DATA.PROJECTS.map(p => ({ ...p, assets: counts[p.id] || 0 })); } if (recordersR.status === 'fulfilled') { window.ZAMPP_DATA.RECORDERS = (recordersR.value || []).map(normalizeRecorder); } if (jobsR.status === 'fulfilled') { window.ZAMPP_DATA.JOBS = (jobsR.value || []).map(normalizeJob); } if (clusterR.status === 'fulfilled') { window.ZAMPP_DATA.NODES = clusterR.value || []; } if (usersR.status === 'fulfilled') { window.ZAMPP_DATA.USERS = (usersR.value || []).map(u => ({ ...u, name: u.display_name || u.username || u.email || 'Unknown', initials: (u.display_name || u.username || '?').slice(0, 2).toUpperCase(), role: u.role || 'viewer', joined: fmtRelative(u.created_at), lastSeen: fmtRelative(u.last_seen || u.updated_at), })); } if (binsR.status === 'fulfilled') { window.ZAMPP_DATA.BINS = (binsR.value || []).map(b => ({ ...b, count: b.asset_count || 0, icon: b.type || 'grid', })); } if (meR.status === 'fulfilled' && meR.value) { const me = meR.value; const label = me.display_name || me.username || 'User'; window.ZAMPP_DATA.ME = { id: me.id, username: me.username, name: label, initials: label.slice(0, 2).toUpperCase(), role: me.role || 'viewer', // True when the server returned a synthetic user (AUTH_ENABLED=false). // Surfaced as a small "auth off" hint in the sidebar so the operator // understands why the corner shows the OS user instead of a login. synthetic: !!me.synthetic, }; } } // ── Sequence API ──────────────────────────────────────────────── async function getSequences(projectId) { return apiFetch('/sequences?project_id=' + projectId); } async function createSequence(data) { return apiFetch('/sequences', { method: 'POST', body: JSON.stringify(data), }); } async function getSequence(sequenceId) { return apiFetch('/sequences/' + sequenceId); } async function updateSequence(sequenceId, data) { return apiFetch('/sequences/' + sequenceId, { method: 'PUT', body: JSON.stringify(data), }); } async function deleteSequence(sequenceId) { return apiFetch('/sequences/' + sequenceId, { method: 'DELETE' }); } async function syncSequenceClips(sequenceId, clips) { return apiFetch('/sequences/' + sequenceId + '/clips', { method: 'PUT', body: JSON.stringify(clips), }); } async function exportSequenceEDL(sequenceId, filename) { const res = await fetch(API + '/sequences/' + sequenceId + '/export/edl', { method: 'POST', credentials: 'include', headers: { 'X-Requested-With': 'dragonflight-ui' }, }); if (!res.ok) throw new Error('EDL export failed: ' + res.status); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename || 'sequence.edl'; a.click(); URL.revokeObjectURL(url); } window.ZAMPP_API = { fetch: apiFetch, loadData, refreshAssets, fmtDuration, fmtSize, fmtRelative, getSequences, createSequence, getSequence, updateSequence, deleteSequence, syncSequenceClips, exportSequenceEDL, }; // Library re-renders after mutations: expose normalizeAsset so the screen // can re-fetch /assets and produce rows with the same shape as the boot load. window.normalizeAsset = normalizeAsset;