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.
|
"""Initial setup endpoint to configure the master password.
|
||||||
|
|
||||||
This endpoint also initializes the configuration with all provided values
|
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():
|
if auth_service.is_configured():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -117,17 +118,9 @@ async def setup_auth(req: SetupRequest):
|
|||||||
# Save the config with all updates
|
# Save the config with all updates
|
||||||
config_service.save_config(config, create_backup=False)
|
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
|
# Sync config.json values to settings object
|
||||||
# (mirroring the logic in fastapi_app.py lifespan)
|
# (mirroring the logic in fastapi_app.py lifespan)
|
||||||
|
from src.config.settings import settings
|
||||||
other_settings = dict(config.other) if config.other else {}
|
other_settings = dict(config.other) if config.other else {}
|
||||||
if other_settings.get("anime_directory"):
|
if other_settings.get("anime_directory"):
|
||||||
settings.anime_directory = str(other_settings["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_download_fanart = config.nfo.download_fanart
|
||||||
settings.nfo_image_size = config.nfo.image_size
|
settings.nfo_image_size = config.nfo.image_size
|
||||||
|
|
||||||
# Perform the initial series sync and mark as completed
|
# Trigger initialization in background task
|
||||||
await perform_initial_setup()
|
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
|
progress_service = get_progress_service()
|
||||||
await perform_nfo_scan_if_needed()
|
|
||||||
|
|
||||||
|
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
|
# Note: Media scan is skipped during setup as it requires
|
||||||
# background_loader service which is only available during
|
# background_loader service which is only available during
|
||||||
# application lifespan. It will run on first application startup.
|
# application lifespan. It will run on first application startup.
|
||||||
|
|||||||
@@ -49,3 +49,13 @@ async def queue_page(request: Request):
|
|||||||
request,
|
request,
|
||||||
title="Download Queue - Aniworld"
|
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."""
|
"""Centralized initialization service for application startup and setup."""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from src.config.settings import settings
|
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__)
|
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.
|
"""Perform initial setup including series sync and scan completion marking.
|
||||||
|
|
||||||
This function is called both during application lifespan startup
|
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
|
4. NFO scan is performed if configured
|
||||||
5. Media scan is performed
|
5. Media scan is performed
|
||||||
|
|
||||||
|
Args:
|
||||||
|
progress_service: Optional ProgressService instance for emitting updates
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if initialization was performed, False if skipped
|
bool: True if initialization was performed, False if skipped
|
||||||
"""
|
"""
|
||||||
from src.server.database.connection import get_db_session
|
from src.server.database.connection import get_db_session
|
||||||
from src.server.database.system_settings_service import SystemSettingsService
|
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
|
# Check if initial setup has been completed
|
||||||
try:
|
try:
|
||||||
async with get_db_session() as db:
|
async with get_db_session() as db:
|
||||||
@@ -35,6 +52,16 @@ async def perform_initial_setup():
|
|||||||
logger.info(
|
logger.info(
|
||||||
"Initial scan already completed, skipping data file sync"
|
"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
|
return False
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -58,11 +85,32 @@ async def perform_initial_setup():
|
|||||||
logger.info(
|
logger.info(
|
||||||
"Initialization skipped - anime directory not configured"
|
"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
|
return False
|
||||||
|
|
||||||
# Only sync from data files on first run
|
# Only sync from data files on first run
|
||||||
if not is_initial_scan_done:
|
if not is_initial_scan_done:
|
||||||
logger.info("Performing initial anime folder scan...")
|
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(
|
sync_count = await sync_series_from_data_files(
|
||||||
settings.anime_directory
|
settings.anime_directory
|
||||||
)
|
)
|
||||||
@@ -70,6 +118,17 @@ async def perform_initial_setup():
|
|||||||
"Data file sync complete. Added %d series.", sync_count
|
"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
|
# Mark initial scan as completed
|
||||||
try:
|
try:
|
||||||
async with get_db_session() as db:
|
async with get_db_session() as db:
|
||||||
@@ -94,6 +153,17 @@ async def perform_initial_setup():
|
|||||||
await anime_service._load_series_from_db()
|
await anime_service._load_series_from_db()
|
||||||
logger.info("Series loaded from database into memory")
|
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
|
return True
|
||||||
|
|
||||||
except (OSError, RuntimeError, ValueError) as e:
|
except (OSError, RuntimeError, ValueError) as e:
|
||||||
@@ -101,11 +171,26 @@ async def perform_initial_setup():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def perform_nfo_scan_if_needed():
|
async def perform_nfo_scan_if_needed(progress_service=None):
|
||||||
"""Perform initial NFO scan if not yet completed and configured."""
|
"""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.connection import get_db_session
|
||||||
from src.server.database.system_settings_service import SystemSettingsService
|
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
|
# Check if initial NFO scan has been completed
|
||||||
try:
|
try:
|
||||||
async with get_db_session() as db:
|
async with get_db_session() as db:
|
||||||
@@ -126,16 +211,51 @@ async def perform_nfo_scan_if_needed():
|
|||||||
):
|
):
|
||||||
if not is_nfo_scan_done:
|
if not is_nfo_scan_done:
|
||||||
logger.info("Performing initial NFO scan...")
|
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:
|
try:
|
||||||
from src.core.services.series_manager_service import (
|
from src.core.services.series_manager_service import (
|
||||||
SeriesManagerService,
|
SeriesManagerService,
|
||||||
)
|
)
|
||||||
|
|
||||||
manager = SeriesManagerService.from_settings()
|
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.scan_and_process_nfo()
|
||||||
await manager.close()
|
await manager.close()
|
||||||
logger.info("Initial NFO scan completed")
|
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
|
# Mark NFO scan as completed
|
||||||
try:
|
try:
|
||||||
async with get_db_session() as db:
|
async with get_db_session() as db:
|
||||||
@@ -155,15 +275,49 @@ async def perform_nfo_scan_if_needed():
|
|||||||
e,
|
e,
|
||||||
exc_info=True
|
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:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Skipping NFO scan - already completed on previous run"
|
"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:
|
else:
|
||||||
if not settings.tmdb_api_key:
|
if not settings.tmdb_api_key:
|
||||||
logger.info(
|
logger.info(
|
||||||
"NFO scan skipped - TMDB API key not configured"
|
"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:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"NFO scan skipped - auto_create and update_on_scan "
|
"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();
|
const data = await response.json();
|
||||||
|
|
||||||
if (response.ok && data.status === 'ok') {
|
if (response.ok && data.status === 'ok') {
|
||||||
showMessage('Setup completed successfully! Redirecting to login...', 'success');
|
// Redirect to loading page if provided, otherwise go to login
|
||||||
setTimeout(() => {
|
if (data.redirect) {
|
||||||
window.location.href = '/login';
|
showMessage('Setup saved! Initializing your anime library...', 'success');
|
||||||
}, 2000);
|
setTimeout(() => {
|
||||||
|
window.location.href = data.redirect;
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
showMessage('Setup completed successfully! Redirecting to login...', 'success');
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = data.detail || data.message || 'Setup failed';
|
const errorMessage = data.detail || data.message || 'Setup failed';
|
||||||
showMessage(errorMessage, 'error');
|
showMessage(errorMessage, 'error');
|
||||||
|
|||||||
Reference in New Issue
Block a user