feat: add loading page with real-time initialization progress
- Create loading.html template with WebSocket-based progress updates - Update initialization_service to emit progress events via ProgressService - Modify setup endpoint to run initialization in background and redirect to loading page - Add /loading route in page_controller - Show real-time progress for series sync, NFO scan, and media scan steps - Display completion message with button to continue to app - Handle errors with visual feedback
This commit is contained in:
@@ -30,7 +30,8 @@ async def setup_auth(req: SetupRequest):
|
||||
"""Initial setup endpoint to configure the master password.
|
||||
|
||||
This endpoint also initializes the configuration with all provided values
|
||||
and saves them to config.json.
|
||||
and saves them to config.json. It triggers background initialization
|
||||
and redirects to a loading page that shows real-time progress.
|
||||
"""
|
||||
if auth_service.is_configured():
|
||||
raise HTTPException(
|
||||
@@ -117,17 +118,9 @@ async def setup_auth(req: SetupRequest):
|
||||
# Save the config with all updates
|
||||
config_service.save_config(config, create_backup=False)
|
||||
|
||||
# Perform initial setup (series sync, NFO scan, media scan)
|
||||
# This ensures everything is initialized immediately after setup
|
||||
# without requiring an application restart
|
||||
from src.config.settings import settings
|
||||
from src.server.services.initialization_service import (
|
||||
perform_initial_setup,
|
||||
perform_nfo_scan_if_needed,
|
||||
)
|
||||
|
||||
# Sync config.json values to settings object
|
||||
# (mirroring the logic in fastapi_app.py lifespan)
|
||||
from src.config.settings import settings
|
||||
other_settings = dict(config.other) if config.other else {}
|
||||
if other_settings.get("anime_directory"):
|
||||
settings.anime_directory = str(other_settings["anime_directory"])
|
||||
@@ -142,12 +135,53 @@ async def setup_auth(req: SetupRequest):
|
||||
settings.nfo_download_fanart = config.nfo.download_fanart
|
||||
settings.nfo_image_size = config.nfo.image_size
|
||||
|
||||
# Perform the initial series sync and mark as completed
|
||||
await perform_initial_setup()
|
||||
# Trigger initialization in background task
|
||||
import asyncio
|
||||
|
||||
from src.server.services.initialization_service import (
|
||||
perform_initial_setup,
|
||||
perform_nfo_scan_if_needed,
|
||||
)
|
||||
from src.server.utils.dependencies import get_progress_service
|
||||
|
||||
# Perform NFO scan if configured
|
||||
await perform_nfo_scan_if_needed()
|
||||
progress_service = get_progress_service()
|
||||
|
||||
async def run_initialization():
|
||||
"""Run initialization steps with progress updates."""
|
||||
try:
|
||||
# Perform the initial series sync and mark as completed
|
||||
await perform_initial_setup(progress_service)
|
||||
|
||||
# Perform NFO scan if configured
|
||||
await perform_nfo_scan_if_needed(progress_service)
|
||||
|
||||
# Send completion event
|
||||
progress_service.emit_progress(
|
||||
progress_id="initialization_complete",
|
||||
progress_type="system",
|
||||
status="completed",
|
||||
title="Initialization Complete",
|
||||
message="All initialization tasks completed successfully",
|
||||
percent=100,
|
||||
metadata={"initialization_complete": True}
|
||||
)
|
||||
except Exception as e:
|
||||
# Send error event
|
||||
progress_service.emit_progress(
|
||||
progress_id="initialization_error",
|
||||
progress_type="error",
|
||||
status="failed",
|
||||
title="Initialization Failed",
|
||||
message=str(e),
|
||||
percent=0,
|
||||
metadata={"initialization_complete": True, "error": str(e)}
|
||||
)
|
||||
|
||||
# Start initialization in background
|
||||
asyncio.create_task(run_initialization())
|
||||
|
||||
# Return redirect to loading page
|
||||
return {"status": "ok", "redirect": "/loading"}
|
||||
# Note: Media scan is skipped during setup as it requires
|
||||
# background_loader service which is only available during
|
||||
# application lifespan. It will run on first application startup.
|
||||
|
||||
@@ -49,3 +49,13 @@ async def queue_page(request: Request):
|
||||
request,
|
||||
title="Download Queue - Aniworld"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/loading", response_class=HTMLResponse)
|
||||
async def loading_page(request: Request):
|
||||
"""Serve the initialization loading page."""
|
||||
return render_template(
|
||||
"loading.html",
|
||||
request,
|
||||
title="Initializing - Aniworld"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Centralized initialization service for application startup and setup."""
|
||||
from typing import Optional
|
||||
|
||||
import structlog
|
||||
|
||||
from src.config.settings import settings
|
||||
@@ -7,7 +9,7 @@ from src.server.services.anime_service import sync_series_from_data_files
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
async def perform_initial_setup():
|
||||
async def perform_initial_setup(progress_service=None):
|
||||
"""Perform initial setup including series sync and scan completion marking.
|
||||
|
||||
This function is called both during application lifespan startup
|
||||
@@ -18,12 +20,27 @@ async def perform_initial_setup():
|
||||
4. NFO scan is performed if configured
|
||||
5. Media scan is performed
|
||||
|
||||
Args:
|
||||
progress_service: Optional ProgressService instance for emitting updates
|
||||
|
||||
Returns:
|
||||
bool: True if initialization was performed, False if skipped
|
||||
"""
|
||||
from src.server.database.connection import get_db_session
|
||||
from src.server.database.system_settings_service import SystemSettingsService
|
||||
|
||||
# Send initial progress update
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="series_sync",
|
||||
progress_type="system",
|
||||
status="started",
|
||||
title="Syncing Series Database",
|
||||
message="Checking initialization status...",
|
||||
percent=0,
|
||||
metadata={"step_id": "series_sync"}
|
||||
)
|
||||
|
||||
# Check if initial setup has been completed
|
||||
try:
|
||||
async with get_db_session() as db:
|
||||
@@ -35,6 +52,16 @@ async def perform_initial_setup():
|
||||
logger.info(
|
||||
"Initial scan already completed, skipping data file sync"
|
||||
)
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="series_sync",
|
||||
progress_type="system",
|
||||
status="completed",
|
||||
title="Syncing Series Database",
|
||||
message="Already completed",
|
||||
percent=100,
|
||||
metadata={"step_id": "series_sync"}
|
||||
)
|
||||
return False
|
||||
else:
|
||||
logger.info(
|
||||
@@ -58,11 +85,32 @@ async def perform_initial_setup():
|
||||
logger.info(
|
||||
"Initialization skipped - anime directory not configured"
|
||||
)
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="series_sync",
|
||||
progress_type="system",
|
||||
status="completed",
|
||||
title="Syncing Series Database",
|
||||
message="No anime directory configured",
|
||||
percent=100,
|
||||
metadata={"step_id": "series_sync"}
|
||||
)
|
||||
return False
|
||||
|
||||
# Only sync from data files on first run
|
||||
if not is_initial_scan_done:
|
||||
logger.info("Performing initial anime folder scan...")
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="series_sync",
|
||||
progress_type="system",
|
||||
status="in_progress",
|
||||
title="Syncing Series Database",
|
||||
message="Scanning anime folders...",
|
||||
percent=25,
|
||||
metadata={"step_id": "series_sync"}
|
||||
)
|
||||
|
||||
sync_count = await sync_series_from_data_files(
|
||||
settings.anime_directory
|
||||
)
|
||||
@@ -70,6 +118,17 @@ async def perform_initial_setup():
|
||||
"Data file sync complete. Added %d series.", sync_count
|
||||
)
|
||||
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="series_sync",
|
||||
progress_type="system",
|
||||
status="in_progress",
|
||||
title="Syncing Series Database",
|
||||
message=f"Synced {sync_count} series from data files",
|
||||
percent=75,
|
||||
metadata={"step_id": "series_sync"}
|
||||
)
|
||||
|
||||
# Mark initial scan as completed
|
||||
try:
|
||||
async with get_db_session() as db:
|
||||
@@ -94,6 +153,17 @@ async def perform_initial_setup():
|
||||
await anime_service._load_series_from_db()
|
||||
logger.info("Series loaded from database into memory")
|
||||
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="series_sync",
|
||||
progress_type="system",
|
||||
status="completed",
|
||||
title="Syncing Series Database",
|
||||
message="Series loaded into memory",
|
||||
percent=100,
|
||||
metadata={"step_id": "series_sync"}
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except (OSError, RuntimeError, ValueError) as e:
|
||||
@@ -101,11 +171,26 @@ async def perform_initial_setup():
|
||||
return False
|
||||
|
||||
|
||||
async def perform_nfo_scan_if_needed():
|
||||
"""Perform initial NFO scan if not yet completed and configured."""
|
||||
async def perform_nfo_scan_if_needed(progress_service=None):
|
||||
"""Perform initial NFO scan if not yet completed and configured.
|
||||
|
||||
Args:
|
||||
progress_service: Optional ProgressService instance for emitting updates
|
||||
"""
|
||||
from src.server.database.connection import get_db_session
|
||||
from src.server.database.system_settings_service import SystemSettingsService
|
||||
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="nfo_scan",
|
||||
progress_type="system",
|
||||
status="started",
|
||||
title="Processing NFO Metadata",
|
||||
message="Checking NFO scan status...",
|
||||
percent=0,
|
||||
metadata={"step_id": "nfo_scan"}
|
||||
)
|
||||
|
||||
# Check if initial NFO scan has been completed
|
||||
try:
|
||||
async with get_db_session() as db:
|
||||
@@ -126,16 +211,51 @@ async def perform_nfo_scan_if_needed():
|
||||
):
|
||||
if not is_nfo_scan_done:
|
||||
logger.info("Performing initial NFO scan...")
|
||||
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="nfo_scan",
|
||||
progress_type="system",
|
||||
status="in_progress",
|
||||
title="Processing NFO Metadata",
|
||||
message="Scanning series for NFO files...",
|
||||
percent=25,
|
||||
metadata={"step_id": "nfo_scan"}
|
||||
)
|
||||
|
||||
try:
|
||||
from src.core.services.series_manager_service import (
|
||||
SeriesManagerService,
|
||||
)
|
||||
|
||||
manager = SeriesManagerService.from_settings()
|
||||
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="nfo_scan",
|
||||
progress_type="system",
|
||||
status="in_progress",
|
||||
title="Processing NFO Metadata",
|
||||
message="Processing NFO files with TMDB data...",
|
||||
percent=50,
|
||||
metadata={"step_id": "nfo_scan"}
|
||||
)
|
||||
|
||||
await manager.scan_and_process_nfo()
|
||||
await manager.close()
|
||||
logger.info("Initial NFO scan completed")
|
||||
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="nfo_scan",
|
||||
progress_type="system",
|
||||
status="completed",
|
||||
title="Processing NFO Metadata",
|
||||
message="NFO scan completed successfully",
|
||||
percent=100,
|
||||
metadata={"step_id": "nfo_scan"}
|
||||
)
|
||||
|
||||
# Mark NFO scan as completed
|
||||
try:
|
||||
async with get_db_session() as db:
|
||||
@@ -155,15 +275,49 @@ async def perform_nfo_scan_if_needed():
|
||||
e,
|
||||
exc_info=True
|
||||
)
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="nfo_scan",
|
||||
progress_type="system",
|
||||
status="failed",
|
||||
title="Processing NFO Metadata",
|
||||
message=f"NFO scan failed: {str(e)}",
|
||||
percent=0,
|
||||
metadata={"step_id": "nfo_scan"}
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"Skipping NFO scan - already completed on previous run"
|
||||
)
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="nfo_scan",
|
||||
progress_type="system",
|
||||
status="completed",
|
||||
title="Processing NFO Metadata",
|
||||
message="Already completed",
|
||||
percent=100,
|
||||
metadata={"step_id": "nfo_scan"}
|
||||
)
|
||||
else:
|
||||
if not settings.tmdb_api_key:
|
||||
logger.info(
|
||||
"NFO scan skipped - TMDB API key not configured"
|
||||
)
|
||||
message = "Skipped - TMDB API key not configured"
|
||||
else:
|
||||
message = "Skipped - NFO features disabled"
|
||||
|
||||
if progress_service:
|
||||
progress_service.emit_progress(
|
||||
progress_id="nfo_scan",
|
||||
progress_type="system",
|
||||
status="completed",
|
||||
title="Processing NFO Metadata",
|
||||
message=message,
|
||||
percent=100,
|
||||
metadata={"step_id": "nfo_scan"}
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"NFO scan skipped - auto_create and update_on_scan "
|
||||
|
||||
487
src/server/web/templates/loading.html
Normal file
487
src/server/web/templates/loading.html
Normal file
@@ -0,0 +1,487 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="light">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AniWorld Manager - Initializing</title>
|
||||
<link rel="stylesheet" href="/static/css/styles.css">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.loading-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--color-primary-light) 0%, var(--color-primary) 100%);
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.loading-card {
|
||||
background: var(--color-surface);
|
||||
border-radius: 16px;
|
||||
padding: 3rem 2rem;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.loading-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.loading-header .logo {
|
||||
font-size: 3.5rem;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 1rem;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-header h1 {
|
||||
margin: 0;
|
||||
color: var(--color-text);
|
||||
font-size: 1.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading-header p {
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.progress-step {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
background: var(--color-background);
|
||||
border-left: 4px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-step.active {
|
||||
border-left-color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.progress-step.completed {
|
||||
border-left-color: var(--color-success);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.progress-step.error {
|
||||
border-left-color: var(--color-error);
|
||||
}
|
||||
|
||||
.step-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
font-size: 1.2rem;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.step-icon.loading {
|
||||
color: var(--color-primary);
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.step-icon.completed {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.step-icon.error {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
font-size: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.step-status {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.step-message {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-left: 2rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.step-progress {
|
||||
margin-left: 2rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
background: var(--color-border);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--color-primary);
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 0.25rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: var(--color-error-light);
|
||||
border: 1px solid var(--color-error);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.error-message i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.completion-message {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.completion-message i {
|
||||
font-size: 3rem;
|
||||
color: var(--color-success);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.completion-message h2 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.completion-message p {
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.btn-continue {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-continue:hover {
|
||||
background: var(--color-primary-dark);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.connection-status.connected {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.connection-status.disconnected {
|
||||
color: var(--color-error);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="loading-container">
|
||||
<div class="loading-card">
|
||||
<div class="loading-header">
|
||||
<div class="logo">
|
||||
<i class="fas fa-tv"></i>
|
||||
</div>
|
||||
<h1>Initializing AniWorld Manager</h1>
|
||||
<p>Please wait while we set up your anime library...</p>
|
||||
</div>
|
||||
|
||||
<div class="progress-container" id="progressContainer">
|
||||
<!-- Steps will be dynamically added here -->
|
||||
</div>
|
||||
|
||||
<div class="connection-status" id="connectionStatus">
|
||||
<i class="fas fa-circle-notch fa-spin"></i> Connecting...
|
||||
</div>
|
||||
|
||||
<div class="completion-message" id="completionMessage" style="display: none;">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<h2>Initialization Complete!</h2>
|
||||
<p>Your anime library is ready to use.</p>
|
||||
<button class="btn-continue" onclick="continueToApp()">
|
||||
<i class="fas fa-arrow-right"></i> Continue to AniWorld Manager
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="error-message" id="errorMessage" style="display: none;">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<span id="errorText"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
const steps = new Map();
|
||||
let isComplete = false;
|
||||
|
||||
const stepOrder = [
|
||||
'series_sync',
|
||||
'nfo_scan',
|
||||
'media_scan'
|
||||
];
|
||||
|
||||
const stepTitles = {
|
||||
'series_sync': 'Syncing Series Database',
|
||||
'nfo_scan': 'Processing NFO Metadata',
|
||||
'media_scan': 'Scanning Media Files'
|
||||
};
|
||||
|
||||
function connectWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws/progress`;
|
||||
|
||||
ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket connected');
|
||||
updateConnectionStatus(true);
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('Progress update:', data);
|
||||
handleProgressUpdate(data);
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
updateConnectionStatus(false);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
updateConnectionStatus(false);
|
||||
|
||||
// Reconnect after delay if not complete
|
||||
if (!isComplete) {
|
||||
setTimeout(connectWebSocket, 3000);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function updateConnectionStatus(connected) {
|
||||
const statusEl = document.getElementById('connectionStatus');
|
||||
if (connected) {
|
||||
statusEl.className = 'connection-status connected';
|
||||
statusEl.innerHTML = '<i class="fas fa-check-circle"></i> Connected';
|
||||
} else {
|
||||
statusEl.className = 'connection-status disconnected';
|
||||
statusEl.innerHTML = '<i class="fas fa-times-circle"></i> Disconnected - Reconnecting...';
|
||||
}
|
||||
}
|
||||
|
||||
function handleProgressUpdate(data) {
|
||||
const { type, status, title, message, percent, current, total, metadata } = data;
|
||||
|
||||
// Determine step ID based on type and metadata
|
||||
let stepId = metadata?.step_id || type;
|
||||
|
||||
// Update or create step
|
||||
if (!steps.has(stepId)) {
|
||||
createStep(stepId, title || stepTitles[stepId] || stepId);
|
||||
}
|
||||
|
||||
updateStep(stepId, status, message, percent, current, total);
|
||||
|
||||
// Check for completion
|
||||
if (metadata?.initialization_complete) {
|
||||
showCompletion();
|
||||
}
|
||||
|
||||
// Handle errors
|
||||
if (status === 'failed') {
|
||||
showError(message || 'An error occurred during initialization');
|
||||
}
|
||||
}
|
||||
|
||||
function createStep(stepId, title) {
|
||||
const container = document.getElementById('progressContainer');
|
||||
|
||||
const stepEl = document.createElement('div');
|
||||
stepEl.id = `step-${stepId}`;
|
||||
stepEl.className = 'progress-step';
|
||||
stepEl.innerHTML = `
|
||||
<div class="step-header">
|
||||
<i class="fas fa-circle-notch fa-spin step-icon loading"></i>
|
||||
<span class="step-title">${title}</span>
|
||||
<span class="step-status">Waiting...</span>
|
||||
</div>
|
||||
<div class="step-message"></div>
|
||||
<div class="step-progress" style="display: none;">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-fill" style="width: 0%;"></div>
|
||||
</div>
|
||||
<div class="progress-text">0%</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert in correct order
|
||||
const existingSteps = Array.from(steps.keys());
|
||||
let insertBefore = null;
|
||||
|
||||
for (const orderId of stepOrder) {
|
||||
if (orderId === stepId) break;
|
||||
if (!existingSteps.includes(orderId)) {
|
||||
continue;
|
||||
}
|
||||
insertBefore = null;
|
||||
}
|
||||
|
||||
for (const orderId of stepOrder.slice(stepOrder.indexOf(stepId) + 1)) {
|
||||
const el = document.getElementById(`step-${orderId}`);
|
||||
if (el) {
|
||||
insertBefore = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (insertBefore) {
|
||||
container.insertBefore(stepEl, insertBefore);
|
||||
} else {
|
||||
container.appendChild(stepEl);
|
||||
}
|
||||
|
||||
steps.set(stepId, stepEl);
|
||||
}
|
||||
|
||||
function updateStep(stepId, status, message, percent, current, total) {
|
||||
const stepEl = steps.get(stepId);
|
||||
if (!stepEl) return;
|
||||
|
||||
const iconEl = stepEl.querySelector('.step-icon');
|
||||
const statusEl = stepEl.querySelector('.step-status');
|
||||
const messageEl = stepEl.querySelector('.step-message');
|
||||
const progressEl = stepEl.querySelector('.step-progress');
|
||||
const progressFillEl = stepEl.querySelector('.progress-bar-fill');
|
||||
const progressTextEl = stepEl.querySelector('.progress-text');
|
||||
|
||||
// Update status
|
||||
stepEl.className = 'progress-step';
|
||||
if (status === 'started' || status === 'in_progress') {
|
||||
stepEl.classList.add('active');
|
||||
iconEl.className = 'fas fa-circle-notch fa-spin step-icon loading';
|
||||
statusEl.textContent = 'In Progress...';
|
||||
} else if (status === 'completed') {
|
||||
stepEl.classList.add('completed');
|
||||
iconEl.className = 'fas fa-check-circle step-icon completed';
|
||||
statusEl.textContent = 'Complete';
|
||||
} else if (status === 'failed') {
|
||||
stepEl.classList.add('error');
|
||||
iconEl.className = 'fas fa-exclamation-circle step-icon error';
|
||||
statusEl.textContent = 'Failed';
|
||||
}
|
||||
|
||||
// Update message
|
||||
if (message) {
|
||||
messageEl.textContent = message;
|
||||
messageEl.style.display = 'block';
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
if (percent > 0 || (current > 0 && total > 0)) {
|
||||
const actualPercent = percent || (current / total * 100);
|
||||
progressEl.style.display = 'block';
|
||||
progressFillEl.style.width = `${actualPercent}%`;
|
||||
progressTextEl.textContent = `${Math.round(actualPercent)}% (${current}/${total})`;
|
||||
}
|
||||
}
|
||||
|
||||
function showCompletion() {
|
||||
isComplete = true;
|
||||
document.getElementById('completionMessage').style.display = 'block';
|
||||
document.getElementById('connectionStatus').style.display = 'none';
|
||||
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const errorEl = document.getElementById('errorMessage');
|
||||
const errorTextEl = document.getElementById('errorText');
|
||||
errorTextEl.textContent = message;
|
||||
errorEl.style.display = 'block';
|
||||
}
|
||||
|
||||
function continueToApp() {
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
// Start WebSocket connection when page loads
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
connectWebSocket();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -703,10 +703,18 @@
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.status === 'ok') {
|
||||
showMessage('Setup completed successfully! Redirecting to login...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
// Redirect to loading page if provided, otherwise go to login
|
||||
if (data.redirect) {
|
||||
showMessage('Setup saved! Initializing your anime library...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = data.redirect;
|
||||
}, 500);
|
||||
} else {
|
||||
showMessage('Setup completed successfully! Redirecting to login...', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
const errorMessage = data.detail || data.message || 'Setup failed';
|
||||
showMessage(errorMessage, 'error');
|
||||
|
||||
Reference in New Issue
Block a user