UXP v2.1.1: main.js — fix recordImport from proxy/hires return values, add conform project select, fix conform panel

This commit is contained in:
Zac Gaetano 2026-05-28 02:24:07 -04:00
parent 76fff5efc2
commit 5d94838830

View file

@ -1,12 +1,11 @@
// Dragonflight UXP Panel — main.js v2.1.0 // Dragonflight UXP Panel — main.js v2.1.1
// Bootstrap + event wiring for all UI panels.
(function () { (function () {
if (window.__df_uxp_started) return; if (window.__df_uxp_started) return;
window.__df_uxp_started = true; window.__df_uxp_started = true;
// ── Helpers ──────────────────────────────────────────────────────
const $ = id => document.getElementById(id); const $ = id => document.getElementById(id);
function syncConnectBtn() { function syncConnectBtn() {
$('connect-btn').disabled = !$('server-url').value.trim() || !$('api-token').value.trim(); $('connect-btn').disabled = !$('server-url').value.trim() || !$('api-token').value.trim();
} }
@ -31,7 +30,7 @@
} }
} }
// ── Connect pane wiring ────────────────────────────────────────── // ── Connect pane ─────────────────────────────────────────────────
function wireConnectPane() { function wireConnectPane() {
$('server-url').value = API.state.serverUrl; $('server-url').value = API.state.serverUrl;
$('api-token').value = API.state.apiToken; $('api-token').value = API.state.apiToken;
@ -45,9 +44,8 @@
}); });
} }
// ── Library pane wiring ────────────────────────────────────────── // ── Library pane ─────────────────────────────────────────────────
function wireLibraryPane() { function wireLibraryPane() {
// Disconnect
$('disconnect-btn').addEventListener('click', () => { $('disconnect-btn').addEventListener('click', () => {
API.disconnect(); API.disconnect();
Library.stopGrowingPoll(); Library.stopGrowingPoll();
@ -58,11 +56,9 @@
UI.setStatus('#connect-status', 'Disconnected.', 'muted'); UI.setStatus('#connect-status', 'Disconnected.', 'muted');
}); });
// Tabs
$('tab-library').addEventListener('click', () => Library.switchTab('library')); $('tab-library').addEventListener('click', () => Library.switchTab('library'));
$('tab-growing').addEventListener('click', () => Library.switchTab('growing')); $('tab-growing').addEventListener('click', () => Library.switchTab('growing'));
// Search (debounced)
let searchTimer; let searchTimer;
$('search-input').addEventListener('input', e => { $('search-input').addEventListener('input', e => {
clearTimeout(searchTimer); clearTimeout(searchTimer);
@ -73,50 +69,53 @@
}, 280); }, 280);
}); });
// Project filter
$('project-filter').addEventListener('change', e => { $('project-filter').addEventListener('change', e => {
Library.state.selectedProject = e.target.value; Library.state.selectedProject = e.target.value;
if (Library.state.currentTab === 'library') Library.refresh(Library.state.searchQuery); if (Library.state.currentTab === 'library') Library.refresh(Library.state.searchQuery);
else Library._pollGrowing(); else Library._pollGrowing();
}); });
// Refresh $('refresh-btn').addEventListener('click', () => Library.refresh($('search-input').value));
$('refresh-btn').addEventListener('click', () =>
Library.refresh($('search-input').value)
);
// Import proxy // ── Import proxy ──
$('import-proxy-btn').addEventListener('click', async () => { $('import-proxy-btn').addEventListener('click', async () => {
const a = Library.selectedAsset(); const a = Library.selectedAsset();
if (!a) return; if (!a) return;
_disableImportBtns(true); _disableImportBtns(true);
try { try {
await Import.proxy(a); const { localPath, safeName } = await Import.proxy(a);
Library.recordImport(a._localPath, { assetId: a.id, displayName: a.display_name || a.filename }); Library.recordImport(localPath, { assetId: a.id, displayName: a.display_name || a.filename });
Library.recordImport('name:' + a._safeName, { assetId: a.id, displayName: a.display_name || a.filename }); Library.recordImport('name:' + safeName, { assetId: a.id, displayName: a.display_name || a.filename });
} catch (e) { UI.hideProgress(); UI.toast('Proxy import failed: ' + e.message, 'error'); } } catch (e) { UI.hideProgress(); UI.toast('Proxy import failed: ' + e.message, 'error'); }
finally { _disableImportBtns(false); Library._syncActions(); } finally { _disableImportBtns(false); Library._syncActions(); }
}); });
// Import hi-res // ── Import hi-res ──
$('import-hires-btn').addEventListener('click', async () => { $('import-hires-btn').addEventListener('click', async () => {
const a = Library.selectedAsset(); const a = Library.selectedAsset();
if (!a) return; if (!a) return;
_disableImportBtns(true); _disableImportBtns(true);
try { await Import.hires(a); } try {
catch (e) { UI.hideProgress(); UI.toast('Hi-res import failed: ' + e.message, 'error'); } const { localPath, safeName } = await Import.hires(a);
Library.recordImport(localPath, { assetId: a.id, displayName: a.display_name || a.filename });
Library.recordImport('name:' + safeName, { assetId: a.id, displayName: a.display_name || a.filename });
} catch (e) { UI.hideProgress(); UI.toast('Hi-res import failed: ' + e.message, 'error'); }
finally { _disableImportBtns(false); Library._syncActions(); } finally { _disableImportBtns(false); Library._syncActions(); }
}); });
// Import all // ── Import all ──
$('import-all-btn').addEventListener('click', async () => { $('import-all-btn').addEventListener('click', async () => {
const assets = Library.state.assets; const assets = Library.state.assets;
if (!assets.length) { UI.toast('No assets to import', 'error'); return; } if (!assets.length) { UI.toast('No assets', 'error'); return; }
_disableImportBtns(true); _disableImportBtns(true);
let ok = 0, fail = 0; let ok = 0, fail = 0;
for (const a of assets) { for (const a of assets) {
try { await Import.proxy(a); ok++; } try {
catch (_) { fail++; } const { localPath, safeName } = await Import.proxy(a);
Library.recordImport(localPath, { assetId: a.id, displayName: a.display_name || a.filename });
Library.recordImport('name:' + safeName, { assetId: a.id, displayName: a.display_name || a.filename });
ok++;
} catch (_) { fail++; }
} }
_disableImportBtns(false); _disableImportBtns(false);
UI.hideProgress(); UI.hideProgress();
@ -124,7 +123,7 @@
Library._syncActions(); Library._syncActions();
}); });
// Mount live // ── Mount live ──
$('mount-live-btn').addEventListener('click', async () => { $('mount-live-btn').addEventListener('click', async () => {
const a = Library.selectedAsset(); const a = Library.selectedAsset();
if (!a) return; if (!a) return;
@ -132,129 +131,119 @@
UI.showProgress('Resolving live path…', 10); UI.showProgress('Resolving live path…', 10);
try { try {
const info = await API.getLivePath(a.id); const info = await API.getLivePath(a.id);
// UXP runs on Windows PPro → use win_path
const hostPath = info.win_path || info.posix_path; const hostPath = info.win_path || info.posix_path;
UI.showProgress('Importing live file…', 50); UI.showProgress('Importing live file…', 50);
await Import.importIntoProject(hostPath); await Import.importIntoProject(hostPath);
Library.recordImport('live:' + a.id, { assetId: a.id, displayName: info.display_name, livePath: hostPath }); Library.recordImport('live:' + a.id, { assetId: a.id, displayName: info.display_name, livePath: hostPath });
Library.startLiveStatusPoll(a.id); Library.startLiveStatusPoll(a.id);
UI.hideProgress(); UI.hideProgress();
UI.toast('Mounted live: ' + info.display_name, 'ok'); UI.toast('Mounted live: ' + (info.display_name || a.id), 'ok');
} catch (e) { } catch (e) {
UI.hideProgress(); UI.hideProgress();
UI.toast('Mount live failed: ' + e.message, 'error'); UI.toast('Mount live failed: ' + e.message, 'error');
} finally { } finally { Library._syncActions(); }
Library._syncActions();
}
}); });
// Relink single asset // ── Relink single asset ──
$('relink-btn').addEventListener('click', async () => { $('relink-btn').addEventListener('click', async () => {
const a = Library.selectedAsset(); const a = Library.selectedAsset();
if (!a) return; if (!a) return;
const entry = Library.getImport('live:' + a.id); const entry = Library.getImport('live:' + a.id);
if (!entry) { UI.toast('No live mount recorded for this asset', 'error'); return; } if (!entry) { UI.toast('No live mount recorded', 'error'); return; }
$('relink-btn').disabled = true; $('relink-btn').disabled = true;
UI.showProgress('Fetching hi-res…', 10); UI.showProgress('Fetching hi-res…', 10);
try { try {
const info = await API.getHiresInfo(a.id); const info = await API.getHiresInfo(a.id);
const safeName = UI.sanitizeFilename(info.filename || (a.display_name || a.id) + '.' + (info.ext || 'mxf')); const safeName = UI.sanitizeFilename(info.filename || (a.display_name || a.id) + '.' + (info.ext || 'mxf'));
const dest = await Import._tempPath(safeName); const dest = await Import._tempPath(safeName);
UI.showProgress('Downloading ' + safeName + '…', 20); UI.showProgress('Downloading ' + safeName + '…', 20);
const r = await API.requestFollow(info.url, {}); const r = await API.requestFollow(info.url, {});
if (!r.ok) throw new Error('Download HTTP ' + r.status); if (!r.ok) throw new Error('Download HTTP ' + r.status);
await Import._streamToFile(r, dest, ({ received, total }) => { await Import._streamToFile(r, dest, ({ received, total }) => {
const pct = total ? 20 + (received / total) * 60 : 20; const pct = total ? 20 + (received / total) * 60 : 20;
UI.showProgress('Downloading ' + UI.formatBytes(received) + '…', pct); UI.showProgress(UI.formatBytes(received) + '…', pct);
}); });
UI.showProgress('Relinking…', 85); UI.showProgress('Relinking…', 85);
const P = require('premierepro'); const P = require('premierepro');
const project = await P.Project.getActiveProject(); const proj = await P.Project.getActiveProject();
if (!project) throw new Error('No active project'); if (!proj) throw new Error('No active project');
await Timeline._relinkInProject(project, entry.livePath, dest); await Timeline._relinkInProject(proj, entry.livePath, dest);
Library.recordImport(dest, { assetId: a.id, displayName: a.display_name || a.filename }); Library.recordImport(dest, { assetId: a.id, displayName: a.display_name || a.filename });
UI.hideProgress(); UI.hideProgress();
UI.toast('Relinked to hi-res: ' + safeName, 'ok'); UI.toast('Relinked: ' + safeName, 'ok');
} catch (e) { } catch (e) {
UI.hideProgress(); UI.hideProgress();
UI.toast('Relink failed: ' + e.message, 'error'); UI.toast('Relink failed: ' + e.message, 'error');
} finally { } finally { Library._syncActions(); }
Library._syncActions();
}
}); });
// Export timeline
$('export-timeline-btn').addEventListener('click', openExportPanel); $('export-timeline-btn').addEventListener('click', openExportPanel);
// Advanced
$('export-conform-btn').addEventListener('click', openConformPanel); $('export-conform-btn').addEventListener('click', openConformPanel);
$('fetch-relink-btn').addEventListener('click', openRelinkPanel); $('fetch-relink-btn').addEventListener('click', openRelinkPanel);
} }
function _disableImportBtns(dis) { function _disableImportBtns(dis) {
$('import-proxy-btn').disabled = dis; ['import-proxy-btn','import-hires-btn'].forEach(id => { $(id).disabled = dis; });
$('import-hires-btn').disabled = dis;
} }
// ── Export timeline panel ───────────────────────────────────────── // ── Export timeline panel ─────────────────────────────────────────
let _timelineCache = null; let _seqCache = null;
async function openExportPanel() { async function openExportPanel() {
UI.showProgress('Reading Premiere sequence…', 20); UI.showProgress('Reading Premiere sequence…', 20);
try { try { _seqCache = await Timeline.readActiveSequence(); }
_timelineCache = await Timeline.readActiveSequence(); catch (e) { UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return; }
} catch (e) { if (!_seqCache.clips.length) { UI.hideProgress(); UI.toast('No clips in active sequence', 'error'); return; }
UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return;
}
if (!_timelineCache.clips.length) {
UI.hideProgress(); UI.toast('No clips in active sequence', 'error'); return;
}
UI.hideProgress(); UI.hideProgress();
const resolved = Library.resolveClipsToAssets(_timelineCache.clips); const resolved = Library.resolveClipsToAssets(_seqCache.clips);
const matched = resolved.filter(c => c.asset_id).length; const matched = resolved.filter(c => c.asset_id).length;
$('export-seq-name').value = _timelineCache.sequenceName || 'Sequence 1'; $('export-seq-name').value = _seqCache.sequenceName || 'Sequence 1';
$('export-clip-info').textContent = matched + ' of ' + _timelineCache.clips.length + ' clip(s) matched to MAM assets'; $('export-clip-info').textContent = matched + ' of ' + _seqCache.clips.length + ' clip(s) matched to MAM assets';
UI.openSlide('export-overlay', 'export-panel'); UI.openSlide('export-overlay', 'export-panel');
} }
function wireExportPanel() { function wireExportPanel() {
$('export-close-btn').addEventListener('click', () => UI.closeSlide('export-overlay', 'export-panel')); $('export-close-btn').addEventListener('click', () => UI.closeSlide('export-overlay', 'export-panel'));
$('export-cancel-btn').addEventListener('click', () => UI.closeSlide('export-overlay', 'export-panel')); $('export-cancel-btn').addEventListener('click', () => UI.closeSlide('export-overlay', 'export-panel'));
$('export-overlay').addEventListener('click', () => UI.closeSlide('export-overlay', 'export-panel'));
$('export-confirm-btn').addEventListener('click', async () => { $('export-confirm-btn').addEventListener('click', async () => {
const seqName = $('export-seq-name').value.trim() || 'Sequence 1'; if (!_seqCache) return;
const seqName = ($('export-seq-name').value || '').trim() || 'Sequence 1';
const projectId = $('export-proj-select').value; const projectId = $('export-proj-select').value;
if (!projectId) { UI.toast('Select a target project', 'error'); return; } if (!projectId) { UI.toast('Select a target project', 'error'); return; }
UI.closeSlide('export-overlay', 'export-panel'); UI.closeSlide('export-overlay', 'export-panel');
UI.showProgress('Pushing timeline…', 20); UI.showProgress('Pushing timeline…', 20);
try { try {
const { matched, skipped } = await Timeline.pushToMAM(seqName, projectId, _timelineCache); const { matched, skipped } = await Timeline.pushToMAM(seqName, projectId, _seqCache);
UI.hideProgress(); UI.hideProgress();
UI.toast('Pushed ' + matched + ' clip(s)' + (skipped ? ' (' + skipped + ' skipped)' : ''), 'ok'); UI.toast('Pushed ' + matched + ' clip(s)' + (skipped ? ' (' + skipped + ' skipped)' : ''), 'ok');
} catch (e) { } catch (e) { UI.hideProgress(); UI.toast('Export failed: ' + e.message, 'error'); }
UI.hideProgress();
UI.toast('Export failed: ' + e.message, 'error');
}
}); });
$('export-overlay').addEventListener('click', () => UI.closeSlide('export-overlay', 'export-panel'));
} }
// ── Conform panel ───────────────────────────────────────────────── // ── Conform panel ─────────────────────────────────────────────────
async function openConformPanel() { async function openConformPanel() {
UI.showProgress('Reading Premiere sequence…', 20); UI.showProgress('Reading Premiere sequence…', 20);
try { try { _seqCache = await Timeline.readActiveSequence(); }
_timelineCache = await Timeline.readActiveSequence(); catch (e) { UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return; }
} catch (e) { if (!_seqCache.clips.length) { UI.hideProgress(); UI.toast('No clips in active sequence', 'error'); return; }
UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return;
}
if (!_timelineCache.clips.length) {
UI.hideProgress(); UI.toast('No clips in active sequence', 'error'); return;
}
UI.hideProgress(); UI.hideProgress();
const resolved = Library.resolveClipsToAssets(_timelineCache.clips); const resolved = Library.resolveClipsToAssets(_seqCache.clips);
const matched = resolved.filter(c => c.asset_id).length; const matched = resolved.filter(c => c.asset_id).length;
$('conform-clip-info').textContent = matched + ' of ' + _timelineCache.clips.length + ' clip(s) matched'; $('conform-clip-info').textContent = matched + ' of ' + _seqCache.clips.length + ' clip(s) matched';
$('conform-start-btn').disabled = matched === 0; $('conform-start-btn').disabled = matched === 0;
// Sync conform project select from loaded projects
const conformProj = $('conform-proj-select');
if (conformProj) {
conformProj.innerHTML = '<option value="">— Select project —</option>';
Library.state.projects.forEach(p => {
const o = document.createElement('option');
o.value = p.id; o.textContent = p.name;
conformProj.appendChild(o);
});
}
UI.openSlide('conform-overlay', 'conform-panel'); UI.openSlide('conform-overlay', 'conform-panel');
} }
@ -263,43 +252,41 @@
$('conform-cancel-btn').addEventListener('click', () => UI.closeSlide('conform-overlay', 'conform-panel')); $('conform-cancel-btn').addEventListener('click', () => UI.closeSlide('conform-overlay', 'conform-panel'));
$('conform-overlay').addEventListener('click', () => UI.closeSlide('conform-overlay', 'conform-panel')); $('conform-overlay').addEventListener('click', () => UI.closeSlide('conform-overlay', 'conform-panel'));
// Preset cards
$('preset-cards').addEventListener('click', e => { $('preset-cards').addEventListener('click', e => {
const card = e.target.closest('.preset-card'); const card = e.target.closest('.preset-card');
if (!card) return; if (!card) return;
document.querySelectorAll('.preset-card').forEach(c => c.classList.remove('selected')); document.querySelectorAll('.preset-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected'); card.classList.add('selected');
const p = card.dataset.preset;
const presets = { const presets = {
broadcast: { codec:'prores_hq', quality:'high', resolution:'1080p', audio:'broadcast' }, broadcast: { codec:'prores_hq', quality:'high', resolution:'1080p', audio:'broadcast' },
web: { codec:'h264', quality:'medium', resolution:'1080p', audio:'web' }, web: { codec:'h264', quality:'medium', resolution:'1080p', audio:'web' },
archive: { codec:'prores_4444', quality:'high', resolution:'uhd', audio:'archive' }, archive: { codec:'prores_4444', quality:'high', resolution:'uhd', audio:'archive' },
}; };
if (presets[p]) { const p = presets[card.dataset.preset];
$('conform-codec').value = presets[p].codec; if (p) {
$('conform-quality').value = presets[p].quality; $('conform-codec').value = p.codec;
$('conform-resolution').value = presets[p].resolution; $('conform-quality').value = p.quality;
$('conform-audio').value = presets[p].audio; $('conform-resolution').value = p.resolution;
$('conform-audio').value = p.audio;
} }
}); });
$('conform-start-btn').addEventListener('click', async () => { $('conform-start-btn').addEventListener('click', async () => {
// Need a target project — use first available or prompt user if (!_seqCache) return;
const projectId = $('export-proj-select').value; const conformProj = $('conform-proj-select');
if (!projectId) { const projectId = conformProj ? conformProj.value : '';
UI.toast('Select a project via Export Timeline first', 'error'); return; if (!projectId) { UI.toast('Select a target project', 'error'); return; }
}
UI.closeSlide('conform-overlay', 'conform-panel'); UI.closeSlide('conform-overlay', 'conform-panel');
UI.showProgress('Starting conform job…', 20); UI.showProgress('Starting conform job…', 15);
try { try {
const jobId = await Timeline.startConform(projectId, _timelineCache.sequenceName, _timelineCache, { const jobId = await Timeline.startConform(projectId, _seqCache.sequenceName, _seqCache, {
codec: $('conform-codec').value, codec: $('conform-codec').value,
quality: $('conform-quality').value, quality: $('conform-quality').value,
resolution: $('conform-resolution').value, resolution: $('conform-resolution').value,
audio: $('conform-audio').value, audio: $('conform-audio').value,
}); });
Timeline.pollConform(jobId, Timeline.pollConform(jobId,
(pct, status) => UI.showProgress('Conform: ' + status + ' (' + pct + '%)…', 20 + pct * 0.78), (pct, status) => UI.showProgress('Conform: ' + status + ' (' + pct + '%)…', 15 + pct * 0.8),
job => { job => {
UI.hideProgress(); UI.hideProgress();
if (job.status === 'completed') { if (job.status === 'completed') {
@ -310,10 +297,7 @@
} }
} }
); );
} catch (e) { } catch (e) { UI.hideProgress(); UI.toast('Conform failed: ' + e.message, 'error'); }
UI.hideProgress();
UI.toast('Conform failed: ' + e.message, 'error');
}
}); });
} }
@ -322,14 +306,11 @@
async function openRelinkPanel() { async function openRelinkPanel() {
UI.showProgress('Reading Premiere sequence…', 20); UI.showProgress('Reading Premiere sequence…', 20);
try { try { _seqCache = await Timeline.readActiveSequence(); }
_timelineCache = await Timeline.readActiveSequence(); catch (e) { UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return; }
} catch (e) {
UI.hideProgress(); UI.toast('Timeline read failed: ' + e.message, 'error'); return;
}
UI.hideProgress(); UI.hideProgress();
const resolved = Library.resolveClipsToAssets(_timelineCache.clips); const resolved = Library.resolveClipsToAssets(_seqCache.clips);
_relinkClips = resolved; _relinkClips = resolved;
const list = $('clip-list'); const list = $('clip-list');
@ -346,8 +327,7 @@
row.className = 'clip-item' + (matched ? ' matched' : ' unmatched'); row.className = 'clip-item' + (matched ? ' matched' : ' unmatched');
const cb = document.createElement('input'); const cb = document.createElement('input');
cb.type = 'checkbox'; cb.id = 'rclip-' + i; cb.type = 'checkbox'; cb.id = 'rclip-' + i;
cb.disabled = !matched; cb.disabled = !matched; cb.checked = matched;
cb.checked = matched;
cb.addEventListener('change', _syncRelinkStart); cb.addEventListener('change', _syncRelinkStart);
const lbl = document.createElement('label'); const lbl = document.createElement('label');
lbl.htmlFor = 'rclip-' + i; lbl.htmlFor = 'rclip-' + i;
@ -366,8 +346,8 @@
} }
function _syncRelinkStart() { function _syncRelinkStart() {
const anyChecked = !!document.querySelector('#clip-list input[type="checkbox"]:checked'); const any = !!document.querySelector('#clip-list input[type="checkbox"]:checked');
$('relink-start-btn').disabled = !anyChecked; $('relink-start-btn').disabled = !any;
} }
function wireRelinkPanel() { function wireRelinkPanel() {
@ -376,28 +356,21 @@
$('relink-overlay').addEventListener('click', () => UI.closeSlide('relink-overlay', 'relink-panel')); $('relink-overlay').addEventListener('click', () => UI.closeSlide('relink-overlay', 'relink-panel'));
$('relink-start-btn').addEventListener('click', async () => { $('relink-start-btn').addEventListener('click', async () => {
const checkboxes = document.querySelectorAll('#clip-list input[type="checkbox"]:checked'); const checked = document.querySelectorAll('#clip-list input[type="checkbox"]:checked');
const indices = []; const selected = [];
checkboxes.forEach(cb => indices.push(parseInt(cb.id.replace('rclip-', ''), 10))); checked.forEach(cb => {
const selected = indices.map(i => _relinkClips[i]).filter(c => c && c.asset_id); const i = parseInt(cb.id.replace('rclip-',''), 10);
if (_relinkClips[i] && _relinkClips[i].asset_id) selected.push(_relinkClips[i]);
});
if (!selected.length) return; if (!selected.length) return;
UI.closeSlide('relink-overlay', 'relink-panel'); UI.closeSlide('relink-overlay', 'relink-panel');
UI.showProgress('Starting batch relink…', 5); UI.showProgress('Starting batch relink…', 5);
try { try {
const results = await Timeline.batchRelink(selected); const res = await Timeline.batchRelink(selected);
UI.hideProgress(); UI.hideProgress();
const msg = results.succeeded + ' clip(s) relinked' + const msg = res.succeeded + ' clip(s) relinked' + (res.failed ? ', ' + res.failed + ' failed' : '');
(results.failed ? ', ' + results.failed + ' failed' : ''); UI.toast(msg, res.failed ? 'error' : 'ok');
UI.toast(msg, results.failed ? 'error' : 'ok'); } catch (e) { UI.hideProgress(); UI.toast('Batch relink failed: ' + e.message, 'error'); }
// Show summary in panel
$('relink-summary').textContent = msg;
$('relink-summary').classList.remove('hidden');
} catch (e) {
UI.hideProgress();
UI.toast('Batch relink failed: ' + e.message, 'error');
}
}); });
} }
@ -418,5 +391,4 @@
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init(); else init();
})(); })();