// Admin panel logic (refactored into IIFE, no inline handlers)
(function(){
async function fetchConfig(){
const r = await fetch('/api/config');
if(!r.ok) throw new Error('Failed to fetch config');
return r.json();
}
async function fetchProviders(){
const r = await fetch('/api/providers');
if(!r.ok) throw new Error('Failed to fetch providers');
return r.json();
}
function setFebboxCookies(cookiesArray){
const listEl = document.getElementById('febboxCookieList');
const hidden = document.querySelector('input[name="febboxCookiesHidden"]');
if(!listEl || !hidden) return;
const arr = Array.isArray(cookiesArray)? cookiesArray.filter(c=>c && typeof c==='string') : [];
hidden.value = arr.join(',');
listEl.innerHTML = arr.length? arr.map((c,i)=>`
${c}
`).join('') : 'No cookies added
';
}
function addFebboxCookie(){
const input = document.getElementById('febboxCookieInput');
const val = (input.value||'').trim();
if(!val) return;
const hidden = document.querySelector('input[name="febboxCookiesHidden"]');
const existing = hidden.value? hidden.value.split(',').filter(Boolean):[];
if(existing.includes(val)) { input.value=''; return; }
existing.push(val);
setFebboxCookies(existing);
input.value='';
validate();
}
function removeFebboxCookie(idx){
const hidden = document.querySelector('input[name="febboxCookiesHidden"]');
const existing = hidden.value? hidden.value.split(',').filter(Boolean):[];
const next = existing.filter((_,i)=> i!=idx);
setFebboxCookies(next);
validate();
}
document.addEventListener('click', (e)=>{
const btn = e.target.closest('#addFebboxCookieBtn');
if(btn){ addFebboxCookie(); }
const rm = e.target.closest('.cookie-item button.remove');
if(rm){ const idx = rm.dataset.idx; removeFebboxCookie(idx); }
});
document.addEventListener('keydown',(e)=>{ if(e.key==='Enter' && e.target.id==='febboxCookieInput'){ e.preventDefault(); addFebboxCookie(); }});
// TMDB multi-key management
function setTmdbKeys(keys){
const listEl = document.getElementById('tmdbKeyList');
const hidden = document.querySelector('input[name="tmdbApiKeysHidden"]');
if(!listEl || !hidden) return;
const arr = Array.isArray(keys)? keys.filter(k=>k && typeof k==='string') : [];
hidden.value = arr.join(',');
listEl.innerHTML = arr.length? arr.map((k,i)=>`${k}
`).join('') : 'No TMDB keys
';
}
function addTmdbKey(){
const input = document.getElementById('tmdbKeyInput');
const val = (input.value||'').trim();
if(!val) return;
const hidden = document.querySelector('input[name="tmdbApiKeysHidden"]');
const existing = hidden.value? hidden.value.split(',').filter(Boolean):[];
if(existing.includes(val)) { input.value=''; return; }
existing.push(val);
setTmdbKeys(existing);
input.value='';
validate();
}
function removeTmdbKey(idx){
const hidden = document.querySelector('input[name="tmdbApiKeysHidden"]');
const existing = hidden.value? hidden.value.split(',').filter(Boolean):[];
setTmdbKeys(existing.filter((_,i)=> i!=idx));
validate();
}
document.addEventListener('click',(e)=>{
if(e.target.closest('#addTmdbKeyBtn')) addTmdbKey();
const rm = e.target.closest('.key-item button.remove');
if(rm){ removeTmdbKey(rm.dataset.idx); }
});
document.addEventListener('keydown',(e)=>{ if(e.key==='Enter' && e.target.id==='tmdbKeyInput'){ e.preventDefault(); addTmdbKey(); }});
function fillForm(data){
const merged = data.merged || {};
const override = data.override || {};
const f = document.getElementById('cfgForm');
const fields = ['port'];
fields.forEach(k=>{ if(!f.elements[k]) return; const ov = override[k]; f.elements[k].value = ov !== undefined ? ov : (merged[k]||''); });
if (f.elements['defaultRegion']) {
const val = override.defaultRegion !== undefined ? override.defaultRegion : (merged.defaultRegion || '');
f.elements['defaultRegion'].value = val || '';
}
f.elements['defaultProviders'].value = (override.defaultProviders || merged.defaultProviders || []).join(',');
(function(){
let cookiesVal = override.febboxCookies || merged.febboxCookies || [];
if(typeof cookiesVal === 'string') {
const trimmed = cookiesVal.trim();
if(trimmed.startsWith('[') && trimmed.endsWith(']')) { try { const arr = JSON.parse(trimmed); if(Array.isArray(arr)) cookiesVal = arr; } catch{} }
else if (trimmed.includes(',') || trimmed.includes('\n') || trimmed.includes(';')) {
cookiesVal = trimmed.split(/[\n;,]+/).map(s=>s.trim()).filter(Boolean);
} else if(trimmed) cookiesVal = [trimmed]; else cookiesVal = [];
}
if(!Array.isArray(cookiesVal)) cookiesVal = [];
setFebboxCookies(cookiesVal);
})();
(function(){
let keys = override.tmdbApiKeys || merged.tmdbApiKeys || [];
if(!Array.isArray(keys)) {
if (override.tmdbApiKey) keys = [override.tmdbApiKey];
else if (merged.tmdbApiKey) keys = [merged.tmdbApiKey];
else keys = [];
}
setTmdbKeys(keys);
})();
const mqRaw = override.minQualitiesRaw !== undefined ? override.minQualitiesRaw : (merged.minQualitiesRaw || '');
const presetValues = ['all','480p','720p','1080p','1440p','2160p'];
let matchedPreset = presetValues.includes((mqRaw||'').toLowerCase()) ? mqRaw.toLowerCase() : (mqRaw? 'custom':'all');
const presetInput = document.querySelector(`input[name="qualityPreset"][value="${matchedPreset}"]`) || document.querySelector('input[name="qualityPreset"][value="all"]');
if (presetInput) presetInput.checked = true;
const customField = document.querySelector('textarea[name="minQualitiesRaw"]');
if (customField) {
if (matchedPreset === 'custom') { customField.disabled = false; customField.value = mqRaw; }
else { customField.disabled = true; customField.value = ''; }
}
const excRaw = override.excludeCodecsRaw !== undefined ? override.excludeCodecsRaw : (merged.excludeCodecsRaw || '');
let parsed = null; try { if(excRaw && /[\{\[]/.test(excRaw)) parsed = JSON.parse(excRaw); } catch {}
let mode = 'none';
if(parsed){
const dv = parsed.excludeDV === true;
const hdr = parsed.excludeHDR === true;
if(!dv && !hdr) mode = 'none';
else if(dv && hdr) mode = 'all';
else if(dv && !hdr) mode = 'dv';
else if(hdr && !dv) mode = 'hdr';
else mode = 'advanced';
} else if(excRaw){
if(/excludeDV":true/.test(excRaw) && !/excludeHDR":true/.test(excRaw)) mode='dv';
else if(/excludeHDR":true/.test(excRaw) && !/excludeDV":true/.test(excRaw)) mode='hdr';
else mode='advanced';
}
const presetInputC = document.querySelector(`input[name="codecPreset"][value="${mode}"]`) || document.querySelector('input[name="codecPreset"][value="all"]');
if(presetInputC) presetInputC.checked = true;
const codecTa = document.querySelector('textarea[name="excludeCodecsRaw"]');
if(mode==='advanced') { codecTa.disabled=false; codecTa.value = excRaw || JSON.stringify({excludeDV:false,excludeHDR:false}); }
else { codecTa.disabled=true; codecTa.value=''; }
document.getElementById('currentCfg').textContent = JSON.stringify(merged, null, 2);
document.getElementById('overrideCfg').textContent = Object.keys(override).length? JSON.stringify(override,null,2): '(none)';
document.getElementById('bootWarn').style.display = (!merged.febboxCookies || merged.febboxCookies.length===0)? 'block':'none';
// Removed version & overrides pills from UI; logic deleted
const cacheBanner = document.getElementById('cacheDisabledBanner'); if(cacheBanner) cacheBanner.style.display = merged.disableCache? 'block':'none';
const tmdbBanner = document.getElementById('tmdbMissingBanner');
if(tmdbBanner){
const tmdbMissing = !((override.tmdbApiKeys && override.tmdbApiKeys.length) || (merged.tmdbApiKeys && merged.tmdbApiKeys.length) || override.tmdbApiKey || merged.tmdbApiKey);
const tmdbProvidersEnabled = [
merged.enableShowboxProvider !== false,
merged.enable4khdhubProvider !== false
].some(Boolean);
tmdbBanner.style.display = (tmdbMissing && tmdbProvidersEnabled)? 'block':'none';
}
const advBools = [
['enable4khdhubProvider','adv_enable4khdhub'],
['enableVixsrcProvider','adv_enableVixsrc'],
['enableProxy','adv_enableProxy'],
['disableCache','adv_disableCache'],
['enablePStreamApi','adv_enablePStream'],
['disableUrlValidation','adv_disableUrlValidation'],
['disable4khdhubUrlValidation','adv_disable4khdhubValidation']
];
advBools.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const src = (override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]); if(el.type==='checkbox'){ el.checked = !!src; } else { el.value = src? 'true':''; } });
const advTexts = [
['showboxCacheDir','adv_showboxCacheDir']
];
advTexts.forEach(([cfgKey, elName])=>{ const el = f.elements[elName]; if(!el) return; const val = override[cfgKey]!==undefined? override[cfgKey] : merged[cfgKey]; el.value = val || ''; });
// Sync checkbox style advanced flags
const advFlagMap = {
adv_disableCache: 'disableCache',
adv_disableUrlValidation: 'disableUrlValidation',
adv_disable4khdhubValidation: 'disable4khdhubUrlValidation'
};
Object.entries(advFlagMap).forEach(([inputName, cfgKey]) => {
const el = f.elements[inputName];
if(!el) return;
const val = override[cfgKey] !== undefined ? override[cfgKey] : merged[cfgKey];
el.checked = !!val;
});
}
async function reload(){
try {
const data = await fetchConfig();
fillForm(data);
renderProviderMatrix(data.merged);
validate();
setStatus('');
} catch(e){ setStatus('Load failed: '+e.message,true); }
}
function setStatus(msg, isErr){
const el = document.getElementById('status');
el.textContent = msg;
el.style.color = isErr? '#dc2626':'inherit';
}
async function save(){
const f = document.getElementById('cfgForm');
const payload = {};
const dp = f.elements['defaultProviders'].value.trim();
if (dp) payload.defaultProviders = dp.split(/[\s,]+/).filter(Boolean);
['port'].forEach(k=>{
if(!f.elements[k]) return;
const v = f.elements[k].value.trim();
payload[k] = v? v : null;
});
const selPreset = document.querySelector('input[name="qualityPreset"]:checked');
if (selPreset) {
if (selPreset.value === 'custom') {
const cv = f.elements['minQualitiesRaw'].value.trim();
if (cv) payload.minQualitiesRaw = cv; else payload.minQualitiesRaw = null;
} else {
payload.minQualitiesRaw = selPreset.value === 'all' ? 'all' : selPreset.value;
}
}
const cPreset = document.querySelector('input[name="codecPreset"]:checked');
if(cPreset){
if(cPreset.value==='none') payload.excludeCodecsRaw = JSON.stringify({excludeDV:false,excludeHDR:false});
else if(cPreset.value==='all') payload.excludeCodecsRaw = JSON.stringify({excludeDV:true,excludeHDR:true});
else if(cPreset.value==='dv') payload.excludeCodecsRaw = JSON.stringify({excludeDV:true,excludeHDR:false});
else if(cPreset.value==='hdr') payload.excludeCodecsRaw = JSON.stringify({excludeDV:false,excludeHDR:true});
else if(cPreset.value==='advanced') {
const raw = document.querySelector('textarea[name="excludeCodecsRaw"]').value.trim();
payload.excludeCodecsRaw = raw || null;
}
}
const drSel = f.elements['defaultRegion'];
if (drSel) payload.defaultRegion = drSel.value ? drSel.value : null;
const cookiesHidden = f.elements['febboxCookiesHidden'];
if(cookiesHidden){
const cookiesVal = cookiesHidden.value.trim();
payload.febboxCookies = cookiesVal? cookiesVal.split(',').filter(Boolean) : [];
}
const boolMap = {
adv_enable4khdhub:'enable4khdhubProvider',
adv_enableVixsrc:'enableVixsrcProvider',
adv_enableProxy:'enableProxy',
adv_disableCache:'disableCache',
adv_enablePStream:'enablePStreamApi',
adv_disableUrlValidation:'disableUrlValidation',
adv_disable4khdhubValidation:'disable4khdhubUrlValidation'
};
Object.entries(boolMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(el) payload[cfgKey] = !!el.checked; });
const textMap = { adv_showboxCacheDir:'showboxCacheDir' };
Object.entries(textMap).forEach(([formName,cfgKey])=>{ const el = f.elements[formName]; if(!el) return; const v = el.value.trim(); payload[cfgKey] = v? v : null; });
const tmdbHidden = f.elements['tmdbApiKeysHidden'];
if (tmdbHidden){
const raw = tmdbHidden.value.trim();
payload.tmdbApiKeys = raw? raw.split(',').filter(Boolean) : [];
payload.tmdbApiKey = null;
}
try {
const r = await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const js = await r.json();
if(!js.success) throw new Error(js.error||'Save failed');
setStatus('Saved ✔');
fillForm(js);
} catch(e){ setStatus(e.message,true); }
}
function openConfirmReset(){
const modal = document.getElementById('confirmReset');
if(!modal) return Promise.resolve(false);
// Respect persisted preference
try {
const skip = localStorage.getItem('ns_confirmReset_skip') === '1';
if (skip) return Promise.resolve(true);
} catch {}
modal.classList.add('show');
return new Promise(resolve => {
const onClick = (e)=>{
const action = e.target?.dataset?.action;
if(action==='confirm'){
try {
const dontAsk = modal.querySelector('#confirmResetDontAsk')?.checked;
if (dontAsk) localStorage.setItem('ns_confirmReset_skip','1');
} catch {}
cleanup(); resolve(true);
}
if(action==='cancel' || e.target===modal.querySelector('.modal-backdrop')){ cleanup(); resolve(false); }
};
function onKey(e){ if(e.key==='Escape'){ cleanup(); resolve(false); } }
function cleanup(){ modal.classList.remove('show'); modal.removeEventListener('click', onClick); document.removeEventListener('keydown', onKey); }
modal.addEventListener('click', onClick);
document.addEventListener('keydown', onKey);
});
}
function openConfirmRestart(){
const modal = document.getElementById('confirmRestart');
if(!modal) return Promise.resolve(false);
modal.classList.add('show');
return new Promise(resolve => {
const onClick = (e)=>{
const action = e.target?.dataset?.action;
if(action==='confirm'){ cleanup(); resolve(true); }
if(action==='cancel' || e.target===modal.querySelector('.modal-backdrop')){ cleanup(); resolve(false); }
};
function onKey(e){ if(e.key==='Escape'){ cleanup(); resolve(false); } }
function cleanup(){ modal.classList.remove('show'); modal.removeEventListener('click', onClick); document.removeEventListener('keydown', onKey); }
modal.addEventListener('click', onClick);
document.addEventListener('keydown', onKey);
});
}
async function clearAll(){
const confirmed = await openConfirmReset();
if(!confirmed) return;
// To truly reset to app defaults, we must:
// - Explicitly clear arrays so env fallbacks don't persist
// - Set boolean flags to their default values (rather than null)
// - Null legacy single TMDB key
const payload = {
port: null,
defaultRegion: null,
defaultProviders: [],
minQualitiesRaw: 'all',
excludeCodecsRaw: JSON.stringify({excludeDV:false,excludeHDR:false}),
// Explicit empty arrays ensure override wins over any prior env mirror
febboxCookies: [],
tmdbApiKeys: [],
tmdbApiKey: null,
// Provider enable flags default to true
enableShowboxProvider: true,
enable4khdhubProvider: true,
enableVixsrcProvider: true,
enableProxy: false,
// Other defaults
disableCache: false,
enablePStreamApi: true,
disableUrlValidation: false,
disable4khdhubUrlValidation: false,
showboxCacheDir: null,
providerCheckTmdbId: null
};
try {
const r = await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const js = await r.json();
if(!js.success) throw new Error(js.error||'Clear failed');
setStatus('All settings reset to defaults');
fillForm(js);
// Re-render provider matrix immediately so toggles reflect defaults without page refresh
try { renderProviderMatrix(js.merged); } catch {}
validate();
} catch(e){ setStatus(e.message,true); }
}
function setTheme(t){
document.documentElement.dataset.theme = t;
localStorage.setItem('ns_theme', t);
}
function initTheme(){
const saved = localStorage.getItem('ns_theme');
setTheme(saved || 'dark');
}
function toggleTheme(){
setTheme(document.documentElement.dataset.theme === 'dark' ? 'light':'dark');
}
function navTo(id){
document.querySelectorAll('.panel').forEach(p=>p.classList.toggle('active', p.id===id));
document.querySelectorAll('.nav-item').forEach(a=>a.classList.toggle('active', a.dataset.target===id));
if(history.replaceState) history.replaceState(null,'', '#'+id);
if(id==='panel-status') loadServerStatus();
}
function attachCoreListeners(){
const saveBtn = document.getElementById('saveBtn');
const reloadBtn = document.getElementById('reloadBtn');
const clearBtn = document.getElementById('clearBtn');
if(saveBtn) saveBtn.addEventListener('click', save);
if(reloadBtn) reloadBtn.addEventListener('click', reload);
if(clearBtn) clearBtn.addEventListener('click', clearAll);
}
function bindNavigation(){
document.querySelectorAll('.nav .nav-item[data-target]').forEach(btn=>{
btn.addEventListener('click', ()=> navTo(btn.dataset.target));
});
const themeBtn = document.querySelector('.nav .theme-toggle');
if(themeBtn) themeBtn.addEventListener('click', toggleTheme);
}
function bindShellControls(){
const burger = document.querySelector('.burger');
if(burger) burger.addEventListener('click', ()=> toggleSidebar());
const overlay = document.querySelector('.overlay');
if(overlay) overlay.addEventListener('click', ()=> toggleSidebar(false));
}
async function verifySession(){
try {
const r = await fetch('/auth/session',{cache:'no-store'});
const j = await r.json();
if(!j.authenticated){ location.replace('/?expired=1'); }
} catch { location.replace('/?expired=1'); }
}
async function loadServerStatus(){
const metricsEl = document.getElementById('statusMetrics');
const endpointsEl = document.getElementById('statusEndpoints');
const provEnhanced = document.getElementById('statusProvidersEnhanced');
if(!metricsEl) return;
metricsEl.textContent = 'Loading...';
if(endpointsEl) endpointsEl.textContent = 'Loading...';
if(provEnhanced) provEnhanced.innerHTML = 'Loading...
';
try {
const r = await fetch('/api/status');
if(!r.ok) throw new Error('Status fetch failed');
const j = await r.json();
if(!j.success) throw new Error('Status error');
metricsEl.textContent = JSON.stringify(j.metrics, null, 2);
const checkTmdbEl = document.getElementById('providerCheckTmdbId');
if(checkTmdbEl) checkTmdbEl.value = j.providerCheckTmdbId || '278';
if(endpointsEl) endpointsEl.textContent = (j.endpoints||[]).join('\n');
if(provEnhanced){
provEnhanced.innerHTML = '';
const header = document.createElement('div');
header.className='prov-row header';
header.innerHTML = 'NameEnabledCookie ReqCookie OKFunctionalNotes';
provEnhanced.appendChild(header);
j.providers.forEach(p=>{
const row = document.createElement('div');
row.className = 'prov-row';
row.dataset.provider = p.name;
row.innerHTML = `${p.name}`+
`${p.enabled? 'on':'off'}`+
`${p.cookieRequired? 'yes':'no'}`+
`${p.cookieOk? 'ok':'missing'}`+
`?`+
``;
provEnhanced.appendChild(row);
});
}
} catch(e){
metricsEl.textContent = 'Failed: '+e.message;
if(endpointsEl) endpointsEl.textContent = 'n/a';
if(provEnhanced) provEnhanced.innerHTML = 'Status load failed
';
}
}
async function runProviderFunctionalChecks(){
const checkTmdbEl = document.getElementById('providerCheckTmdbId');
const targetTmdb = (checkTmdbEl && checkTmdbEl.value.trim()) || '278'; // TMDB ID (editable on status page)
const provEnhanced = document.getElementById('statusProvidersEnhanced');
const summary = document.getElementById('providerCheckSummary');
if(!provEnhanced) return;
summary.textContent = 'Running checks...';
const rows = Array.from(provEnhanced.querySelectorAll('.prov-row:not(.header)'));
let completed=0, passed=0;
for(const row of rows){
const name = row.dataset.provider;
const funcBadge = row.querySelector('[data-role="functional"]');
const notesEl = row.querySelector('[data-role="notes"]');
if(funcBadge){ funcBadge.textContent='…'; funcBadge.className='badge'; }
if(notesEl) notesEl.textContent='';
// Skip disabled providers
if(row.querySelector('.badge.ok, .badge.off') && row.children[1].textContent==='off'){
if(funcBadge){ funcBadge.textContent='skip'; funcBadge.className='badge off'; }
completed++; continue;
}
try {
const r = await fetch(`/api/streams/${name}/movie/${targetTmdb}`);
if(!r.ok) throw new Error('HTTP '+r.status);
const j = await r.json();
const isOk = j && Array.isArray(j.streams) && j.streams.length>0;
if(isOk){
passed++;
if(funcBadge){ funcBadge.textContent='ok'; funcBadge.className='badge ok'; }
if(notesEl) notesEl.textContent = `${j.streams.length} streams`;
}else{
if(funcBadge){ funcBadge.textContent='none'; funcBadge.className='badge fail'; }
if(notesEl) notesEl.textContent = 'No streams array';
}
}catch(err){
if(funcBadge){ funcBadge.textContent='fail'; funcBadge.className='badge fail'; }
if(notesEl) notesEl.textContent = err.message;
}
completed++;
summary.textContent = `Progress: ${completed}/${rows.length} (passed: ${passed})`;
}
summary.textContent = `Checks complete: ${passed}/${rows.length} passed`;
}
async function saveProviderCheckTmdbId(){
const input = document.getElementById('providerCheckTmdbId');
const val = (input ? input.value : '').trim();
if(!val){ setStatus('Enter a TMDB ID to save', true); return; }
if(!/^\d+$/.test(val)){ setStatus('TMDB ID must be numeric', true); return; }
try {
const r = await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ providerCheckTmdbId: val })});
const js = await r.json();
if(!js.success) throw new Error(js.error||'Save failed');
setStatus('Provider check TMDB ID saved: '+val);
} catch(e){ setStatus(e.message, true); }
}
document.addEventListener('DOMContentLoaded', ()=> {
initTheme();
attachCoreListeners();
bindNavigation();
bindShellControls();
const logoutBtn = document.getElementById('logoutBtn');
if(logoutBtn){
logoutBtn.addEventListener('click', async ()=>{
try { await fetch('/auth/logout',{method:'POST'}); } catch {}
// Use replace to avoid creating extra history entry
location.replace('/?loggedOut=1');
});
}
const restartBtn = document.getElementById('restartBtn');
if(restartBtn){
restartBtn.addEventListener('click', async ()=>{
const ok = await openConfirmRestart();
if(!ok) return;
setStatus('Restarting server…');
try {
const r = await fetch('/api/restart',{ method:'POST' });
if(!r.ok){ const t = await r.text().catch(()=>r.statusText); throw new Error(t||('HTTP '+r.status)); }
// Server will shut down soon; start polling until it comes back
await new Promise(resolve => setTimeout(resolve, 1000));
const start = Date.now();
const timeoutMs = 60_000;
(async function poll(){
try {
const pr = await fetch('/api/health',{ cache:'no-store' });
if(pr.ok){ location.reload(); return; }
} catch {}
if(Date.now()-start > timeoutMs){ setStatus('Restart timed out', true); return; }
setTimeout(poll, 1000);
})();
} catch(e){ setStatus('Restart failed: '+e.message, true); }
});
}
reload();
const h = location.hash.slice(1);
navTo(h||'panel-core');
// Revalidate session on back-forward cache navigation or visibility
window.addEventListener('pageshow', (e)=>{
if(e.persisted){ verifySession(); }
});
document.addEventListener('visibilitychange', ()=>{ if(document.visibilityState==='visible') verifySession(); });
// Advanced flag checkbox change -> validation
document.getElementById('advFlags')?.addEventListener('change', (e)=>{
if(e.target.matches('.adv-flag-item input[type="checkbox"]')) {
validate();
}
});
const rsb = document.getElementById('refreshStatusBtn');
if(rsb) rsb.addEventListener('click', loadServerStatus);
const rpc = document.getElementById('runProviderChecksBtn');
if(rpc) rpc.addEventListener('click', runProviderFunctionalChecks);
const spc = document.getElementById('saveProviderCheckTmdbBtn');
if(spc) spc.addEventListener('click', saveProviderCheckTmdbId);
});
function toggleSidebar(force){
const sb = document.getElementById('sidebar');
const showing = force !== undefined ? force : !sb.classList.contains('open');
sb.classList.toggle('open', showing);
document.querySelector('.overlay').classList.toggle('show', showing);
}
function validate(){
const f = document.getElementById('cfgForm');
const errors = [];
const qp = document.querySelector('input[name="qualityPreset"]:checked');
if(qp && qp.value==='custom') {
const mq = f.elements['minQualitiesRaw'].value.trim();
if(mq && /[\{\[]/.test(mq)) { try { JSON.parse(mq); } catch(e){ errors.push('Min Qualities invalid JSON'); markInvalid(f.elements['minQualitiesRaw']); } }
}
const codecPreset = document.querySelector('input[name="codecPreset"]:checked');
if(codecPreset && codecPreset.value==='advanced') {
const ec = f.elements['excludeCodecsRaw'].value.trim();
if(ec && /[\{\[]/.test(ec)) { try { JSON.parse(ec); } catch(e){ errors.push('Exclude Codecs invalid JSON'); markInvalid(f.elements['excludeCodecsRaw']); } }
}
const p = f.elements['port'].value.trim();
if(p) { const pn = Number(p); if(!(pn>0 && pn<=65535)) { errors.push('Port must be 1-65535'); markInvalid(f.elements['port']); } }
const dp = f.elements['defaultProviders'].value.trim();
if(dp && !/^[a-z0-9_,\-\s]+$/.test(dp)) { errors.push('Default Providers contains invalid chars'); markInvalid(f.elements['defaultProviders']); }
const sum = document.getElementById('validationSummary');
if(errors.length){ sum.textContent = errors.join(' • '); sum.classList.add('has-errors'); }
else { sum.textContent=''; sum.classList.remove('has-errors'); }
document.getElementById('saveBtn').disabled = errors.length>0;
return errors.length===0;
}
function markInvalid(el){ el.classList.add('invalid'); setTimeout(()=>el.classList.remove('invalid'), 3000); }
document.addEventListener('input', (e)=>{
if(e.target.closest('#cfgForm')) validate();
});
document.addEventListener('change', (e)=>{
const t = e.target;
if(t.name === 'qualityPreset') {
const customField = document.querySelector('textarea[name="minQualitiesRaw"]');
if(t.value === 'custom') { customField.disabled = false; } else { customField.disabled = true; customField.value=''; }
validate();
}
if(t.name === 'codecPreset') {
const ta = document.querySelector('textarea[name="excludeCodecsRaw"]');
if(t.value==='advanced') { ta.disabled=false; if(!ta.value.trim()) ta.value = JSON.stringify({excludeDV:false,excludeHDR:false}); }
else { ta.disabled=true; ta.value=''; }
validate();
}
});
async function renderProviderMatrix(merged){
if(!merged){ try { const data = await fetchConfig(); merged = data.merged||{}; } catch { return; } }
const container = document.getElementById('providerMatrix'); if(!container) return;
const defaults = (merged.defaultProviders||[]).map(s=>s.toLowerCase());
// Fetch available providers from API
let availableProviders = [];
try {
const providersResponse = await fetch('/api/providers');
if (providersResponse.ok) {
const providersData = await providersResponse.json();
availableProviders = providersData.providers || [];
}
} catch (e) {
console.warn('Failed to fetch providers list:', e.message);
}
// Create provider list with enable flags
const providers = availableProviders.map(p => {
const flag = `enable${p.name.charAt(0).toUpperCase() + p.name.slice(1)}Provider`;
return [p.name, flag];
});
container.innerHTML = providers.map(([name,flag])=>{
const enabled = merged[flag] !== false;
const isDefault = defaults.includes(name);
return ``
+``
+`${name}`
+``
+`
`;
}).join('');
const hidden = document.querySelector('input[name="defaultProviders"]'); if(hidden) hidden.value = defaults.join(',');
if(!container._bound){
container.addEventListener('click', async (e)=>{
const btn = e.target.closest('button[data-action]'); if(!btn) return;
const item = btn.closest('.pm-item'); if(!item) return;
const prov = item.dataset.prov; const flag = item.dataset.flag;
let data; try { data = await fetchConfig(); } catch(err){ setStatus('Provider fetch failed',true); return; }
const mergedNow = data.merged||{};
let defaultsNow = (mergedNow.defaultProviders||[]).slice();
const patch = {};
if(btn.dataset.action==='toggle-enable'){
const currentlyEnabled = item.dataset.enabled==='true';
patch[flag] = !currentlyEnabled;
if(currentlyEnabled){ defaultsNow = defaultsNow.filter(d=>d!==prov); patch.defaultProviders = defaultsNow; }
} else if(btn.dataset.action==='toggle-default'){
const isDefault = item.dataset.default==='true';
if(isDefault){ defaultsNow = defaultsNow.filter(d=>d!==prov); }
else { if(!defaultsNow.includes(prov)) defaultsNow.push(prov); patch[flag] = true; }
patch.defaultProviders = defaultsNow;
}
try {
await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(patch)});
const updated = await fetchConfig();
fillForm(updated);
renderProviderMatrix(updated.merged);
reloadLiveOnly();
} catch(err){ setStatus('Update failed: '+err.message,true); }
});
container._bound = true;
}
}
function reloadLiveOnly(){
fetchConfig().then(c=>{
document.getElementById('currentCfg').textContent = JSON.stringify(c.merged||{},null,2);
document.getElementById('overrideCfg').textContent = (c.override && Object.keys(c.override).length)? JSON.stringify(c.override,null,2):'(none)';
}).catch(()=>{});
}
// No globals exported; all handlers bound programmatically
})();