diff --git a/py/routes/handlers/model_handlers.py b/py/routes/handlers/model_handlers.py index cfab2835..2968fbf8 100644 --- a/py/routes/handlers/model_handlers.py +++ b/py/routes/handlers/model_handlers.py @@ -50,6 +50,7 @@ from ...services.errors import RateLimitError, ResourceNotFoundError from ...utils.civitai_utils import resolve_license_payload from ...utils.file_utils import calculate_sha256 from ...utils.metadata_manager import MetadataManager +from ...utils.url_utils import relative_root_prefix LICENSE_FIELDS = ( "allowNoCredit", @@ -204,6 +205,7 @@ class ModelPageView: "version": self._get_app_version(), "provider_presets_json": json.dumps(PROVIDER_PRESETS), "provider_models_json": "{}", + "rel_prefix": relative_root_prefix(request.path), } if not is_initializing: diff --git a/py/routes/handlers/recipe_handlers.py b/py/routes/handlers/recipe_handlers.py index 9b26ba10..e0661e47 100644 --- a/py/routes/handlers/recipe_handlers.py +++ b/py/routes/handlers/recipe_handlers.py @@ -36,6 +36,7 @@ from ...utils.constants import NSFW_LEVELS from ...utils.directory_browser import WINDOWS_DRIVES_TOKEN, browse_directory from ...utils.exif_utils import ExifUtils from ...utils.recipe_open_stats import RecipeOpenStats +from ...utils.url_utils import relative_root_prefix from ...recipes.merger import GenParamsMerger from ...recipes.enrichment import RecipeEnricher from ...services.websocket_manager import ws_manager as default_ws_manager @@ -215,6 +216,7 @@ class RecipePageView: settings=self._settings, request=request, t=self._server_i18n.get_translation, + rel_prefix=relative_root_prefix(request.path), ) except Exception as cache_error: # pragma: no cover - logging path self._logger.error("Error loading recipe cache data: %s", cache_error) @@ -223,6 +225,7 @@ class RecipePageView: settings=self._settings, request=request, t=self._server_i18n.get_translation, + rel_prefix=relative_root_prefix(request.path), ) return web.Response(text=rendered, content_type="text/html") except Exception as exc: # pragma: no cover - logging path diff --git a/py/routes/stats_routes.py b/py/routes/stats_routes.py index da520ab9..f0963286 100644 --- a/py/routes/stats_routes.py +++ b/py/routes/stats_routes.py @@ -13,6 +13,7 @@ from ..services.server_i18n import server_i18n from ..services.service_registry import ServiceRegistry from ..services.model_query import normalize_sub_type, resolve_sub_type from ..utils.constants import VALID_LORA_SUB_TYPES, VALID_CHECKPOINT_SUB_TYPES +from ..utils.url_utils import relative_root_prefix from ..utils.usage_stats import UsageStats logger = logging.getLogger(__name__) @@ -106,6 +107,7 @@ class StatsRoutes: settings=settings_manager, request=request, t=server_i18n.get_translation, + rel_prefix=relative_root_prefix(request.path), ) return web.Response( diff --git a/py/utils/url_utils.py b/py/utils/url_utils.py new file mode 100644 index 00000000..a8581538 --- /dev/null +++ b/py/utils/url_utils.py @@ -0,0 +1,18 @@ +"""Helpers for generating URLs that survive reverse-proxy subpath mounts.""" + +from __future__ import annotations + + +def relative_root_prefix(request_path: str) -> str: + """Return the relative prefix ("", "../", ...) that takes a manager page + back to the mount root. + + Templates reference assets and pages with relative URLs (e.g. + ``{{ rel_prefix }}loras_static/...``) so the browser keeps whatever + subpath a reverse proxy (llama-swap, SwarmUI, ...) mounted ComfyUI under. + The backend only ever sees the stripped path, so the depth of the page + route is all that matters: "/loras" -> "", "/loras/recipes" -> "../". + """ + + segments = [segment for segment in request_path.split("/") if segment] + return "../" * max(len(segments) - 1, 0) diff --git a/static/js/components/ContextMenu/RecipeContextMenu.js b/static/js/components/ContextMenu/RecipeContextMenu.js index c11e5ef5..69d17061 100644 --- a/static/js/components/ContextMenu/RecipeContextMenu.js +++ b/static/js/components/ContextMenu/RecipeContextMenu.js @@ -9,6 +9,7 @@ import { moveManager } from '../../managers/MoveManager.js'; import { rematchModalManager } from '../../managers/RematchModalManager.js'; import { showRematchSummary } from '../RematchSummaryModal.js'; import { probeExtension, delegateReimport, getCivitaiImageInfo } from '../../utils/extensionReimportBridge.js'; +import { withBasePath } from '../../utils/basePath.js'; export class RecipeContextMenu extends BaseContextMenu { constructor() { @@ -180,7 +181,7 @@ export class RecipeContextMenu extends BaseContextMenu { setSessionItem('filterRecipeName', recipe.title); // Navigate to the LoRAs page - window.location.href = '/loras'; + window.location.href = withBasePath('/loras'); } else { showToast('recipes.contextMenu.viewLoras.noLorasFound', {}, 'info'); } diff --git a/static/js/components/RecipeModal.js b/static/js/components/RecipeModal.js index 262b2709..68dbd664 100644 --- a/static/js/components/RecipeModal.js +++ b/static/js/components/RecipeModal.js @@ -7,6 +7,7 @@ import { state } from '../state/index.js'; import { setSessionItem, removeSessionItem, getStorageItem, setStorageItem } from '../utils/storageHelpers.js'; import { fetchRecipeDetails, updateRecipeMetadata, sendRecipeWorkflow, extractRecipeId } from '../api/recipeApi.js'; import { downloadManager } from '../managers/DownloadManager.js'; +import { withBasePath } from '../utils/basePath.js'; import { MODEL_TYPES } from '../api/apiConfig.js'; import { openMediaViewer } from './shared/MediaViewer.js'; import { showRecipeDeleteConfirmation } from './RecipeCard.js'; @@ -3231,7 +3232,7 @@ class RecipeModal { setSessionItem('filterCheckpointRecipeName', this.currentRecipe.title); } - window.location.href = '/checkpoints'; + window.location.href = withBasePath('/checkpoints'); } _getCheckpointHash(checkpoint) { @@ -3281,7 +3282,7 @@ class RecipeModal { } // Navigate to the LoRAs page - window.location.href = '/loras'; + window.location.href = withBasePath('/loras'); } // Only in-library LoRA items are row-navigable: the row opens the local diff --git a/static/js/components/shared/RecipeTab.js b/static/js/components/shared/RecipeTab.js index a17bfc74..bd508855 100644 --- a/static/js/components/shared/RecipeTab.js +++ b/static/js/components/shared/RecipeTab.js @@ -3,6 +3,7 @@ */ import { showToast, copyToClipboard } from '../../utils/uiHelpers.js'; import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js'; +import { withBasePath } from '../../utils/basePath.js'; /** * Loads recipes that use the specified model and renders them in the tab. @@ -356,7 +357,7 @@ function navigateToRecipesPage({ modelKind, displayName, modelHash }) { } // Directly navigate to recipes page - window.location.href = '/loras/recipes'; + window.location.href = withBasePath('/loras/recipes'); } /** @@ -378,7 +379,7 @@ function navigateToRecipeDetails(recipeId) { setSessionItem('viewRecipeId', recipeId); // Directly navigate to recipes page - window.location.href = '/loras/recipes'; + window.location.href = withBasePath('/loras/recipes'); } function getRecipesEndpoint(modelKind) { diff --git a/static/js/utils/basePath.js b/static/js/utils/basePath.js new file mode 100644 index 00000000..6236d1ba --- /dev/null +++ b/static/js/utils/basePath.js @@ -0,0 +1,16 @@ +/** + * Base-path helpers for the manager pages. + * + * `window.LM_BASE_PATH` is set by the inline bootstrap in + * templates/components/base_path_bootstrap.html: it is the reverse-proxy + * subpath the manager is served under (e.g. "/comfyui"), or "" for normal + * and standalone deployments. + */ + +export function getBasePath() { + return window.LM_BASE_PATH || ''; +} + +export function withBasePath(path) { + return `${getBasePath()}${path}`; +} diff --git a/templates/base.html b/templates/base.html index ff67f783..15c89666 100644 --- a/templates/base.html +++ b/templates/base.html @@ -2,19 +2,20 @@ + {% include 'components/base_path_bootstrap.html' %} {% block title %}{{ t('header.appTitle') }}{% endblock %} - - - + + + {% block page_css %}{% endblock %} - - - - + + + - + + {% else %} {% block main_script %}{% endblock %} {% endif %} diff --git a/templates/checkpoints.html b/templates/checkpoints.html index fb82eed4..a64bace4 100644 --- a/templates/checkpoints.html +++ b/templates/checkpoints.html @@ -75,5 +75,5 @@ {% endblock %} {% block main_script %} - + {% endblock %} diff --git a/templates/components/base_path_bootstrap.html b/templates/components/base_path_bootstrap.html new file mode 100644 index 00000000..c857a7d1 --- /dev/null +++ b/templates/components/base_path_bootstrap.html @@ -0,0 +1,195 @@ +{# + Base-path bootstrap. Must be the FIRST element in : it detects the + reverse-proxy subpath (e.g. "/comfyui" when served via llama-swap, or + "/ComfyBackendDirect" via SwarmUI) from the current page URL and, only when + a prefix exists, patches fetch/XHR/WebSocket/innerHTML/DOM URL setters so + that root-absolute URLs ("/api/lm/...", "/loras_static/...") generated + anywhere in the frontend or in backend JSON payloads get the prefix + prepended. When no prefix is detected (normal or standalone deployment) + nothing is patched and behavior is byte-identical to before. +#} + diff --git a/templates/components/header.html b/templates/components/header.html index bfe75268..be61399b 100644 --- a/templates/components/header.html +++ b/templates/components/header.html @@ -3,8 +3,8 @@
- - + + {{ t('header.appTitle') }}
@@ -23,26 +23,26 @@ {% set current_page = 'loras' %} {% endif %}