chore: bump version to 1.0.1, add CHANGELOG, restart UX (UI modal + nodemon trigger), config/filters fixes, Docker ownership and compose docs

This commit is contained in:
Inside4ndroid
2025-09-17 11:35:15 +01:00
parent 484ea4bb3f
commit e489a1b613
79 changed files with 334 additions and 11769 deletions
+40 -9
View File
@@ -12,6 +12,8 @@
<div class="brand">TMDB Embed API</div>
<nav class="nav">
<button class="nav-item" id="logoutBtn" data-action="logout">Logout</button>
<button class="nav-item" id="restartBtn" type="button">Restart Server</button>
<div class="nav-sep" role="separator" aria-hidden="true"></div>
<button class="nav-item" data-target="panel-status">Server Status</button>
<button class="nav-item" data-target="panel-core">Core</button>
<button class="nav-item" data-target="panel-quality">Quality / Filters</button>
@@ -97,8 +99,9 @@
</label>
<textarea name="minQualitiesRaw" class="mini quality-json" placeholder='{"default":"900p"}' disabled></textarea>
</div>
<div class="field codec-group"><label>Exclude Codecs<p class="hint">Preset radios serialize to a simple string. Custom & advanced modes use the JSON fields.</p>
<div class="field codec-group"><label>Exclude Codecs<p class="hint">Preset radios serialize to JSON. Advanced lets you edit the JSON directly.</p>
<div class="codec-presets" id="codecPresets">
<label class="c-item"><input type="radio" name="codecPreset" value="none"> <span>None</span></label>
<label class="c-item"><input type="radio" name="codecPreset" value="all"> <span>All</span></label>
<label class="c-item"><input type="radio" name="codecPreset" value="dv"> <span>Dolby Vision (DV)</span></label>
<label class="c-item"><input type="radio" name="codecPreset" value="hdr"> <span>HDR / HDR10(+)</span></label>
@@ -172,20 +175,13 @@
<p class="hint">These settings allow you to customize the behavior of the api and will effect timings.</p>
<div class="field adv-flag-grid" id="advFlags" aria-label="Core Feature Flags">
<label class="adv-flag-item"><input type="checkbox" name="adv_disableCache" value="1" /> <span>Disable Cache</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_showboxRotatingProxy" value="1" /> <span>Showbox Rotating Proxy</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_disableUrlValidation" value="1" /> <span>Disable URL Validation</span></label>
<label class="adv-flag-item"><input type="checkbox" name="adv_disable4khdhubValidation" value="1" /> <span>Disable 4KHDHub Validation</span></label>
</div>
<h1>Proxy / Paths</h1>
<h1>Paths</h1>
<p class="hint">Provider enable/disable & feature flags apply immediately. Click toggles to enable (highlighted) or disable (dim).</p>
<div class="adv-group">
<div class="field small"><label>Showbox Cache Dir <input type="text" name="adv_showboxCacheDir" placeholder="/data/showbox-cache"/></label></div>
<div class="field small"><label>Showbox Proxy (Primary) <input type="text" name="adv_showboxProxyPrimary" placeholder="https://proxy.example/"/></label></div>
<div class="field small"><label>Showbox Proxy (Alternate) <input type="text" name="adv_showboxProxyAlternate" placeholder="https://proxy2.example/"/></label></div>
<div class="field small"><label>Xprime Proxy URL <input type="text" name="adv_xprimeProxyUrl" placeholder="https://..."/></label></div>
<div class="field small"><label>VidZee Proxy URL <input type="text" name="adv_vidzeeProxyUrl" placeholder="https://..."/></label></div>
<div class="field small"><label>VidSrc Proxy URL <input type="text" name="adv_vidsrcProxyUrl" placeholder="https://..."/></label></div>
<div class="field small"><label>MoviesMod Proxy URL <input type="text" name="adv_moviesmodProxyUrl" placeholder="https://..."/></label></div>
</div>
</section>
<section id="panel-live" class="panel">
@@ -206,4 +202,39 @@
<span id="validationSummary" class="validation-summary"></span>
</div>
</main>
<!-- Confirmation Modal -->
<div id="confirmReset" class="modal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="confirmResetTitle">
<div class="modal-backdrop"></div>
<div class="modal-window">
<h3 id="confirmResetTitle">Reset All Settings?</h3>
<p class="modal-text">This will remove all overrides, delete all TMDB API keys and FebBox cookies, and restore provider and feature flags to their defaults. This action cannot be undone.</p>
<ul class="modal-list">
<li>Clear TMDB keys and FebBox cookies</li>
<li>Reset provider enable/default selections</li>
<li>Reapply default quality and validation settings</li>
</ul>
<label class="modal-opt"><input type="checkbox" id="confirmResetDontAsk" /> Don't ask again on this device</label>
<div class="modal-actions">
<button type="button" class="danger" data-action="confirm">Reset Everything</button>
<button type="button" data-action="cancel">Cancel</button>
</div>
</div>
</div>
<!-- Restart Confirmation Modal -->
<div id="confirmRestart" class="modal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="confirmRestartTitle">
<div class="modal-backdrop"></div>
<div class="modal-window">
<h3 id="confirmRestartTitle">Restart Server?</h3>
<p class="modal-text">This will stop the server process and attempt to restart it. Active requests will be interrupted. Make sure the app is managed by Docker, PM2, systemd, or another supervisor.</p>
<ul class="modal-list">
<li>Interrupts any in-flight API calls</li>
<li>Server will exit and come back up</li>
<li>UI will auto-reload when available</li>
</ul>
<div class="modal-actions">
<button type="button" class="danger" data-action="confirm">Restart Now</button>
<button type="button" data-action="cancel">Cancel</button>
</div>
</div>
</div>
</body></html>
+112 -29
View File
@@ -124,13 +124,14 @@ function fillForm(data){
}
const excRaw = override.excludeCodecsRaw !== undefined ? override.excludeCodecsRaw : (merged.excludeCodecsRaw || '');
let parsed = null; try { if(excRaw && /[\{\[]/.test(excRaw)) parsed = JSON.parse(excRaw); } catch {}
let mode = 'all';
let mode = 'none';
if(parsed){
const dv = parsed.excludeDV === true;
const hdr = parsed.excludeHDR === true;
if(dv && !hdr) mode = 'dv';
if(!dv && !hdr) mode = 'none';
else if(dv && hdr) mode = 'all';
else if(dv && !hdr) mode = 'dv';
else if(hdr && !dv) mode = 'hdr';
else if(dv && hdr) mode = 'advanced';
else mode = 'advanced';
} else if(excRaw){
if(/excludeDV":true/.test(excRaw) && !/excludeHDR":true/.test(excRaw)) mode='dv';
@@ -167,25 +168,17 @@ function fillForm(data){
['enableVixsrcProvider','adv_enableVixsrc'],
['disableCache','adv_disableCache'],
['enablePStreamApi','adv_enablePStream'],
['showboxUseRotatingProxy','adv_showboxRotatingProxy'],
['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'],
['showboxProxyPrimary','adv_showboxProxyPrimary'],
['showboxProxyAlternate','adv_showboxProxyAlternate'],
['xprimeProxyUrl','adv_xprimeProxyUrl'],
['vidzeeProxyUrl','adv_vidzeeProxyUrl'],
['vidsrcProxyUrl','adv_vidsrcProxyUrl'],
['moviesmodProxyUrl','adv_moviesmodProxyUrl']
['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_showboxRotatingProxy: 'showboxUseRotatingProxy',
adv_disableUrlValidation: 'disableUrlValidation',
adv_disable4khdhubValidation: 'disable4khdhubUrlValidation'
};
@@ -234,7 +227,8 @@ async function save(){
}
const cPreset = document.querySelector('input[name="codecPreset"]:checked');
if(cPreset){
if(cPreset.value==='all') payload.excludeCodecsRaw = null;
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') {
@@ -258,20 +252,11 @@ async function save(){
adv_enableVixsrc:'enableVixsrcProvider',
adv_disableCache:'disableCache',
adv_enablePStream:'enablePStreamApi',
adv_showboxRotatingProxy:'showboxUseRotatingProxy',
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',
adv_showboxProxyPrimary:'showboxProxyPrimary',
adv_showboxProxyAlternate:'showboxProxyAlternate',
adv_xprimeProxyUrl:'xprimeProxyUrl',
adv_vidzeeProxyUrl:'vidzeeProxyUrl',
adv_vidsrcProxyUrl:'vidsrcProxyUrl',
adv_moviesmodProxyUrl:'moviesmodProxyUrl'
};
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){
@@ -288,18 +273,92 @@ async function save(){
} 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(){
if(!confirm('Remove ALL overrides?')) return;
const payload = { port:null, defaultRegion:null, defaultProviders:[], minQualitiesRaw:null, excludeCodecsRaw:null, febboxCookies:null,
enableXprimeProvider:null, enable4khdhubProvider:null, enableMoviesmodProvider:null, enableMp4hydraProvider:null, enableVidzeeProvider:null, enableVixsrcProvider:null,
disableCache:null, enablePStreamApi:null, showboxUseRotatingProxy:null, disableUrlValidation:null, disable4khdhubUrlValidation:null,
showboxCacheDir:null, showboxProxyPrimary:null, showboxProxyAlternate:null, xprimeProxyUrl:null, vidzeeProxyUrl:null, vidsrcProxyUrl:null, moviesmodProxyUrl:null };
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,
enableXprimeProvider: true,
enable4khdhubProvider: true,
enableMoviesmodProvider: true,
enableMp4hydraProvider: true,
enableVidzeeProvider: true,
enableVixsrcProvider: true,
// Other defaults
disableCache: false,
enablePStreamApi: true,
disableUrlValidation: false,
disable4khdhubUrlValidation: false,
showboxCacheDir: 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('Overrides cleared');
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); }
}
@@ -443,6 +502,30 @@ document.addEventListener('DOMContentLoaded', ()=> {
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');
+15
View File
@@ -129,6 +129,9 @@ body {
.nav-item.theme-toggle { margin-top: 12px; }
.nav-item#logoutBtn { border:1px solid var(--danger); color: var(--danger); font-weight:700; }
.nav-item#logoutBtn:hover { background: var(--danger); color:#fff; }
.nav-item#restartBtn { border:1px solid var(--danger); color: var(--danger); font-weight:700; }
.nav-item#restartBtn:hover { background: var(--danger); color:#fff; }
.nav-sep { height: 1px; background: var(--border); margin: 6px 0 8px; opacity: .6; margin-bottom: 1rem; margin-top: 1rem;}
.grow { flex-grow: 1; }
.side-footer { font-size: .65rem; opacity: .6; line-height: 1.4; text-align: center;}
.main {
@@ -364,3 +367,15 @@ input.invalid, textarea.invalid { border-color: var(--danger)!important; animati
.providers-table .prov-row { border:1px solid var(--border); }
.prov-row strong { font-size:.7rem; letter-spacing:.5px; }
}
/* Modal */
.modal { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; z-index: 100; }
.modal.show { display: flex; }
.modal .modal-backdrop { position: absolute; inset:0; background: rgba(0,0,0,.45); backdrop-filter: blur(2px); }
.modal .modal-window { position: relative; z-index: 1; width: min(560px, 94vw); background: var(--bg-alt); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); padding: 18px 20px; }
.modal .modal-window h3 { margin: 0 0 8px; font-size: 1.05rem; letter-spacing: .5px; }
.modal .modal-text { margin: 0 0 10px; font-size: .85rem; color: var(--text-dim); }
.modal .modal-list { margin: 0 0 14px 18px; padding: 0; color: var(--text-dim); font-size: .8rem; }
.modal .modal-opt { display:flex; align-items:center; gap:8px; margin: 0 0 12px; font-size:.8rem; color: var(--text-dim); }
.modal .modal-opt input { accent-color: var(--primary); }
.modal .modal-actions { display: flex; gap: 10px; justify-content: flex-end; }