feat(setup): separate NFO scan into dedicated phase

- Add /nfo-scan-phase endpoint to trigger NFO scan independently
- Move NFO scan out of initial setup into separate post-unresolved phase
- Add phase query param handling for /loading page (?phase=initial, ?phase=nfo)
- Update setup redirect middleware to handle phase-based redirects
- Update auth setup to pass phase=initial to loading page
This commit is contained in:
2026-06-07 17:37:32 +02:00
parent cf00c9f7c5
commit 07c311c1cd
7 changed files with 350 additions and 71 deletions

View File

@@ -279,6 +279,10 @@
let ws = null;
const steps = new Map();
let isComplete = false;
// Get phase from URL query parameter
const urlParams = new URLSearchParams(window.location.search);
const currentPhase = urlParams.get('phase') || 'initial';
const stepOrder = [
'series_sync',
@@ -290,6 +294,68 @@
'nfo_scan': 'Scanning NFO Files'
};
// State management for setup flow
const SETUP_STATES = {
INITIAL: 'initial',
UNRESOLVED: 'unresolved',
NFO: 'nfo'
};
function setSetupPhase(phase) {
sessionStorage.setItem('setup_phase', phase);
}
function getSetupPhase() {
return sessionStorage.getItem('setup_phase');
}
function clearSetupPhase() {
sessionStorage.removeItem('setup_phase');
}
function validateStateAndRedirect() {
const storedPhase = getSetupPhase();
if (storedPhase && storedPhase !== currentPhase) {
// State mismatch - redirect to correct page based on stored phase
if (storedPhase === SETUP_STATES.INITIAL) {
window.location.href = '/loading?phase=initial';
return false;
} else if (storedPhase === SETUP_STATES.UNRESOLVED) {
window.location.href = '/setup/unresolved';
return false;
} else if (storedPhase === SETUP_STATES.NFO) {
window.location.href = '/loading?phase=nfo';
return false;
}
}
return true;
}
// For initial phase, we only show series_sync step
// For nfo phase, we only show nfo_scan step
function getStepsForPhase(phase) {
if (phase === 'nfo') {
return ['nfo_scan'];
}
return ['series_sync'];
}
function triggerNfoScanPhase() {
// Call API to trigger NFO scan phase
fetch('/api/setup/nfo-scan-phase', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
}).then(res => {
if (!res.ok) {
console.error('Failed to trigger NFO scan phase');
}
}).catch(err => {
console.error('Error triggering NFO scan phase:', err);
});
}
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/connect`;
@@ -300,21 +366,24 @@
console.log('WebSocket connected');
updateConnectionStatus(true);
// Subscribe to system room for progress updates
ws.send(JSON.stringify({
action: 'join',
data: {
room: 'system'
}
}));
// Subscribe to scan room for NFO scan progress
ws.send(JSON.stringify({
action: 'join',
data: {
room: 'scan'
}
}));
// Subscribe to rooms based on phase
if (currentPhase === 'nfo') {
// For nfo phase, only subscribe to scan room
ws.send(JSON.stringify({
action: 'join',
data: {
room: 'scan'
}
}));
} else {
// For initial phase (series_sync), subscribe to system room
ws.send(JSON.stringify({
action: 'join',
data: {
room: 'system'
}
}));
}
};
ws.onmessage = (event) => {
@@ -359,12 +428,18 @@
const data = message.data || message;
const { type, status, title, message: msg, percent, current, total, metadata } = data;
// Handle NFO scan events
if (type === 'nfo_scan_started' || type === 'nfo_scan_progress' || type === 'nfo_scan_completed') {
// For NFO phase, all events go to handleNfoScanUpdate
if (currentPhase === 'nfo') {
handleNfoScanUpdate(data);
return;
}
// For initial phase (series_sync), skip NFO scan events
if (type === 'nfo_scan_started' || type === 'nfo_scan_progress' || type === 'nfo_scan_completed') {
// Ignore NFO scan events during initial phase
return;
}
// Determine step ID based on type and metadata
let stepId = metadata?.step_id || type;
@@ -375,9 +450,10 @@
updateStep(stepId, status, msg, percent, current, total);
// Check for completion
if (metadata?.initialization_complete) {
showCompletion();
// Check for completion of series_sync
if (metadata?.initialization_complete || type === 'series_sync' && status === 'completed') {
// For initial phase, series_sync completion leads to /setup/unresolved
handleSeriesSyncComplete();
}
// Handle errors
@@ -385,6 +461,22 @@
showError(msg || 'An error occurred during initialization');
}
}
function handleSeriesSyncComplete() {
isComplete = true;
document.getElementById('connectionStatus').style.display = 'none';
if (ws) {
ws.close();
}
// Clear the initial phase state
clearSetupPhase();
// For initial phase, series_sync completion always leads to /setup/unresolved
// The unresolved page will handle checking if there are folders or redirect to nfo phase
window.location.href = '/setup/unresolved';
}
function handleNfoScanUpdate(data) {
const stepId = 'nfo_scan';
@@ -404,7 +496,7 @@
const progressTextEl = stepEl.querySelector('.progress-text');
const nfoData = data.data || data;
const { status, message, current, total, key, folder } = nfoData;
const { status, message, current, total, key, folder, metadata } = nfoData;
// Update status
stepEl.className = 'progress-step';
@@ -447,13 +539,26 @@
progressTextEl.textContent = `${Math.round(percent)}%`;
}
// Check for completion
if (data.type === 'nfo_scan_completed') {
setTimeout(() => {
checkUnresolvedAndProceed();
}, 1000);
// Check for completion - handle based on phase
if (data.type === 'nfo_scan_completed' || metadata?.nfo_scan_complete) {
handleNfoPhaseComplete();
}
}
function handleNfoPhaseComplete() {
isComplete = true;
document.getElementById('connectionStatus').style.display = 'none';
if (ws) {
ws.close();
}
// Clear the NFO phase state
clearSetupPhase();
// For NFO phase, completion always goes to login
window.location.href = '/login';
}
function createStep(stepId, title) {
const container = document.getElementById('progressContainer');
@@ -595,6 +700,27 @@
// Start WebSocket connection when page loads
document.addEventListener('DOMContentLoaded', () => {
// Validate state and redirect if there's a mismatch
if (!validateStateAndRedirect()) {
return; // Redirect in progress
}
// Set up the correct state for this phase
if (currentPhase === 'nfo') {
setSetupPhase(SETUP_STATES.NFO);
} else {
setSetupPhase(SETUP_STATES.INITIAL);
}
// Initialize the correct steps based on phase
const stepsForPhase = getStepsForPhase(currentPhase);
if (stepsForPhase.length === 1 && stepsForPhase[0] === 'nfo_scan') {
// For nfo phase, create the step and trigger the scan immediately
createStep('nfo_scan', stepTitles['nfo_scan']);
// Trigger NFO scan phase via API
triggerNfoScanPhase();
}
connectWebSocket();
});
</script>

View File

@@ -790,37 +790,14 @@
const data = await response.json();
if (response.ok && data.status === 'ok') {
// Redirect to loading page if provided, otherwise check for unresolved folders
if (data.redirect) {
showMessage('Setup saved! Initializing your anime library...', 'success');
setTimeout(() => {
window.location.href = data.redirect;
}, 500);
} else {
// Check for unresolved folders before redirecting
showMessage('Setup completed successfully! Checking for unresolved series...', 'success');
setTimeout(async () => {
try {
const token = localStorage.getItem('auth_token');
const res = await fetch('/api/setup/unresolved', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.ok) {
const unresolved = await res.json();
if (unresolved && unresolved.length > 0) {
window.location.href = '/setup/unresolved';
} else {
window.location.href = '/login';
}
} else {
window.location.href = '/login';
}
} catch (e) {
console.error('Error checking unresolved folders:', e);
window.location.href = '/login';
}
}, 1000);
}
// Always redirect to loading page with initial phase
// The loading page will handle unresolved folder check
showMessage('Setup saved! Initializing your anime library...', 'success');
setTimeout(() => {
// Set session storage state before redirecting
sessionStorage.setItem('setup_phase', 'initial');
window.location.href = '/loading?phase=initial';
}, 500);
} else {
const errorMessage = data.detail || data.message || 'Setup failed';
showMessage(errorMessage, 'error');

View File

@@ -652,7 +652,11 @@
listEl.style.display = 'none';
emptyEl.style.display = 'block';
document.getElementById('skip-link').style.display = 'block';
setTimeout(() => { window.location.href = '/loading'; }, 2000);
// No unresolved folders - redirect to NFO scan phase
setTimeout(() => {
sessionStorage.setItem('setup_phase', 'nfo');
window.location.href = '/loading?phase=nfo';
}, 2000);
} else {
listEl.style.display = 'flex';
emptyEl.style.display = 'none';
@@ -852,7 +856,11 @@
emptyEl.style.display = 'block';
skipLink.style.display = 'block';
showToast('All series configured!', 'success');
setTimeout(() => { window.location.href = '/loading'; }, 2000);
// All folders resolved - redirect to NFO scan phase
setTimeout(() => {
sessionStorage.setItem('setup_phase', 'nfo');
window.location.href = '/loading?phase=nfo';
}, 2000);
}
}
@@ -877,7 +885,12 @@
const result = await completeUnresolved();
if (result.status === 'success') {
showToast(result.message, 'success');
setTimeout(() => { window.location.href = '/loading'; }, 1000);
// Clear unresolved state and set NFO phase before redirecting
clearSetupPhase();
setTimeout(() => {
sessionStorage.setItem('setup_phase', 'nfo');
window.location.href = '/loading?phase=nfo';
}, 1000);
} else {
showToast(result.message || 'Failed to complete', 'error');
doneBtn.disabled = false;
@@ -896,8 +909,40 @@
doneBtn.style.display = 'inline-flex';
}
// State management for setup flow
function setSetupPhase(phase) {
sessionStorage.setItem('setup_phase', phase);
}
function clearSetupPhase() {
sessionStorage.removeItem('setup_phase');
}
function validateStateAndRedirect() {
const storedPhase = sessionStorage.getItem('setup_phase');
// If we have a stored phase that isn't 'unresolved', redirect appropriately
if (storedPhase && storedPhase !== 'unresolved') {
if (storedPhase === 'initial') {
window.location.href = '/loading?phase=initial';
return false;
} else if (storedPhase === 'nfo') {
window.location.href = '/loading?phase=nfo';
return false;
}
}
return true;
}
// Init
(async function init() {
// Validate state and redirect if there's a mismatch
if (!validateStateAndRedirect()) {
return; // Redirect in progress
}
// Set the unresolved phase state
setSetupPhase('unresolved');
const folders = await fetchUnresolved();
if (folders !== null) {
renderFolders(folders);