diff --git a/py/routes/handlers/recipe_handlers.py b/py/routes/handlers/recipe_handlers.py index c7761ac5..af04a0e1 100644 --- a/py/routes/handlers/recipe_handlers.py +++ b/py/routes/handlers/recipe_handlers.py @@ -176,11 +176,19 @@ class RecipePageView: user_language = self._settings.get("language", "en") self._server_i18n.set_locale(user_language) + # While the initial scan is running, show the initialization + # screen (same as the model pages) instead of an empty grid; the + # page reloads itself when the scanner broadcasts completion. + is_initializing = ( + recipe_scanner._cache is None or recipe_scanner.is_initializing() + ) + try: - await recipe_scanner.get_cached_data(force_refresh=False) + if not is_initializing: + await recipe_scanner.get_cached_data(force_refresh=False) rendered = self._template_env.get_template(self._template_name).render( recipes=[], - is_initializing=False, + is_initializing=is_initializing, settings=self._settings, request=request, t=self._server_i18n.get_translation, diff --git a/py/services/recipe_scanner.py b/py/services/recipe_scanner.py index f18f8670..8ed2b9ca 100644 --- a/py/services/recipe_scanner.py +++ b/py/services/recipe_scanner.py @@ -19,6 +19,7 @@ from ..utils.recipe_open_stats import RecipeOpenStats from .model_scanner import WEIGHT_FILE_EXTENSIONS from .recipe_cache import RecipeCache from .recipes.errors import RecipeNotFoundError, RecipePersistenceError +from .websocket_manager import ws_manager from natsort import natsorted import sys import re @@ -481,6 +482,10 @@ class RecipeScanner: return str(value) return "unknown" + def is_initializing(self) -> bool: + """Check if the scanner is currently initializing""" + return self._is_initializing + def on_library_changed(self) -> None: """Reset cached state when the active library changes.""" @@ -1410,6 +1415,14 @@ class RecipeScanner: self._is_initializing = True self._initialization_task = asyncio.current_task() try: + await ws_manager.broadcast_init_progress({ + 'stage': 'loading_cache', + 'progress': 0, + 'details': 'Loading recipe cache...', + 'scanner_type': 'recipe', + 'pageType': 'recipes', + }) + await self._wait_for_lora_scanner() # Set initial empty cache to avoid None reference errors @@ -1442,11 +1455,38 @@ class RecipeScanner: logger.info( f"Recipe cache initialized in {elapsed_time:.2f} seconds. Found {recipe_count} recipes" ) + await ws_manager.broadcast_init_progress({ + 'stage': 'finalizing', + 'progress': 100, + 'status': 'complete', + 'details': f'Found {recipe_count} recipes.', + 'scanner_type': 'recipe', + 'pageType': 'recipes', + }) self._schedule_post_scan_enrichment() # Schedule FTS index build in background (non-blocking) self._schedule_fts_index_build() except Exception as e: logger.error(f"Recipe Scanner: Error initializing cache in background: {e}") + # Ensure the cache is never None so the page stops showing the + # initialization screen, and let waiting clients reload into the + # regular (possibly empty) view instead of stalling. + if self._cache is None: + self._cache = RecipeCache( + raw_data=[], + sorted_by_name=[], + sorted_by_date=[], + folders=[], + folder_tree={}, + ) + await ws_manager.broadcast_init_progress({ + 'stage': 'finalizing', + 'progress': 100, + 'status': 'complete', + 'details': 'Recipe cache initialization failed.', + 'scanner_type': 'recipe', + 'pageType': 'recipes', + }) finally: # Mark initialization as complete regardless of outcome self._is_initializing = False diff --git a/static/js/components/initialization.js b/static/js/components/initialization.js index 707ff07d..00a9409a 100644 --- a/static/js/components/initialization.js +++ b/static/js/components/initialization.js @@ -52,7 +52,11 @@ class InitializationManager { detectPageType() { // Get the current page type from URL or data attribute const path = window.location.pathname; - if (path.includes('/checkpoints')) { + // The recipes page lives at /loras/recipes, so it must be matched + // before the generic '/loras' check. + if (path.includes('/recipes')) { + this.pageType = 'recipes'; + } else if (path.includes('/checkpoints')) { this.pageType = 'checkpoints'; } else if (path.includes('/loras')) { this.pageType = 'loras'; @@ -216,7 +220,8 @@ class InitializationManager { const scannerTypeToPageType = { 'lora': 'loras', 'checkpoint': 'checkpoints', - 'embedding': 'embeddings' + 'embedding': 'embeddings', + 'recipe': 'recipes' }; if (scannerTypeToPageType[data.scanner_type] !== this.pageType) {