1	/**
2	 * Wild Dragon MAM - Premiere Pro Panel
3	 * Main JavaScript file for the CEP panel
4	 */
5
6	// Adobe CEP interface — must be instantiated before any host (ExtendScript) calls
7	const csInterface = new CSInterface();
8
9	// ============================================================================
10	// State Management
11	// ============================================================================
12
13	const state = {
14	    serverUrl:        localStorage.getItem('mam_server_url') || 'http://localhost:7434',
15	    isConnected:      false,
16	    isConnecting:     false,
17	    selectedAsset:    null,
18	    assets:           [],
19	    projects:         [],
20	    selectedProject:  'all',
21	    searchQuery:      '',
22	    currentPage:      0,
23	    pageSize:         50,
24	    totalAssets:      0,
25	    downloadProgress: 0,
26	    isDownloading:    false,
27	    thumbCache:       {}, // assetId -> signed URL
28	    // Maps tempFilePath → { assetId, displayName } for timeline export resolution.
29	    // Also stores 'name:filename.mp4' → same as a fuzzy fallback.
30	    importedAssets:   JSON.parse(localStorage.getItem('mam_imported_assets') || '{}'),
31	    exportPanelVisible:  false,
32	    currentSequenceName: '',
33	};
34
35	// ============================================================================
36	// DOM Elements
37	// ============================================================================
38
39	let elements = {};
40
41	function initDOMElements() {
42	    elements = {
43	        serverUrlInput:    document.getElementById('server-url'),
44	        connectBtn:        document.getElementById('connect-btn'),
45	        statusIndicator:   document.getElementById('status-indicator'),
46	        searchInput:       document.getElementById('search-input'),
47	        projectFilter:     document.getElementById('project-filter'),
48	        assetGrid:         document.getElementById('asset-grid'),
49	        emptyState:        document.getElementById('empty-state'),
50	        detailsPanel:      document.getElementById('details-panel'),
51	        detailsFilename:   document.getElementById('details-filename'),
52	        detailsCodec:      document.getElementById('details-codec'),
53	        detailsResolution: document.getElementById('details-resolution'),
54	        detailsFps:        document.getElementById('details-fps'),
55	        detailsDuration:   document.getElementById('details-duration'),
56	        detailsSize:       document.getElementById('details-size'),
57	        detailsTags:       document.getElementById('details-tags'),
58	        importBtn:         document.getElementById('import-btn'),
59	        importHiresBtn:    document.getElementById('import-hires-btn'),
60	        importAllBtn:      document.getElementById('import-all-btn'),
61	        exportTimelineBtn: document.getElementById('export-timeline-btn'),
62	        progressContainer: document.getElementById('progress-container'),
63	        progressLabel:     document.getElementById('progress-label'),
64	        progressFill:      document.getElementById('progress-fill'),
65	        // Export panel
66	        exportPanel:       document.getElementById('export-panel'),
67	        exportSeqName:     document.getElementById('export-seq-name'),
68	        exportProjSelect:  document.getElementById('export-proj-select'),
69	        exportClipInfo:    document.getElementById('export-clip-info'),
70	        exportConfirmBtn:  document.getElementById('export-confirm-btn'),
71	        exportCancelBtn:   document.getElementById('export-cancel-btn'),
72	        // Sequence info bar
73	        seqInfoBar:        document.getElementById('seq-info-bar'),
74	        seqInfoName:       document.getElementById('seq-info-name'),
75	        seqRefreshBtn:     document.getElementById('seq-refresh-btn'),
76	    };
77	}
78
79	// ============================================================================
80	// Thumbnail Lazy Loading (IntersectionObserver)
81	// ============================================================================
82
83	const thumbObserver = new IntersectionObserver((entries) => {
84	    entries.forEach((entry) => {
85	        if (entry.isIntersecting) {
86	            const img     = entry.target;
87	            const assetId = img.dataset.assetId;
88	            if (assetId && !img.dataset.loaded) loadThumbnail(img, assetId);
89	            thumbObserver.unobserve(img);
90	        }
91	    });
92	}, { rootMargin: '100px' });
93
94	async function loadThumbnail(img, assetId) {
95	    if (state.thumbCache[assetId]) {
96	        img.src = state.thumbCache[assetId];
97	        img.dataset.loaded = '1';
98	        return;
99	    }
100	    try {
101	        const r = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/thumbnail`, {
102	            headers: { Accept: 'application/json' },
103	        });
104	        if (!r.ok) return;
105	        const { url } = await r.json();
106	        if (!url) return;
107	        state.thumbCache[assetId] = url;
108	        img.src = url;
109	        img.dataset.loaded = '1';
110	    } catch (_) {
111	        // thumbnail unavailable — leave placeholder
112	    }
113	}
114
115	// ============================================================================
116	// Initialization
117	// ============================================================================
118
119	document.addEventListener('DOMContentLoaded', () => {
120	    initDOMElements();
121	    setupEventListeners();
122	    restoreSettings();
123	    logMessage('Wild Dragon MAM panel initialized');
124	});
125
126	function setupEventListeners() {
127	    elements.serverUrlInput.addEventListener('change', (e) => {
128	        state.serverUrl = e.target.value.trim().replace(/\/$/, '');
129	        localStorage.setItem('mam_server_url', state.serverUrl);
130	        state.thumbCache = {};
131	    });
132	    elements.connectBtn.addEventListener('click', connectToServer);
133	    elements.searchInput.addEventListener('input', debounce(handleSearch, 300));
134	    elements.projectFilter.addEventListener('change', handleProjectFilter);
135	    elements.assetGrid.addEventListener('click', handleAssetClick);
136	    elements.importBtn.addEventListener('click', importSelectedAsset);
137	    elements.importHiresBtn.addEventListener('click', importSelectedAssetHires);
138	    elements.importAllBtn.addEventListener('click', importAllAssets);
139	    elements.exportTimelineBtn.addEventListener('click', startExportTimeline);
140	    elements.exportConfirmBtn.addEventListener('click', confirmExportTimeline);
141	    elements.exportCancelBtn.addEventListener('click', cancelExportTimeline);
142	    elements.seqRefreshBtn.addEventListener('click', refreshCurrentSequenceInfo);
143	}
144
145	function restoreSettings() {
146	    elements.serverUrlInput.value = state.serverUrl;
147	}
148
149	// ============================================================================
150	// Server Connection
151	// ============================================================================
152
153	async function connectToServer() {
154	    if (state.isConnecting) return;
155
156	    state.isConnecting = true;
157	    updateConnectionStatus('connecting');
158	    elements.connectBtn.disabled = true;
159
160	    try {
161	        const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
162	            method: 'GET',
163	            headers: { Accept: 'application/json' },
164	            credentials: 'include',
165	        });
166
167	        if (response.ok) {
168	            state.isConnected = true;
169	            updateConnectionStatus('connected');
170	            elements.connectBtn.textContent = 'Reconnect';
171	            logMessage('Connected to Wild Dragon MAM');
172
173	            const projectData = await response.json();
174	            await fetchProjects(projectData);
175	            await fetchAssets();
176	            refreshCurrentSequenceInfo();
177	        } else {
178	            throw new Error(`HTTP ${response.status}`);
179	        }
180	    } catch (error) {
181	        console.error('Connection error:', error);
182	        state.isConnected = false;
183	        updateConnectionStatus('disconnected');
184	        elements.connectBtn.textContent = 'Connect';
185	        showErrorMessage(`Failed to connect: ${error.message}`);
186	    } finally {
187	        state.isConnecting = false;
188	        elements.connectBtn.disabled = false;
189	    }
190	}
191
192	function updateConnectionStatus(status) {
193	    const indicator = elements.statusIndicator;
194	    indicator.classList.remove('connected', 'connecting');
195	    if (status === 'connected')       indicator.classList.add('connected');
196	    else if (status === 'connecting') indicator.classList.add('connecting');
197	}
198
199	// ============================================================================
200	// API Calls
201	// ============================================================================
202
203	async function fetchProjects(preloadedData) {
204	    try {
205	        let projects;
206	        if (preloadedData) {
207	            projects = Array.isArray(preloadedData) ? preloadedData : [];
208	        } else {
209	            const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
210	                headers: { Accept: 'application/json' },
211	                credentials: 'include',
212	            });
213	            if (!response.ok) throw new Error(`HTTP ${response.status}`);
214	            const data = await response.json();
215	            projects = Array.isArray(data) ? data : [];
216	        }
217
218	        state.projects = projects;
219
220	        const savedProject = state.selectedProject;
221	        elements.projectFilter.innerHTML = '<option value="all">All Projects</option>';
222	        state.projects.forEach((p) => {
223	            const opt = document.createElement('option');
224	            opt.value       = p.id;
225	            opt.textContent = p.name;
226	            elements.projectFilter.appendChild(opt);
227	        });
228	        elements.projectFilter.value = savedProject;
229
230	        // Also populate the export panel project selector
231	        populateExportProjectSelect();
232
233	        logMessage(`Loaded ${state.projects.length} projects`);
234	    } catch (error) {
235	        console.error('Error fetching projects:', error);
236	    }
237	}
238
239	async function fetchAssets(page = 0) {
240	    if (!state.isConnected) return;
241	    try {
242	        const params = new URLSearchParams({
243	            offset: page * state.pageSize,
244	            limit:  state.pageSize,
245	        });
246	        if (state.searchQuery)               params.append('search',     state.searchQuery);
247	        if (state.selectedProject !== 'all') params.append('project_id', state.selectedProject);
248
249	        const response = await fetch(
250	            `${state.serverUrl}/api/v1/assets?${params.toString()}`,
251	            { headers: { Accept: 'application/json' }, credentials: 'include' }
252	        );
253	        if (!response.ok) throw new Error(`HTTP ${response.status}`);
254
255	        const data        = await response.json();
256	        state.assets      = data.assets || [];
257	        state.totalAssets = data.total  || 0;
258	        state.currentPage = page;
259
260	        renderAssets();
261	        logMessage(`Loaded ${state.assets.length} assets`);
262	    } catch (error) {
263	        console.error('Error fetching assets:', error);
264	        showErrorMessage('Failed to fetch assets');
265	    }
266	}
267
268	async function fetchAssetDetails(assetId) {
269	    if (!state.isConnected) return null;
270	    try {
271	        const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}`, {
272	            headers: { Accept: 'application/json' },
273	            credentials: 'include',
274	        });
275	        if (!response.ok) throw new Error(`HTTP ${response.status}`);
276	        return await response.json();
277	    } catch (error) {
278	        console.error('Error fetching asset details:', error);
279	        return null;
280	    }
281	}
282
283	/**
284	 * Returns the download URL for the proxy.  /stream returns a server-relative
285	 * path (/api/v1/assets/:id/video) designed for browser playback; we prepend
286	 * serverUrl so Node.js http.get() receives a valid absolute URL.
287	 */
288	async function getSignedDownloadUrl(assetId) {
289	    const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/stream`, {
290	        headers: { Accept: 'application/json' },
291	        credentials: 'include',
292	    });
293	    if (!response.ok) throw new Error(`HTTP ${response.status} from /stream`);
294	    const { url } = await response.json();
295	    if (!url) throw new Error('Stream endpoint returned no URL');
296	    return url.startsWith('/') ? state.serverUrl + url : url;
297	}
298
299	/**
300	 * Returns presigned S3 URL + filename/ext/file_size for the hi-res original.
301	 * GET /api/v1/assets/:id/hires
302	 */
303	async function getHiresDownloadInfo(assetId) {
304	    const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/hires`, {
305	        headers: { Accept: 'application/json' },
306	        credentials: 'include',
307	    });
308	    if (!response.ok) throw new Error(`HTTP ${response.status} from /hires`);
309	    const data = await response.json();
310	    if (!data.url) throw new Error('Hi-res endpoint returned no URL');
311	    return data;
312	}
313
314	// ============================================================================
315	// UI Rendering
316	// ============================================================================
317
318	function renderAssets() {
319	    elements.assetGrid.innerHTML = '';
320
321	    if (state.assets.length === 0) {
322	        elements.emptyState.style.display = 'flex';
323	        return;
324	    }
325
326	    elements.emptyState.style.display = 'none';
327	    state.assets.forEach((asset) => {
328	        elements.assetGrid.appendChild(createAssetCard(asset));
329	    });
330	}
331
332	function createAssetCard(asset) {
333	    const card = document.createElement('div');
334	    card.className = 'asset-card';
335	    if (state.selectedAsset && state.selectedAsset.id === asset.id) {
336	        card.classList.add('selected');
337	    }
338	    card.dataset.assetId = asset.id;
339
340	    // Thumbnail — lazy loaded via IntersectionObserver
341	    const thumbnail = document.createElement('div');
342	    thumbnail.className = 'asset-thumbnail';
343
344	    const img = document.createElement('img');
345	    img.dataset.assetId = asset.id;
346	    img.alt             = escapeHtml(asset.display_name || asset.filename || '');
347	    img.style.cssText   = 'width:100%;height:100%;object-fit:cover;display:block;';
348	    img.onerror         = () => { img.style.display = 'none'; };
349	    thumbnail.appendChild(img);
350	    thumbObserver.observe(img);
351	    card.appendChild(thumbnail);
352
353	    // Info row
354	    const info = document.createElement('div');
355	    info.className = 'asset-info';
356
357	    const name       = asset.display_name || asset.filename || 'Untitled';
358	    const filenameEl = document.createElement('div');
359	    filenameEl.className   = 'asset-filename';
360	    filenameEl.title       = name;
361	    filenameEl.textContent = name;
362	    info.appendChild(filenameEl);
363
364	    const durationSec = asset.duration_ms ? asset.duration_ms / 1000 : null;
365	    const codec = asset.codec || asset.media_type || 'video';
366	    const meta  = document.createElement('div');
367	    meta.className   = 'asset-meta';
368	    meta.innerHTML   = [
369	        '' + (durationSec ? formatDuration(durationSec) : 'N/A') + '',
370	        '' + escapeHtml(codec.toUpperCase()) + '',
371	    ].join('');
372	    info.appendChild(meta);
373
374	    const statusStr   = asset.status || 'ready';
375	    const statusBadge = document.createElement('div');
376	    statusBadge.className   = 'asset-status-badge status-badge ' + statusStr;
377	    statusBadge.textContent = statusStr.toUpperCase();
378	    info.appendChild(statusBadge);
379
380	    card.appendChild(info);
381	    return card;
382	}
383
384	function showAssetDetails(asset) {
385	    state.selectedAsset = asset;
386	    elements.detailsPanel.classList.remove('hidden');
387
388	    elements.detailsFilename.textContent   = asset.display_name || asset.filename;
389	    elements.detailsCodec.textContent      = asset.codec || asset.media_type || 'Unknown';
390	    elements.detailsResolution.textContent = asset.resolution || 'N/A';
391	    elements.detailsFps.textContent        = asset.fps ? asset.fps + ' fps' : 'N/A';
392	    elements.detailsDuration.textContent   = asset.duration_ms
393	        ? formatDuration(asset.duration_ms / 1000)
394	        : 'N/A';
395	    elements.detailsSize.textContent       = asset.file_size
396	        ? formatFileSize(asset.file_size)
397	        : 'N/A';
398
399	    elements.detailsTags.innerHTML = '';
400	    const tags = asset.tags || [];
401	    if (tags.length > 0) {
402	        tags.forEach((tag) => {
403	            const el       = document.createElement('span');
404	            el.className   = 'tag';
405	            el.textContent = tag;
406	            elements.detailsTags.appendChild(el);
407	        });
408	    } else {
409	        elements.detailsTags.innerHTML =
410	            'No tags';
411	    }
412
413	    elements.importBtn.disabled      = false;
414	    elements.importHiresBtn.disabled = false;
415	}
416
417	function hideAssetDetails() {
418	    state.selectedAsset = null;
419	    elements.detailsPanel.classList.add('hidden');
420	    elements.importBtn.disabled      = true;
421	    elements.importHiresBtn.disabled = true;
422	}
423
424	// ============================================================================
425	// Search and Filter
426	// ============================================================================
427
428	function handleSearch(e) {
429	    state.searchQuery = e.target.value;
430	    state.currentPage = 0;
431	    fetchAssets();
432	}
433
434	function handleProjectFilter(e) {
435	    state.selectedProject = e.target.value;
436	    state.currentPage     = 0;
437	    fetchAssets();
438	}
439
440	// ============================================================================
441	// Import — Proxy
442	// ============================================================================
443
444	async function importSelectedAsset() {
445	    if (!state.selectedAsset) {
446	        showErrorMessage('No asset selected');
447	        return;
448	    }
449	    await importAsset(state.selectedAsset);
450	}
451
452	async function importAllAssets() {
453	    if (state.assets.length === 0) {
454	        showErrorMessage('No assets to import');
455	        return;
456	    }
457	    for (const asset of state.assets) {
458	        await importAsset(asset);
459	    }
460	    showSuccessMessage('All assets imported');
461	}
462
463	async function importAsset(asset) {
464	    try {
465	        elements.importBtn.disabled = true;
466
467	        // 1. Get proxy URL (absolute — /stream returns a relative path)
468	        showProgress('Getting download link...', 5);
469	        const url = await getSignedDownloadUrl(asset.id);
470
471	        // 2. Download proxy to OS temp dir via Node.js
472	        const safeName = sanitizeFilename(
473	            (asset.display_name || asset.filename || asset.id) + '.mp4'
474	        );
475	        showProgress('Downloading ' + safeName + '...', 10);
476	        const filePath = await downloadFile(url, safeName);
477
478	        // 3. Hand the local path to Premiere Pro
479	        showProgress('Importing into Premiere Pro...', 85);
480	        await importFileToPremiereProject(filePath);
481
482	        // 4. Store path → assetId so timeline export can resolve this clip
483	        saveImportMapping(filePath, safeName, asset);
484
485	        hideProgress();
486	        showSuccessMessage('Imported: ' + safeName);
487	    } catch (error) {
488	        console.error('Import error:', error);
489	        hideProgress();
490	        showErrorMessage('Import failed: ' + error.message);
491	    } finally {
492	        elements.importBtn.disabled = !state.selectedAsset;
493	    }
494	}
495
496	// ============================================================================
497	// Import — Hi-Res Original
498	// ============================================================================
499
500	async function importSelectedAssetHires() {
501	    if (!state.selectedAsset) {
502	        showErrorMessage('No asset selected');
503	        return;
504	    }
505	    await importAssetHires(state.selectedAsset);
506	}
507
508	async function importAssetHires(asset) {
509	    try {
510	        elements.importHiresBtn.disabled = true;
511
512	        // 1. Get hi-res presigned URL and metadata
513	        showProgress('Getting hi-res link...', 5);
514	        let hiresInfo;
515	        try {
516	            hiresInfo = await getHiresDownloadInfo(asset.id);
517	        } catch (err) {
518	            throw new Error('No hi-res source: ' + err.message);
519	        }
520
521	        const sizeNote = hiresInfo.file_size ? ' (' + formatFileSize(hiresInfo.file_size) + ')' : '';
522	        const safeName = sanitizeFilename(
523	            hiresInfo.filename || (asset.display_name || asset.id) + '.mxf'
524	        );
525
526	        // 2. Download hi-res file to OS temp dir
527	        showProgress('Downloading hi-res' + sizeNote + '...', 10);
528	        const filePath = await downloadFile(hiresInfo.url, safeName);
529
530	        // 3. Import into Premiere Pro
531	        showProgress('Importing hi-res into Premiere Pro...', 85);
532	        await importFileToPremiereProject(filePath);
533
534	        // 4. Map this path to the same asset ID (hi-res round-trips to same MAM record)
535	        saveImportMapping(filePath, safeName, asset);
536
537	        hideProgress();
538	        showSuccessMessage('Hi-res imported: ' + safeName);
539	    } catch (error) {
540	        console.error('Hi-res import error:', error);
541	        hideProgress();
542	        showErrorMessage('Hi-res import failed: ' + error.message);
543	    } finally {
544	        elements.importHiresBtn.disabled = !state.selectedAsset;
545	    }
546	}
547
548	/**
549	 * Persists two keys per import:
550	 *   - exact temp path   → so Premiere getMediaPath() matches on the same machine
551	 *   - 'name:filename'   → fuzzy fallback across sessions / machines
552	 */
553	function saveImportMapping(filePath, safeName, asset) {
554	    const entry = {
555	        assetId:     asset.id,
556	        displayName: asset.display_name || asset.filename || '',
557	    };
558	    state.importedAssets[filePath]          = entry;
559	    state.importedAssets['name:' + safeName] = entry;
560	    try {
561	        localStorage.setItem('mam_imported_assets', JSON.stringify(state.importedAssets));
562	    } catch (_) {}
563	}
564
565	// ============================================================================
566	// Active Sequence Info Bar
567	// ============================================================================
568
569	function refreshCurrentSequenceInfo() {
570	    csInterface.evalScript('getActiveSequence()', function (resultStr) {
571	        try {
572	            var parsed = JSON.parse(resultStr);
573	            state.currentSequenceName = parsed.sequenceName || '';
574	            if (state.currentSequenceName) {
575	                elements.seqInfoName.textContent = state.currentSequenceName;
576	                elements.seqInfoBar.classList.remove('hidden');
577	            } else {
578	                elements.seqInfoBar.classList.add('hidden');
579	            }
580	        } catch (e) {
581	            elements.seqInfoBar.classList.add('hidden');
582	        }
583	    });
584	}
585
586	// ============================================================================
587	// Export Panel UI
588	// ============================================================================
589
590	function populateExportProjectSelect() {
591	    elements.exportProjSelect.innerHTML = '<option value="">— Select project —</option>';
592	    state.projects.forEach(function (p) {
593	        var opt = document.createElement('option');
594	        opt.value       = p.id;
595	        opt.textContent = p.name;
596	        if (p.id === state.selectedProject) opt.selected = true;
597	        elements.exportProjSelect.appendChild(opt);
598	    });
599	}
600
601	function showExportPanel(timelineData) {
602	    elements.exportSeqName.value = timelineData.sequenceName || 'Sequence 1';
603	    populateExportProjectSelect();
604
605	    var totalClips  = (timelineData.clips || []).length;
606	    var matchedClips = resolveClipsToAssets(timelineData.clips || []).filter(function (c) {
607	        return c.asset_id;
608	    }).length;
609
610	    elements.exportClipInfo.textContent =
611	        matchedClips + ' of ' + totalClips + ' clip(s) matched to MAM assets';
612	    elements.exportClipInfo.style.color =
613	        matchedClips === 0 ? 'var(--error)' : 'var(--text-secondary)';
614
615	    // Stash the full timeline data for confirmExportTimeline
616	    elements.exportPanel.dataset.timelineJson = JSON.stringify(timelineData);
617	    elements.exportPanel.classList.remove('hidden');
618	    state.exportPanelVisible = true;
619	    elements.exportSeqName.focus();
620	}
621
622	function hideExportPanel() {
623	    elements.exportPanel.classList.add('hidden');
624	    state.exportPanelVisible = false;
625	}
626
627	// ============================================================================
628	// Timeline Export — Premiere → MAM
629	// ============================================================================
630
631	/**
632	 * Step 1 of 2: read the Premiere timeline then show the export confirmation
633	 * panel with pre-filled sequence name and matched clip count.
634	 */
635	async function startExportTimeline() {
636	    if (!state.isConnected) {
637	        showErrorMessage('Connect to MAM first');
638	        return;
639	    }
640
641	    // Toggle: clicking again closes the panel
642	    if (state.exportPanelVisible) {
643	        hideExportPanel();
644	        return;
645	    }
646
647	    showProgress('Reading Premiere timeline...', 20);
648
649	    const timelineData = await new Promise(function (resolve) {
650	        csInterface.evalScript('exportTimelineData()', function (resultStr) {
651	            try { resolve(JSON.parse(resultStr)); }
652	            catch (e) { resolve({ success: false, message: 'Parse error: ' + e.message, clips: [] }); }
653	        });
654	    });
655
656	    hideProgress();
657
658	    if (!timelineData.success) {
659	        showErrorMessage('Timeline read failed: ' + (timelineData.message || 'unknown error'));
660	        return;
661	    }
662
663	    if (!timelineData.clips || timelineData.clips.length === 0) {
664	        showErrorMessage('No clips found in the active sequence');
665	        return;
666	    }
667
668	    showExportPanel(timelineData);
669	}
670
671	/**
672	 * Step 2 of 2: resolve clip paths to MAM asset IDs, upsert the sequence,
673	 * then PUT the clip array to /api/v1/sequences/:id/clips.
674	 */
675	async function confirmExportTimeline() {
676	    var timelineData;
677	    try {
678	        timelineData = JSON.parse(elements.exportPanel.dataset.timelineJson || '{}');
679	    } catch (e) {
680	        showErrorMessage('Invalid timeline data — try reading again');
681	        return;
682	    }
683
684	    var seqName   = (elements.exportSeqName.value || '').trim() || 'Sequence 1';
685	    var projectId = elements.exportProjSelect.value;
686	    if (!projectId) {
687	        showErrorMessage('Select a target project');
688	        return;
689	    }
690
691	    var resolved = resolveClipsToAssets(timelineData.clips || []);
692	    var matched  = resolved.filter(function (c) { return c.asset_id; });
693
694	    if (matched.length === 0) {
695	        hideExportPanel();
696	        showErrorMessage('No clips matched MAM assets — import proxies or hi-res first');
697	        return;
698	    }
699
700	    hideExportPanel();
701	    showProgress('Creating sequence in MAM...', 20);
702
703	    try {
704	        var seqId = await upsertSequence(
705	            projectId,
706	            seqName,
707	            timelineData.frameRate || 59.94,
708	            timelineData.width     || 1920,
709	            timelineData.height    || 1080
710	        );
711
712	        showProgress('Writing ' + matched.length + ' clip(s)...', 60);
713
714	        var clipPayload = matched.map(function (c) {
715	            return {
716	                asset_id:            c.asset_id,
717	                track:               c.trackIndex,
718	                timeline_in_frames:  c.timelineInFrames,
719	                timeline_out_frames: c.timelineOutFrames,
720	                source_in_frames:    c.sourceInFrames,
721	                source_out_frames:   c.sourceOutFrames,
722	            };
723	        });
724
725	        var clipsRes = await fetch(
726	            state.serverUrl + '/api/v1/sequences/' + seqId + '/clips',
727	            {
728	                method:      'PUT',
729	                headers:     { 'Content-Type': 'application/json', Accept: 'application/json' },
730	                credentials: 'include',
731	                body:        JSON.stringify(clipPayload),
732	            }
733	        );
734	        if (!clipsRes.ok) throw new Error('Clip push failed: HTTP ' + clipsRes.status);
735
736	        hideProgress();
737	        showSuccessMessage('Timeline pushed: ' + matched.length + ' clip(s) → "' + seqName + '"');
738
739	        var skipped = resolved.length - matched.length;
740	        if (skipped > 0) {
741	            _showFlash(skipped + ' clip(s) skipped — not in MAM (import them first)', 'info-message');
742	        }
743	    } catch (err) {
744	        hideProgress();
745	        showErrorMessage('Export failed: ' + err.message);
746	    }
747	}
748
749	function cancelExportTimeline() {
750	    hideExportPanel();
751	}
752
753	/**
754	 * Maps Premiere clip file paths back to MAM asset IDs.
755	 * Lookup order: exact temp path → name-based fallback.
756	 */
757	function resolveClipsToAssets(clips) {
758	    return clips.map(function (clip) {
759	        var entry = state.importedAssets[clip.filePath];
760	        if (!entry) entry = state.importedAssets['name:' + clip.fileName];
761	        return Object.assign({}, clip, { asset_id: entry ? entry.assetId : null });
762	    });
763	}
764
765	/**
766	 * Finds an existing sequence by name in the target project; creates it if
767	 * absent.  Updates frame_rate/width/height on the existing record.
768	 * Returns the sequence ID.
769	 */
770	async function upsertSequence(projectId, name, frameRate, width, height) {
771	    var listRes = await fetch(
772	        state.serverUrl + '/api/v1/sequences?project_id=' + encodeURIComponent(projectId),
773	        { headers: { Accept: 'application/json' }, credentials: 'include' }
774	    );
775	    if (listRes.ok) {
776	        var seqs     = await listRes.json();
777	        var existing = seqs.find(function (s) { return s.name === name; });
778	        if (existing) {
779	            await fetch(state.serverUrl + '/api/v1/sequences/' + existing.id, {
780	                method:      'PUT',
781	                headers:     { 'Content-Type': 'application/json', Accept: 'application/json' },
782	                credentials: 'include',
783	                body:        JSON.stringify({ frame_rate: frameRate, width: width, height: height }),
784	            });
785	            return existing.id;
786	        }
787	    }
788
789	    var createRes = await fetch(state.serverUrl + '/api/v1/sequences', {
790	        method:      'POST',
791	        headers:     { 'Content-Type': 'application/json', Accept: 'application/json' },
792	        credentials: 'include',
793	        body:        JSON.stringify({
794	            project_id: projectId,
795	            name:       name,
796	            frame_rate: frameRate,
797	            width:      width,
798	            height:     height,
799	        }),
800	    });
801	    if (!createRes.ok) throw new Error('Failed to create sequence: HTTP ' + createRes.status);
802	    var seq = await createRes.json();
803	    return seq.id;
804	}
805
806	// ============================================================================
807	// UI Helpers
808	// ============================================================================
809
810	function handleAssetClick(e) {
811	    var card = e.target.closest('.asset-card');
812	    if (!card) return;
813
814	    document.querySelectorAll('.asset-card.selected').forEach(function (el) {
815	        el.classList.remove('selected');
816	    });
817	    card.classList.add('selected');
818
819	    var assetId = card.dataset.assetId;
820	    var asset   = state.assets.find(function (a) { return a.id === assetId; });
821	    if (asset) showAssetDetails(asset);
822	}
823
824	function showProgress(label, percent) {
825	    elements.progressContainer.classList.add('visible');
826	    elements.progressLabel.textContent = label;
827	    state.downloadProgress = percent;
828	    updateProgressUI();
829	}
830
831	function hideProgress() {
832	    elements.progressContainer.classList.remove('visible');
833	    state.downloadProgress = 0;
834	    updateProgressUI();
835	}
836
837	function updateProgressUI() {
838	    elements.progressFill.style.width = state.downloadProgress + '%';
839	}
840
841	function showErrorMessage(message) {
842	    _showFlash(message, 'error-message');
843	}
844
845	function showSuccessMessage(message) {
846	    _showFlash(message, 'success-message');
847	}
848
849	function _showFlash(message, className) {
850	    var el         = document.createElement('div');
851	    el.className   = className;
852	    el.textContent = message;
853	    var anchor     = document.querySelector('.search-filter-area');
854	    anchor.insertBefore(el, anchor.firstChild);
855	    setTimeout(function () { el.remove(); }, 5000);
856	}
857
858	function logMessage(message) {
859	    console.log('[MAM Panel] ' + message);
860	}
861
862	// ============================================================================
863	// File Download and Premiere Import (Node.js / ExtendScript)
864	// ============================================================================
865
866	/**
867	 * Downloads a remote URL to a local temp file using Node.js http/https.
868	 * Requires --enable-nodejs in the CEP manifest.
869	 */
870	function downloadFile(url, filename) {
871	    return new Promise(function (resolve, reject) {
872	        try {
873	            var https = require('https');
874	            var http  = require('http');
875	            var fs    = require('fs');
876	            var path  = require('path');
877	            var os    = require('os');
878
879	            var tempPath = path.join(os.tmpdir(), filename);
880	            var file     = fs.createWriteStream(tempPath);
881	            var protocol = url.startsWith('https') ? https : http;
882
883	            protocol.get(url, function (res) {
884	                if (res.statusCode !== 200) {
885	                    file.close();
886	                    fs.unlink(tempPath, function () {});
887	                    reject(new Error('Download HTTP ' + res.statusCode));
888	                    return;
889	                }
890
891	                var total    = parseInt(res.headers['content-length'] || '0', 10);
892	                var received = 0;
893
894	                res.on('data', function (chunk) {
895	                    received += chunk.length;
896	                    if (total > 0) {
897	                        state.downloadProgress = 10 + (received / total) * 75;
898	                        updateProgressUI();
899	                    }
900	                });
901
902	                res.pipe(file);
903
904	                file.on('finish', function () {
905	                    file.close(function () { resolve(tempPath); });
906	                });
907
908	                file.on('error', function (err) {
909	                    fs.unlink(tempPath, function () {});
910	                    reject(err);
911	                });
912	            }).on('error', function (err) {
913	                fs.unlink(tempPath, function () {});
914	                reject(err);
915	            });
916	        } catch (err) {
917	            reject(new Error('Node.js unavailable for download: ' + err.message));
918	        }
919	    });
920	}
921
922	/**
923	 * Calls csInterface.evalScript to import a local file into the open Premiere project.
924	 */
925	function importFileToPremiereProject(filePath) {
926	    return new Promise(function (resolve, reject) {
927	        var safePath = filePath.replace(/\\/g, '\\\\');
928
929	        var script = [
930	            '(function() {',
931	            '  var result = { success: false, message: "" };',
932	            '  try {',
933	            '    if (!app.project) {',
934	            '      result.message = "No active Premiere Pro project";',
935	            '      return JSON.stringify(result);',
936	            '    }',
937	            '    var f = new File("' + safePath + '");',
938	            '    if (!f.exists) {',
939	            '      result.message = "File not found: ' + safePath + '";',
940	            '      return JSON.stringify(result);',
941	            '    }',
942	            '    app.project.importFiles(["' + safePath + '"]);',
943	            '    result.success = true;',
944	            '    result.message = "Imported successfully";',
945	            '  } catch (e) {',
946	            '    result.message = e.message;',
947	            '  }',
948	            '  return JSON.stringify(result);',
949	            '})();',
950	        ].join('\n');
951
952	        csInterface.evalScript(script, function (resultStr) {
953	            try {
954	                var parsed = JSON.parse(resultStr);
955	                if (parsed.success) resolve(parsed);
956	                else reject(new Error(parsed.message));
957	            } catch (e) {
958	                reject(new Error('ExtendScript error: ' + resultStr));
959	            }
960	        });
961	    });
962	}
963
964	// ============================================================================
965	// Utility Functions
966	// ============================================================================
967
968	function debounce(func, delay) {
969	    var timeout;
970	    return function () {
971	        var args = arguments;
972	        clearTimeout(timeout);
973	        timeout = setTimeout(function () { func.apply(null, args); }, delay);
974	    };
975	}
976
977	function escapeHtml(str) {
978	    return String(str)
979	        .replace(/&/g,  '&')
980	        .replace(/</g,  '<')
981	        .replace(/>/g,  '>')
982	        .replace(/"/g,  '"')
983	        .replace(/'/g,  ''');
984	}
985
986	function sanitizeFilename(name) {
987	    return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_');
988	}
989
990	function formatDuration(seconds) {
991	    if (!seconds) return 'N/A';
992	    var h = Math.floor(seconds / 3600);
993	    var m = Math.floor((seconds % 3600) / 60);
994	    var s = Math.floor(seconds % 60);
995	    if (h > 0) return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
996	    return m + ':' + String(s).padStart(2, '0');
997	}
998
999	function formatFileSize(bytes) {
1000	    if (!bytes) return '0 B';
1001	    var k     = 1024;
1002	    var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
1003	    var i     = Math.floor(Math.log(bytes) / Math.log(k));
1004	    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1005	}
1006

/**
 * Wild Dragon MAM - Premiere Pro Panel
 * Main JavaScript file for the CEP panel
 */

// Adobe CEP interface — must be instantiated before any host (ExtendScript) calls
const csInterface = new CSInterface();

// ============================================================================
// State Management
// ============================================================================

const state = {
    serverUrl:        localStorage.getItem('mam_server_url') || 'http://localhost:7434',
    isConnected:      false,
    isConnecting:     false,
    selectedAsset:    null,
    assets:           [],
    projects:         [],
    selectedProject:  'all',
    searchQuery:      '',
    currentPage:      0,
    pageSize:         50,
    totalAssets:      0,
    downloadProgress: 0,
    isDownloading:    false,
    thumbCache:       {}, // assetId -> signed URL
    // Maps tempFilePath → { assetId, displayName } for timeline export resolution.
    // Also stores 'name:filename.mp4' → same as a fuzzy fallback.
    importedAssets:   JSON.parse(localStorage.getItem('mam_imported_assets') || '{}'),
    exportPanelVisible:  false,
    currentSequenceName: '',
};

// ============================================================================
// DOM Elements
// ============================================================================

let elements = {};

function initDOMElements() {
    elements = {
        serverUrlInput:    document.getElementById('server-url'),
        connectBtn:        document.getElementById('connect-btn'),
        statusIndicator:   document.getElementById('status-indicator'),
        searchInput:       document.getElementById('search-input'),
        projectFilter:     document.getElementById('project-filter'),
        assetGrid:         document.getElementById('asset-grid'),
        emptyState:        document.getElementById('empty-state'),
        detailsPanel:      document.getElementById('details-panel'),
        detailsFilename:   document.getElementById('details-filename'),
        detailsCodec:      document.getElementById('details-codec'),
        detailsResolution: document.getElementById('details-resolution'),
        detailsFps:        document.getElementById('details-fps'),
        detailsDuration:   document.getElementById('details-duration'),
        detailsSize:       document.getElementById('details-size'),
        detailsTags:       document.getElementById('details-tags'),
        importBtn:         document.getElementById('import-btn'),
        importHiresBtn:    document.getElementById('import-hires-btn'),
        importAllBtn:      document.getElementById('import-all-btn'),
        exportTimelineBtn: document.getElementById('export-timeline-btn'),
        progressContainer: document.getElementById('progress-container'),
        progressLabel:     document.getElementById('progress-label'),
        progressFill:      document.getElementById('progress-fill'),
        // Export panel
        exportPanel:       document.getElementById('export-panel'),
        exportSeqName:     document.getElementById('export-seq-name'),
        exportProjSelect:  document.getElementById('export-proj-select'),
        exportClipInfo:    document.getElementById('export-clip-info'),
        exportConfirmBtn:  document.getElementById('export-confirm-btn'),
        exportCancelBtn:   document.getElementById('export-cancel-btn'),
        // Sequence info bar
        seqInfoBar:        document.getElementById('seq-info-bar'),
        seqInfoName:       document.getElementById('seq-info-name'),
        seqRefreshBtn:     document.getElementById('seq-refresh-btn'),
    };
}

// ============================================================================
// Thumbnail Lazy Loading (IntersectionObserver)
// ============================================================================

const thumbObserver = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
        if (entry.isIntersecting) {
            const img     = entry.target;
            const assetId = img.dataset.assetId;
            if (assetId && !img.dataset.loaded) loadThumbnail(img, assetId);
            thumbObserver.unobserve(img);
        }
    });
}, { rootMargin: '100px' });

async function loadThumbnail(img, assetId) {
    if (state.thumbCache[assetId]) {
        img.src = state.thumbCache[assetId];
        img.dataset.loaded = '1';
        return;
    }
    try {
        const r = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/thumbnail`, {
            headers: { Accept: 'application/json' },
        });
        if (!r.ok) return;
        const { url } = await r.json();
        if (!url) return;
        state.thumbCache[assetId] = url;
        img.src = url;
        img.dataset.loaded = '1';
    } catch (_) {
        // thumbnail unavailable — leave placeholder
    }
}

// ============================================================================
// Initialization
// ============================================================================

document.addEventListener('DOMContentLoaded', () => {
    initDOMElements();
    setupEventListeners();
    restoreSettings();
    logMessage('Wild Dragon MAM panel initialized');
});

function setupEventListeners() {
    elements.serverUrlInput.addEventListener('change', (e) => {
        state.serverUrl = e.target.value.trim().replace(/\/$/, '');
        localStorage.setItem('mam_server_url', state.serverUrl);
        state.thumbCache = {};
    });
    elements.connectBtn.addEventListener('click', connectToServer);
    elements.searchInput.addEventListener('input', debounce(handleSearch, 300));
    elements.projectFilter.addEventListener('change', handleProjectFilter);
    elements.assetGrid.addEventListener('click', handleAssetClick);
    elements.importBtn.addEventListener('click', importSelectedAsset);
    elements.importHiresBtn.addEventListener('click', importSelectedAssetHires);
    elements.importAllBtn.addEventListener('click', importAllAssets);
    elements.exportTimelineBtn.addEventListener('click', startExportTimeline);
    elements.exportConfirmBtn.addEventListener('click', confirmExportTimeline);
    elements.exportCancelBtn.addEventListener('click', cancelExportTimeline);
    elements.seqRefreshBtn.addEventListener('click', refreshCurrentSequenceInfo);
}

function restoreSettings() {
    elements.serverUrlInput.value = state.serverUrl;
}

// ============================================================================
// Server Connection
// ============================================================================

async function connectToServer() {
    if (state.isConnecting) return;

    state.isConnecting = true;
    updateConnectionStatus('connecting');
    elements.connectBtn.disabled = true;

    try {
        const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
            method: 'GET',
            headers: { Accept: 'application/json' },
            credentials: 'include',
        });

        if (response.ok) {
            state.isConnected = true;
            updateConnectionStatus('connected');
            elements.connectBtn.textContent = 'Reconnect';
            logMessage('Connected to Wild Dragon MAM');

            const projectData = await response.json();
            await fetchProjects(projectData);
            await fetchAssets();
            refreshCurrentSequenceInfo();
        } else {
            throw new Error(`HTTP ${response.status}`);
        }
    } catch (error) {
        console.error('Connection error:', error);
        state.isConnected = false;
        updateConnectionStatus('disconnected');
        elements.connectBtn.textContent = 'Connect';
        showErrorMessage(`Failed to connect: ${error.message}`);
    } finally {
        state.isConnecting = false;
        elements.connectBtn.disabled = false;
    }
}

function updateConnectionStatus(status) {
    const indicator = elements.statusIndicator;
    indicator.classList.remove('connected', 'connecting');
    if (status === 'connected')       indicator.classList.add('connected');
    else if (status === 'connecting') indicator.classList.add('connecting');
}

// ============================================================================
// API Calls
// ============================================================================

async function fetchProjects(preloadedData) {
    try {
        let projects;
        if (preloadedData) {
            projects = Array.isArray(preloadedData) ? preloadedData : [];
        } else {
            const response = await fetch(`${state.serverUrl}/api/v1/projects`, {
                headers: { Accept: 'application/json' },
                credentials: 'include',
            });
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            const data = await response.json();
            projects = Array.isArray(data) ? data : [];
        }

        state.projects = projects;

        const savedProject = state.selectedProject;
        elements.projectFilter.innerHTML = '<option value="all">All Projects</option>';
        state.projects.forEach((p) => {
            const opt = document.createElement('option');
            opt.value       = p.id;
            opt.textContent = p.name;
            elements.projectFilter.appendChild(opt);
        });
        elements.projectFilter.value = savedProject;

        // Also populate the export panel project selector
        populateExportProjectSelect();

        logMessage(`Loaded ${state.projects.length} projects`);
    } catch (error) {
        console.error('Error fetching projects:', error);
    }
}

async function fetchAssets(page = 0) {
    if (!state.isConnected) return;
    try {
        const params = new URLSearchParams({
            offset: page * state.pageSize,
            limit:  state.pageSize,
        });
        if (state.searchQuery)               params.append('search',     state.searchQuery);
        if (state.selectedProject !== 'all') params.append('project_id', state.selectedProject);

        const response = await fetch(
            `${state.serverUrl}/api/v1/assets?${params.toString()}`,
            { headers: { Accept: 'application/json' }, credentials: 'include' }
        );
        if (!response.ok) throw new Error(`HTTP ${response.status}`);

        const data        = await response.json();
        state.assets      = data.assets || [];
        state.totalAssets = data.total  || 0;
        state.currentPage = page;

        renderAssets();
        logMessage(`Loaded ${state.assets.length} assets`);
    } catch (error) {
        console.error('Error fetching assets:', error);
        showErrorMessage('Failed to fetch assets');
    }
}

async function fetchAssetDetails(assetId) {
    if (!state.isConnected) return null;
    try {
        const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}`, {
            headers: { Accept: 'application/json' },
            credentials: 'include',
        });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return await response.json();
    } catch (error) {
        console.error('Error fetching asset details:', error);
        return null;
    }
}

/**
 * Returns the download URL for the proxy.  /stream returns a server-relative
 * path (/api/v1/assets/:id/video) designed for browser playback; we prepend
 * serverUrl so Node.js http.get() receives a valid absolute URL.
 */
async function getSignedDownloadUrl(assetId) {
    const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/stream`, {
        headers: { Accept: 'application/json' },
        credentials: 'include',
    });
    if (!response.ok) throw new Error(`HTTP ${response.status} from /stream`);
    const { url } = await response.json();
    if (!url) throw new Error('Stream endpoint returned no URL');
    return url.startsWith('/') ? state.serverUrl + url : url;
}

/**
 * Returns presigned S3 URL + filename/ext/file_size for the hi-res original.
 * GET /api/v1/assets/:id/hires
 */
async function getHiresDownloadInfo(assetId) {
    const response = await fetch(`${state.serverUrl}/api/v1/assets/${assetId}/hires`, {
        headers: { Accept: 'application/json' },
        credentials: 'include',
    });
    if (!response.ok) throw new Error(`HTTP ${response.status} from /hires`);
    const data = await response.json();
    if (!data.url) throw new Error('Hi-res endpoint returned no URL');
    return data;
}

// ============================================================================
// UI Rendering
// ============================================================================

function renderAssets() {
    elements.assetGrid.innerHTML = '';

    if (state.assets.length === 0) {
        elements.emptyState.style.display = 'flex';
        return;
    }

    elements.emptyState.style.display = 'none';
    state.assets.forEach((asset) => {
        elements.assetGrid.appendChild(createAssetCard(asset));
    });
}

function createAssetCard(asset) {
    const card = document.createElement('div');
    card.className = 'asset-card';
    if (state.selectedAsset && state.selectedAsset.id === asset.id) {
        card.classList.add('selected');
    }
    card.dataset.assetId = asset.id;

    // Thumbnail — lazy loaded via IntersectionObserver
    const thumbnail = document.createElement('div');
    thumbnail.className = 'asset-thumbnail';

    const img = document.createElement('img');
    img.dataset.assetId = asset.id;
    img.alt             = escapeHtml(asset.display_name || asset.filename || '');
    img.style.cssText   = 'width:100%;height:100%;object-fit:cover;display:block;';
    img.onerror         = () => { img.style.display = 'none'; };
    thumbnail.appendChild(img);
    thumbObserver.observe(img);
    card.appendChild(thumbnail);

    // Info row
    const info = document.createElement('div');
    info.className = 'asset-info';

    const name       = asset.display_name || asset.filename || 'Untitled';
    const filenameEl = document.createElement('div');
    filenameEl.className   = 'asset-filename';
    filenameEl.title       = name;
    filenameEl.textContent = name;
    info.appendChild(filenameEl);

    const durationSec = asset.duration_ms ? asset.duration_ms / 1000 : null;
    const codec = asset.codec || asset.media_type || 'video';
    const meta  = document.createElement('div');
    meta.className   = 'asset-meta';
    meta.innerHTML   = [
        '' + (durationSec ? formatDuration(durationSec) : 'N/A') + '',
        '' + escapeHtml(codec.toUpperCase()) + '',
    ].join('');
    info.appendChild(meta);

    const statusStr   = asset.status || 'ready';
    const statusBadge = document.createElement('div');
    statusBadge.className   = 'asset-status-badge status-badge ' + statusStr;
    statusBadge.textContent = statusStr.toUpperCase();
    info.appendChild(statusBadge);

    card.appendChild(info);
    return card;
}

function showAssetDetails(asset) {
    state.selectedAsset = asset;
    elements.detailsPanel.classList.remove('hidden');

    elements.detailsFilename.textContent   = asset.display_name || asset.filename;
    elements.detailsCodec.textContent      = asset.codec || asset.media_type || 'Unknown';
    elements.detailsResolution.textContent = asset.resolution || 'N/A';
    elements.detailsFps.textContent        = asset.fps ? asset.fps + ' fps' : 'N/A';
    elements.detailsDuration.textContent   = asset.duration_ms
        ? formatDuration(asset.duration_ms / 1000)
        : 'N/A';
    elements.detailsSize.textContent       = asset.file_size
        ? formatFileSize(asset.file_size)
        : 'N/A';

    elements.detailsTags.innerHTML = '';
    const tags = asset.tags || [];
    if (tags.length > 0) {
        tags.forEach((tag) => {
            const el       = document.createElement('span');
            el.className   = 'tag';
            el.textContent = tag;
            elements.detailsTags.appendChild(el);
        });
    } else {
        elements.detailsTags.innerHTML =
            'No tags';
    }

    elements.importBtn.disabled      = false;
    elements.importHiresBtn.disabled = false;
}

function hideAssetDetails() {
    state.selectedAsset = null;
    elements.detailsPanel.classList.add('hidden');
    elements.importBtn.disabled      = true;
    elements.importHiresBtn.disabled = true;
}

// ============================================================================
// Search and Filter
// ============================================================================

function handleSearch(e) {
    state.searchQuery = e.target.value;
    state.currentPage = 0;
    fetchAssets();
}

function handleProjectFilter(e) {
    state.selectedProject = e.target.value;
    state.currentPage     = 0;
    fetchAssets();
}

// ============================================================================
// Import — Proxy
// ============================================================================

async function importSelectedAsset() {
    if (!state.selectedAsset) {
        showErrorMessage('No asset selected');
        return;
    }
    await importAsset(state.selectedAsset);
}

async function importAllAssets() {
    if (state.assets.length === 0) {
        showErrorMessage('No assets to import');
        return;
    }
    for (const asset of state.assets) {
        await importAsset(asset);
    }
    showSuccessMessage('All assets imported');
}

async function importAsset(asset) {
    try {
        elements.importBtn.disabled = true;

        // 1. Get proxy URL (absolute — /stream returns a relative path)
        showProgress('Getting download link...', 5);
        const url = await getSignedDownloadUrl(asset.id);

        // 2. Download proxy to OS temp dir via Node.js
        const safeName = sanitizeFilename(
            (asset.display_name || asset.filename || asset.id) + '.mp4'
        );
        showProgress('Downloading ' + safeName + '...', 10);
        const filePath = await downloadFile(url, safeName);

        // 3. Hand the local path to Premiere Pro
        showProgress('Importing into Premiere Pro...', 85);
        await importFileToPremiereProject(filePath);

        // 4. Store path → assetId so timeline export can resolve this clip
        saveImportMapping(filePath, safeName, asset);

        hideProgress();
        showSuccessMessage('Imported: ' + safeName);
    } catch (error) {
        console.error('Import error:', error);
        hideProgress();
        showErrorMessage('Import failed: ' + error.message);
    } finally {
        elements.importBtn.disabled = !state.selectedAsset;
    }
}

// ============================================================================
// Import — Hi-Res Original
// ============================================================================

async function importSelectedAssetHires() {
    if (!state.selectedAsset) {
        showErrorMessage('No asset selected');
        return;
    }
    await importAssetHires(state.selectedAsset);
}

async function importAssetHires(asset) {
    try {
        elements.importHiresBtn.disabled = true;

        // 1. Get hi-res presigned URL and metadata
        showProgress('Getting hi-res link...', 5);
        let hiresInfo;
        try {
            hiresInfo = await getHiresDownloadInfo(asset.id);
        } catch (err) {
            throw new Error('No hi-res source: ' + err.message);
        }

        const sizeNote = hiresInfo.file_size ? ' (' + formatFileSize(hiresInfo.file_size) + ')' : '';
        const safeName = sanitizeFilename(
            hiresInfo.filename || (asset.display_name || asset.id) + '.mxf'
        );

        // 2. Download hi-res file to OS temp dir
        showProgress('Downloading hi-res' + sizeNote + '...', 10);
        const filePath = await downloadFile(hiresInfo.url, safeName);

        // 3. Import into Premiere Pro
        showProgress('Importing hi-res into Premiere Pro...', 85);
        await importFileToPremiereProject(filePath);

        // 4. Map this path to the same asset ID (hi-res round-trips to same MAM record)
        saveImportMapping(filePath, safeName, asset);

        hideProgress();
        showSuccessMessage('Hi-res imported: ' + safeName);
    } catch (error) {
        console.error('Hi-res import error:', error);
        hideProgress();
        showErrorMessage('Hi-res import failed: ' + error.message);
    } finally {
        elements.importHiresBtn.disabled = !state.selectedAsset;
    }
}

/**
 * Persists two keys per import:
 *   - exact temp path   → so Premiere getMediaPath() matches on the same machine
 *   - 'name:filename'   → fuzzy fallback across sessions / machines
 */
function saveImportMapping(filePath, safeName, asset) {
    const entry = {
        assetId:     asset.id,
        displayName: asset.display_name || asset.filename || '',
    };
    state.importedAssets[filePath]          = entry;
    state.importedAssets['name:' + safeName] = entry;
    try {
        localStorage.setItem('mam_imported_assets', JSON.stringify(state.importedAssets));
    } catch (_) {}
}

// ============================================================================
// Active Sequence Info Bar
// ============================================================================

function refreshCurrentSequenceInfo() {
    csInterface.evalScript('getActiveSequence()', function (resultStr) {
        try {
            var parsed = JSON.parse(resultStr);
            state.currentSequenceName = parsed.sequenceName || '';
            if (state.currentSequenceName) {
                elements.seqInfoName.textContent = state.currentSequenceName;
                elements.seqInfoBar.classList.remove('hidden');
            } else {
                elements.seqInfoBar.classList.add('hidden');
            }
        } catch (e) {
            elements.seqInfoBar.classList.add('hidden');
        }
    });
}

// ============================================================================
// Export Panel UI
// ============================================================================

function populateExportProjectSelect() {
    elements.exportProjSelect.innerHTML = '<option value="">— Select project —</option>';
    state.projects.forEach(function (p) {
        var opt = document.createElement('option');
        opt.value       = p.id;
        opt.textContent = p.name;
        if (p.id === state.selectedProject) opt.selected = true;
        elements.exportProjSelect.appendChild(opt);
    });
}

function showExportPanel(timelineData) {
    elements.exportSeqName.value = timelineData.sequenceName || 'Sequence 1';
    populateExportProjectSelect();

    var totalClips  = (timelineData.clips || []).length;
    var matchedClips = resolveClipsToAssets(timelineData.clips || []).filter(function (c) {
        return c.asset_id;
    }).length;

    elements.exportClipInfo.textContent =
        matchedClips + ' of ' + totalClips + ' clip(s) matched to MAM assets';
    elements.exportClipInfo.style.color =
        matchedClips === 0 ? 'var(--error)' : 'var(--text-secondary)';

    // Stash the full timeline data for confirmExportTimeline
    elements.exportPanel.dataset.timelineJson = JSON.stringify(timelineData);
    elements.exportPanel.classList.remove('hidden');
    state.exportPanelVisible = true;
    elements.exportSeqName.focus();
}

function hideExportPanel() {
    elements.exportPanel.classList.add('hidden');
    state.exportPanelVisible = false;
}

// ============================================================================
// Timeline Export — Premiere → MAM
// ============================================================================

/**
 * Step 1 of 2: read the Premiere timeline then show the export confirmation
 * panel with pre-filled sequence name and matched clip count.
 */
async function startExportTimeline() {
    if (!state.isConnected) {
        showErrorMessage('Connect to MAM first');
        return;
    }

    // Toggle: clicking again closes the panel
    if (state.exportPanelVisible) {
        hideExportPanel();
        return;
    }

    showProgress('Reading Premiere timeline...', 20);

    const timelineData = await new Promise(function (resolve) {
        csInterface.evalScript('exportTimelineData()', function (resultStr) {
            try { resolve(JSON.parse(resultStr)); }
            catch (e) { resolve({ success: false, message: 'Parse error: ' + e.message, clips: [] }); }
        });
    });

    hideProgress();

    if (!timelineData.success) {
        showErrorMessage('Timeline read failed: ' + (timelineData.message || 'unknown error'));
        return;
    }

    if (!timelineData.clips || timelineData.clips.length === 0) {
        showErrorMessage('No clips found in the active sequence');
        return;
    }

    showExportPanel(timelineData);
}

/**
 * Step 2 of 2: resolve clip paths to MAM asset IDs, upsert the sequence,
 * then PUT the clip array to /api/v1/sequences/:id/clips.
 */
async function confirmExportTimeline() {
    var timelineData;
    try {
        timelineData = JSON.parse(elements.exportPanel.dataset.timelineJson || '{}');
    } catch (e) {
        showErrorMessage('Invalid timeline data — try reading again');
        return;
    }

    var seqName   = (elements.exportSeqName.value || '').trim() || 'Sequence 1';
    var projectId = elements.exportProjSelect.value;
    if (!projectId) {
        showErrorMessage('Select a target project');
        return;
    }

    var resolved = resolveClipsToAssets(timelineData.clips || []);
    var matched  = resolved.filter(function (c) { return c.asset_id; });

    if (matched.length === 0) {
        hideExportPanel();
        showErrorMessage('No clips matched MAM assets — import proxies or hi-res first');
        return;
    }

    hideExportPanel();
    showProgress('Creating sequence in MAM...', 20);

    try {
        var seqId = await upsertSequence(
            projectId,
            seqName,
            timelineData.frameRate || 59.94,
            timelineData.width     || 1920,
            timelineData.height    || 1080
        );

        showProgress('Writing ' + matched.length + ' clip(s)...', 60);

        var clipPayload = matched.map(function (c) {
            return {
                asset_id:            c.asset_id,
                track:               c.trackIndex,
                timeline_in_frames:  c.timelineInFrames,
                timeline_out_frames: c.timelineOutFrames,
                source_in_frames:    c.sourceInFrames,
                source_out_frames:   c.sourceOutFrames,
            };
        });

        var clipsRes = await fetch(
            state.serverUrl + '/api/v1/sequences/' + seqId + '/clips',
            {
                method:      'PUT',
                headers:     { 'Content-Type': 'application/json', Accept: 'application/json' },
                credentials: 'include',
                body:        JSON.stringify(clipPayload),
            }
        );
        if (!clipsRes.ok) throw new Error('Clip push failed: HTTP ' + clipsRes.status);

        hideProgress();
        showSuccessMessage('Timeline pushed: ' + matched.length + ' clip(s) → "' + seqName + '"');

        var skipped = resolved.length - matched.length;
        if (skipped > 0) {
            _showFlash(skipped + ' clip(s) skipped — not in MAM (import them first)', 'info-message');
        }
    } catch (err) {
        hideProgress();
        showErrorMessage('Export failed: ' + err.message);
    }
}

function cancelExportTimeline() {
    hideExportPanel();
}

/**
 * Maps Premiere clip file paths back to MAM asset IDs.
 * Lookup order: exact temp path → name-based fallback.
 */
function resolveClipsToAssets(clips) {
    return clips.map(function (clip) {
        var entry = state.importedAssets[clip.filePath];
        if (!entry) entry = state.importedAssets['name:' + clip.fileName];
        return Object.assign({}, clip, { asset_id: entry ? entry.assetId : null });
    });
}

/**
 * Finds an existing sequence by name in the target project; creates it if
 * absent.  Updates frame_rate/width/height on the existing record.
 * Returns the sequence ID.
 */
async function upsertSequence(projectId, name, frameRate, width, height) {
    var listRes = await fetch(
        state.serverUrl + '/api/v1/sequences?project_id=' + encodeURIComponent(projectId),
        { headers: { Accept: 'application/json' }, credentials: 'include' }
    );
    if (listRes.ok) {
        var seqs     = await listRes.json();
        var existing = seqs.find(function (s) { return s.name === name; });
        if (existing) {
            await fetch(state.serverUrl + '/api/v1/sequences/' + existing.id, {
                method:      'PUT',
                headers:     { 'Content-Type': 'application/json', Accept: 'application/json' },
                credentials: 'include',
                body:        JSON.stringify({ frame_rate: frameRate, width: width, height: height }),
            });
            return existing.id;
        }
    }

    var createRes = await fetch(state.serverUrl + '/api/v1/sequences', {
        method:      'POST',
        headers:     { 'Content-Type': 'application/json', Accept: 'application/json' },
        credentials: 'include',
        body:        JSON.stringify({
            project_id: projectId,
            name:       name,
            frame_rate: frameRate,
            width:      width,
            height:     height,
        }),
    });
    if (!createRes.ok) throw new Error('Failed to create sequence: HTTP ' + createRes.status);
    var seq = await createRes.json();
    return seq.id;
}

// ============================================================================
// UI Helpers
// ============================================================================

function handleAssetClick(e) {
    var card = e.target.closest('.asset-card');
    if (!card) return;

    document.querySelectorAll('.asset-card.selected').forEach(function (el) {
        el.classList.remove('selected');
    });
    card.classList.add('selected');

    var assetId = card.dataset.assetId;
    var asset   = state.assets.find(function (a) { return a.id === assetId; });
    if (asset) showAssetDetails(asset);
}

function showProgress(label, percent) {
    elements.progressContainer.classList.add('visible');
    elements.progressLabel.textContent = label;
    state.downloadProgress = percent;
    updateProgressUI();
}

function hideProgress() {
    elements.progressContainer.classList.remove('visible');
    state.downloadProgress = 0;
    updateProgressUI();
}

function updateProgressUI() {
    elements.progressFill.style.width = state.downloadProgress + '%';
}

function showErrorMessage(message) {
    _showFlash(message, 'error-message');
}

function showSuccessMessage(message) {
    _showFlash(message, 'success-message');
}

function _showFlash(message, className) {
    var el         = document.createElement('div');
    el.className   = className;
    el.textContent = message;
    var anchor     = document.querySelector('.search-filter-area');
    anchor.insertBefore(el, anchor.firstChild);
    setTimeout(function () { el.remove(); }, 5000);
}

function logMessage(message) {
    console.log('[MAM Panel] ' + message);
}

// ============================================================================
// File Download and Premiere Import (Node.js / ExtendScript)
// ============================================================================

/**
 * Downloads a remote URL to a local temp file using Node.js http/https.
 * Requires --enable-nodejs in the CEP manifest.
 */
function downloadFile(url, filename) {
    return new Promise(function (resolve, reject) {
        try {
            var https = require('https');
            var http  = require('http');
            var fs    = require('fs');
            var path  = require('path');
            var os    = require('os');

            var tempPath = path.join(os.tmpdir(), filename);
            var file     = fs.createWriteStream(tempPath);
            var protocol = url.startsWith('https') ? https : http;

            protocol.get(url, function (res) {
                if (res.statusCode !== 200) {
                    file.close();
                    fs.unlink(tempPath, function () {});
                    reject(new Error('Download HTTP ' + res.statusCode));
                    return;
                }

                var total    = parseInt(res.headers['content-length'] || '0', 10);
                var received = 0;

                res.on('data', function (chunk) {
                    received += chunk.length;
                    if (total > 0) {
                        state.downloadProgress = 10 + (received / total) * 75;
                        updateProgressUI();
                    }
                });

                res.pipe(file);

                file.on('finish', function () {
                    file.close(function () { resolve(tempPath); });
                });

                file.on('error', function (err) {
                    fs.unlink(tempPath, function () {});
                    reject(err);
                });
            }).on('error', function (err) {
                fs.unlink(tempPath, function () {});
                reject(err);
            });
        } catch (err) {
            reject(new Error('Node.js unavailable for download: ' + err.message));
        }
    });
}

/**
 * Calls csInterface.evalScript to import a local file into the open Premiere project.
 */
function importFileToPremiereProject(filePath) {
    return new Promise(function (resolve, reject) {
        var safePath = filePath.replace(/\\/g, '\\\\');

        var script = [
            '(function() {',
            '  var result = { success: false, message: "" };',
            '  try {',
            '    if (!app.project) {',
            '      result.message = "No active Premiere Pro project";',
            '      return JSON.stringify(result);',
            '    }',
            '    var f = new File("' + safePath + '");',
            '    if (!f.exists) {',
            '      result.message = "File not found: ' + safePath + '";',
            '      return JSON.stringify(result);',
            '    }',
            '    app.project.importFiles(["' + safePath + '"]);',
            '    result.success = true;',
            '    result.message = "Imported successfully";',
            '  } catch (e) {',
            '    result.message = e.message;',
            '  }',
            '  return JSON.stringify(result);',
            '})();',
        ].join('\n');

        csInterface.evalScript(script, function (resultStr) {
            try {
                var parsed = JSON.parse(resultStr);
                if (parsed.success) resolve(parsed);
                else reject(new Error(parsed.message));
            } catch (e) {
                reject(new Error('ExtendScript error: ' + resultStr));
            }
        });
    });
}

// ============================================================================
// Utility Functions
// ============================================================================

function debounce(func, delay) {
    var timeout;
    return function () {
        var args = arguments;
        clearTimeout(timeout);
        timeout = setTimeout(function () { func.apply(null, args); }, delay);
    };
}

function escapeHtml(str) {
    return String(str)
        .replace(/&/g,  '&')
        .replace(/</g,  '<')
        .replace(/>/g,  '>')
        .replace(/"/g,  '"')
        .replace(/'/g,  ''');
}

function sanitizeFilename(name) {
    return name.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_');
}

function formatDuration(seconds) {
    if (!seconds) return 'N/A';
    var h = Math.floor(seconds / 3600);
    var m = Math.floor((seconds % 3600) / 60);
    var s = Math.floor(seconds % 60);
    if (h > 0) return h + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
    return m + ':' + String(s).padStart(2, '0');
}

function formatFileSize(bytes) {
    if (!bytes) return '0 B';
    var k     = 1024;
    var sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
    var i     = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}


Lets do a debugging & polish session on https://forge.wilddragon.net/zgaetano/wild-dragon , its currently deployed to Truenas. http://10.0.0.25:47434/   SRT / Recorder / Ingest page is still buggy, pr…


Debug ingest, monitors, buttons and prototype file editing


Edit

// GET /:id/stream
router.get('/:id/stream', async (req, res, next) => {
  try {
    const { id } = req.params;
    const r = await pool.query('SELECT * FROM assets WHERE id = $1', [id]);
    if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
    const a = r.rows[0];
    if (a.status === 'live') return res.json({ url: `/live/${a.id}/index.m3u8`, type: 'hls', live: true });
    if (a.proxy_s3_key) return res.json({ url: `/api/v1/assets/${id}/video`, type: 'mp4' });
    const orig = a.original_s3_key;
    if (orig && orig.toLowerCase().endsWith('.mp4')) return res.json({ url: `/api/v1/assets/${id}/video`, type: 'mp4' });
    return res.json({ url: null, type: null, reason: 'no_proxy' });
  } catch (err) {
    next(err);
  }
});

// GET /:id/stream
router.get('/:id/stream', async (req, res, next) => {
  try {
    const { id } = req.params;
    const r = await pool.query('SELECT * FROM assets WHERE id = $1', [id]);
    if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
    const a = r.rows[0];
    if (a.status === 'live') return res.json({ url: `/live/${a.id}/index.m3u8`, type: 'hls', live: true });
    if (a.proxy_s3_key) return res.json({ url: `/api/v1/assets/${id}/video`, type: 'mp4' });
    const orig = a.original_s3_key;
    if (orig && orig.toLowerCase().endsWith('.mp4')) return res.json({ url: `/api/v1/assets/${id}/video`, type: 'mp4' });
    return res.json({ url: null, type: null, reason: 'no_proxy' });
  } catch (err) {
    next(err);
  }
});

// GET /:id/live-path
// Returns the SMB UNC path of a live (growing) asset so the Premiere panel
// can mount the file in place rather than downloading a proxy first.
// Editors mount the SMB share once; the panel passes individual file paths.
router.get('/:id/live-path', async (req, res, next) => {
  try {
    const { id } = req.params;
    const a = await pool.query(
      'SELECT id, project_id, display_name, status FROM assets WHERE id = $1',
      [id]
    );
    if (a.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
    const asset = a.rows[0];

    if (asset.status !== 'live') {
      return res.status(409).json({ error: 'Asset is not currently growing', status: asset.status });
    }

    const s = await pool.query(
      `SELECT key, value FROM settings
       WHERE key IN ('growing_enabled', 'growing_smb_url')`
    );
    const cfg = {};
    for (const { key, value } of s.rows) cfg[key] = value;
    if (cfg.growing_enabled !== 'true') {
      return res.status(409).json({ error: 'Growing-files mode is disabled' });
    }
    if (!cfg.growing_smb_url) {
      return res.status(409).json({ error: 'No SMB URL configured — set growing_smb_url in Settings' });
    }

    // Find the recorder driving this asset so we know the right file extension.
    const rec = await pool.query(
      `SELECT recording_container FROM recorders
       WHERE current_session_id = $1
       ORDER BY updated_at DESC LIMIT 1`,
      [asset.display_name]
    );
    const ext = rec.rows[0]?.recording_container || 'mov';

    const smbRoot = cfg.growing_smb_url.replace(/\/+$/, '');
    const winPath = smbRoot.replace(/^smb:\/\//, '\\\\').replace(/\//g, '\\') +
                    `\\${asset.project_id}\\${asset.display_name}.${ext}`;
    const posix   = smbRoot.replace(/^smb:\/\//, '//') +
                    `/${asset.project_id}/${asset.display_name}.${ext}`;
    res.json({
      smb_url:  `${smbRoot}/${asset.project_id}/${asset.display_name}.${ext}`,
      win_path: winPath,
      posix_path: posix,
      project_id: asset.project_id,
      display_name: asset.display_name,
      ext,
    });
  } catch (err) {
    next(err);
  }
});


The file /tmp/wd/wild-dragon/services/mam-api/src/routes/assets.js has been updated successfully. (file state is current in your context — no need to Read it back)

// GET /:id/stream
router.get('/:id/stream', async (req, res, next) => {
  try {
    const { id } = req.params;
    const r = await pool.query('SELECT * FROM assets WHERE id = $1', [id]);
    if (r.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
    const a = r.rows[0];
    if (a.status === 'live') return res.json({ url: `/live/${a.id}/index.m3u8`, type: 'hls', live: true });
    if (a.proxy_s3_key) return res.json({ url: `/api/v1/assets/${id}/video`, type: 'mp4' });
    const orig = a.original_s3_key;
    if (orig && orig.toLowerCase().endsWith('.mp4')) return res.json({ url: `/api/v1/assets/${id}/video`, type: 'mp4' });
    return res.json({ url: null, type: null, reason: 'no_proxy' });
  } catch (err) {
    next(err);
  }
});

// GET /:id/live-path
// Returns the SMB UNC path of a live (growing) asset so the Premiere panel
// can mount the file in place rather than downloading a proxy first.
// Editors mount the SMB share once; the panel passes individual file paths.
router.get('/:id/live-path', async (req, res, next) => {
  try {
    const { id } = req.params;
    const a = await pool.query(
      'SELECT id, project_id, display_name, status FROM assets WHERE id = $1',
      [id]
    );
    if (a.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });
    const asset = a.rows[0];

    if (asset.status !== 'live') {
      return res.status(409).json({ error: 'Asset is not currently growing', status: asset.status });
    }

    const s = await pool.query(
      `SELECT key, value FROM settings
       WHERE key IN ('growing_enabled', 'growing_smb_url')`
    );
    const cfg = {};
    for (const { key, value } of s.rows) cfg[key] = value;
    if (cfg.growing_enabled !== 'true') {
      return res.status(409).json({ error: 'Growing-files mode is disabled' });
    }
    if (!cfg.growing_smb_url) {
      return res.status(409).json({ error: 'No SMB URL configured — set growing_smb_url in Settings' });
    }

    // Find the recorder driving this asset so we know the right file extension.
    const rec = await pool.query(
      `SELECT recording_container FROM recorders
       WHERE current_session_id = $1
       ORDER BY updated_at DESC LIMIT 1`,
      [asset.display_name]
    );
    const ext = rec.rows[0]?.recording_container || 'mov';

    const smbRoot = cfg.growing_smb_url.replace(/\/+$/, '');
    const winPath = smbRoot.replace(/^smb:\/\//, '\\\\').replace(/\//g, '\\') +
                    `\\${asset.project_id}\\${asset.display_name}.${ext}`;
    const posix   = smbRoot.replace(/^smb:\/\//, '//') +
                    `/${asset.project_id}/${asset.display_name}.${ext}`;
    res.json({
      smb_url:  `${smbRoot}/${asset.project_id}/${asset.display_name}.${ext}`,
      win_path: winPath,
      posix_path: posix,
      project_id: asset.project_id,
      display_name: asset.display_name,
      ext,
    });
  } catch (err) {
    next(err);
  }
});

+// Returns the SMB UNC path of a live (growing) asset so the Premiere panel

+// can mount the file in place rather than downloading a proxy first.

+// Editors mount the SMB share once; the panel passes individual file paths.

+router.get('/:id/live-path', async (req, res, next) => {

+      'SELECT id, project_id, display_name, status FROM assets WHERE id = $1',

+    if (a.rows.length === 0) return res.status(404).json({ error: 'Asset not found' });

+      return res.status(409).json({ error: 'Asset is not currently growing', status: asset.status });

+       WHERE key IN ('growing_enabled', 'growing_smb_url')`

+    for (const { key, value } of s.rows) cfg[key] = value;

+    if (cfg.growing_enabled !== 'true') {

+      return res.status(409).json({ error: 'Growing-files mode is disabled' });

+      return res.status(409).json({ error: 'No SMB URL configured — set growing_smb_url in Settings' });

+    // Find the recorder driving this asset so we know the right file extension.

+      `SELECT recording_container FROM recorders

+       ORDER BY updated_at DESC LIMIT 1`,

+    const ext = rec.rows[0]?.recording_container || 'mov';

+    const smbRoot = cfg.growing_smb_url.replace(/\/+$/, '');

+    const winPath = smbRoot.replace(/^smb:\/\//, '\\\\').replace(/\//g, '\\') +

+    const posix   = smbRoot.replace(/^smb:\/\//, '//') +

+      smb_url:  `${smbRoot}/${asset.project_id}/${asset.display_name}.${ext}`,

+      display_name: asset.display_name,

router.get('/:id/video', async (req, res, next) => {


Now extend the Premier panel: add a "Mount Live" button, status polling for live→ready relink, and a LIVE badge on growing assets.


                <button id="import-btn" disabled title="Download proxy and import into Premiere">Import Proxy</button>
                <button id="import-hires-btn" class="secondary" disabled title="Download original hi-res and import into Premiere">Hi-Res</button>

                <button id="import-all-btn" class="secondary" title="Import all visible assets as proxy">Import All</button>
                <button id="export-timeline-btn" class="secondary" title="Push the current Premiere sequence to MAM">Export Timeline ↑</button>

                <button id="import-btn" disabled title="Download proxy and import into Premiere">Import Proxy</button>
                <button id="import-hires-btn" class="secondary" disabled title="Download original hi-res and import into Premiere">Hi-Res</button>

                <button id="mount-live-btn" class="secondary" disabled title="Open the live (growing) file directly from the SMB share">Mount Live</button>
                <button id="relink-btn" class="secondary" disabled title="Relink the imported clip from proxy to the finalized hi-res original">Relink to Hi-Res</button>

                <button id="import-all-btn" class="secondary" title="Import all visible assets as proxy">Import All</button>
                <button id="export-timeline-btn" class="secondary" title="Push the current Premiere sequence to MAM">Export Timeline ↑</button>


The file /tmp/wd/wild-dragon/services/premiere-plugin/index.html has been updated successfully. (file state is current in your context — no need to Read it back)

                <button id="import-btn" disabled title="Download proxy and import into Premiere">Import Proxy</button>
                <button id="import-hires-btn" class="secondary" disabled title="Download original hi-res and import into Premiere">Hi-Res</button>

                <button id="mount-live-btn" class="secondary" disabled title="Open the live (growing) file directly from the SMB share">Mount Live</button>
                <button id="relink-btn" class="secondary" disabled title="Relink the imported clip from proxy to the finalized hi-res original">Relink to Hi-Res</button>

                <button id="import-all-btn" class="secondary" title="Import all visible assets as proxy">Import All</button>
                <button id="export-timeline-btn" class="secondary" title="Push the current Premiere sequence to MAM">Export Timeline ↑</button>

<!DOCTYPE html>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Wild Dragon MAM</title>
    <link rel="stylesheet" href="css/styles.css">

                    <input
                        type="text"
                        id="server-url"
                        class="server-url"
                        placeholder="http://localhost:7434"
                        title="MAM server URL"
                    >

                <button id="connect-btn" class="connect-btn">Connect</button>

                <input
                    type="text"
                    id="search-input"
                    class="search-input"
                    placeholder="Search assets..."
                >

                <select id="project-filter" title="Filter by project">
                    <option value="all">All Projects</option>
                </select>

            SEQ

            <button id="seq-refresh-btn" class="seq-refresh-btn" title="Refresh active sequence">↻</button>

📁

Connect to server and load assets

Filename

Codec

Resolution

Frame Rate

Duration

File Size

Tags

Downloading...

Push Timeline to MAM

            <input
                type="text"
                id="export-seq-name"
                placeholder="Sequence name"
                title="Name this sequence will have in the MAM"
            >
            <select id="export-proj-select" title="Target project in MAM">
                <option value="">— Select project —</option>
            </select>

                <button id="export-confirm-btn">Push to MAM</button>
                <button id="export-cancel-btn" class="secondary">Cancel</button>

                <button id="import-btn" disabled title="Download proxy and import into Premiere">Import Proxy</button>
                <button id="import-hires-btn" class="secondary" disabled title="Download original hi-res and import into Premiere">Hi-Res</button>

                <button id="import-all-btn" class="secondary" title="Import all visible assets as proxy">Import All</button>
                <button id="export-timeline-btn" class="secondary" title="Push the current Premiere sequence to MAM">Export Timeline ↑</button>

-            <!-- Row 2: bulk + timeline actions -->

+            <!-- Row 2: growing-file actions -->

+                <button id="mount-live-btn" class="secondary" disabled title="Open the live (growing) file directly from the SMB share">Mount Live</button>

+                <button id="relink-btn" class="secondary" disabled title="Relink the imported clip from proxy to the finalized hi-res original">Relink to Hi-Res</button>

+            <!-- Row 3: bulk + timeline actions -->