@@ -188,7 +188,6 @@ class GrpcServiceNameConflictError(GrpcServiceError): # type: ignore
188188# This will be set by main.py when it imports admin_router
189189logging_service: Optional[LoggingService] = None
190190LOGGER: logging.Logger = logging.getLogger("mcpgateway.admin")
191-
192191UI_SECTION_TO_TABS: Dict[str, tuple[str, ...]] = {
193192 "overview": ("overview",),
194193 "servers": ("catalog",),
@@ -214,6 +213,50 @@ class GrpcServiceNameConflictError(GrpcServiceError): # type: ignore
214213UI_HIDE_SECTIONS_COOKIE_NAME = "mcpgateway_ui_hide_sections"
215214UI_HIDE_SECTIONS_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 days
216215
216+ # Cache for the bundle filename to avoid reading manifest on every request
217+ # Using a mutable dict to avoid the need for a global statement in the accessor function
218+ _bundle_js_cache: dict[str, Optional[str]] = {"filename": None}
219+
220+
221+ def get_bundle_js_filename() -> str:
222+ """Get the hashed bundle.js filename from Vite manifest.
223+
224+ Reads the Vite manifest file to get the current hashed bundle filename.
225+ Falls back to scanning for bundle-*.js on disk if the manifest is unreadable.
226+ Invalidates the cache when the cached bundle file no longer exists on disk.
227+
228+ Returns:
229+ str: The bundle filename (e.g., 'bundle-abc123.js')
230+ """
231+ static_dir = Path(__file__).parent / "static"
232+
233+ # Use cache if the bundle file still exists on disk
234+ cached = _bundle_js_cache["filename"]
235+ if cached is not None and (static_dir / cached).exists():
236+ return cached
237+
238+ manifest_path = static_dir / ".vite" / "manifest.json"
239+ try:
240+ if manifest_path.exists():
241+ with open(manifest_path, "r", encoding="utf-8") as f:
242+ manifest = orjson.loads(f.read())
243+ # The key is the input path relative to the project root
244+ entry_key = "mcpgateway/admin_ui/index.js"
245+ if entry_key in manifest and manifest[entry_key].get("file"):
246+ _bundle_js_cache["filename"] = manifest[entry_key]["file"]
247+ return _bundle_js_cache["filename"] # type: ignore[return-value]
248+ except Exception as e:
249+ LOGGER.warning(f"Failed to read Vite manifest: {e}")
250+
251+ # Manifest unreadable or missing entry — find bundle file directly on disk
252+ bundles = sorted(static_dir.glob("bundle-*.js"), key=lambda p: p.stat().st_mtime, reverse=True)
253+ if bundles:
254+ _bundle_js_cache["filename"] = bundles[0].name
255+ return _bundle_js_cache["filename"] # type: ignore[return-value]
256+
257+ LOGGER.error("No bundle-*.js found in %s — admin UI will not load", static_dir)
258+ return ""
259+
217260
218261def _normalize_ui_hide_values(raw: Any, valid_values: frozenset[str], aliases: Optional[Dict[str, str]] = None) -> set[str]:
219262 """Normalize UI hide values from CSV/list input into a validated set.
@@ -3548,6 +3591,7 @@ def _to_dict_and_filter(raw_list):
35483591 "roots": roots,
35493592 "include_inactive": include_inactive,
35503593 "root_path": root_path,
3594+ "bundle_js": get_bundle_js_filename(),
35513595 "max_name_length": max_name_length,
35523596 "gateway_tool_name_separator": settings.gateway_tool_name_separator,
35533597 "bulk_import_max_tools": settings.mcpgateway_bulk_import_max_tools,
@@ -5569,7 +5613,7 @@ async def admin_get_team_edit(
55695613 </select>
55705614 </div>
55715615 <div class="flex justify-end space-x-3">
5572- <button type="button" onclick="hideTeamEditModal()"
5616+ <button type="button" onclick="Admin. hideTeamEditModal()"
55735617 class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700">
55745618 Cancel
55755619 </button>
@@ -7268,7 +7312,7 @@ async def admin_get_user_edit(
72687312
72697313 # Create edit form HTML
72707314 edit_form = f"""
7271- <div class="space-y-4">
7315+ <div id="user-edit-modal-content" class="space-y-4">
72727316 <h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Edit User</h3>
72737317 <div id="edit-user-error"></div>
72747318 <form hx-post="{root_path}/admin/users/{user_email}/update" hx-target="#edit-user-error" hx-swap="innerHTML" class="space-y-4">
@@ -7292,13 +7336,13 @@ async def admin_get_user_edit(
72927336 <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">New Password (leave empty to keep current)</label>
72937337 <input type="password" name="password" id="password-field"
72947338 class="mt-1 px-1.5 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 text-gray-900 dark:text-white"
7295- oninput="validatePasswordRequirements(); validatePasswordMatch();">
7339+ oninput="Admin. validatePasswordRequirements(); Admin. validatePasswordMatch();">
72967340 </div>
72977341 <div>
72987342 <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Confirm New Password</label>
72997343 <input type="password" name="confirm_password" id="confirm-password-field"
73007344 class="mt-1 px-1.5 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 text-gray-900 dark:text-white"
7301- oninput="validatePasswordMatch()">
7345+ oninput="Admin. validatePasswordMatch()">
73027346 <div id="password-match-message" class="mt-1 text-sm text-red-600 hidden">Passwords do not match</div>
73037347 </div>
73047348 {password_requirements_html}
@@ -7312,7 +7356,7 @@ async def admin_get_user_edit(
73127356 data-require-special="{"true" if settings.password_require_special else "false"}"
73137357 ></div>
73147358 <div class="flex justify-end space-x-3">
7315- <button type="button" onclick="hideUserEditModal()"
7359+ <button type="button" onclick="Admin. hideUserEditModal()"
73167360 class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700">
73177361 Cancel
73187362 </button>
@@ -7406,6 +7450,9 @@ async def admin_update_user(
74067450 success_html = """
74077451 <div class="text-green-500 text-center p-4">
74087452 <p>User updated successfully</p>
7453+ <button type="button" onclick="Admin.hideUserEditModal()" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700">
7454+ Close
7455+ </button>
74097456 </div>
74107457 """
74117458 response = HTMLResponse(content=success_html)
0 commit comments