Files
MajoorWaldi-ComfyUI-Majoor-…/js/features/panel/AssetsManagerPanel.js
T
Your Name 7183c87400 Refactor filtersController to manage event listeners with lifecycle support
- Introduced `addManagedListener` function to handle event listeners with automatic cleanup.
- Updated event listeners for various filter controls to use the new management function.
- Added lifecycle signal support for event listeners to ensure proper disposal.
- Implemented cleanup logic to remove event listeners when no longer needed.

---

Update database schema handling to support async operations

- Changed `db.has_table` to `db.ahas_table` for async compatibility.
- Updated related database checks to use the new async method.

---

Refactor SQLite adapter to prioritize async operations

- Transitioned public API to expose async methods as primary.
- Removed synchronous methods and replaced them with async counterparts.
- Updated internal transaction handling to support async context management.
- Enhanced error handling and logging for async operations.

---

Add async support for FFProbe tool

- Implemented `aread` and `aread_batch` methods for asynchronous metadata reading.
- Utilized asyncio subprocess for non-blocking execution of ffprobe commands.

---

Enhance IndexScanner with I/O throttling

- Introduced `SCAN_IOPS_LIMIT` to control I/O operations during directory scans.
- Implemented pacing logic to manage scan I/O operations effectively.

---

Update MetadataService to utilize async reading methods

- Refactored methods to call async variants of exif and ffprobe reading.
- Improved error handling for async operations.

---

Implement path resolution checks in asset routes

- Added `_is_resolved_path_allowed` function to enforce allowed roots for file paths.
- Updated asset route handlers to validate resolved paths against allowed roots.

---

Refactor database maintenance routes for async handling

- Changed database close operations to use async `aclose` method.
- Updated error handling during database maintenance operations.

---

Update scan routes to utilize async database vacuuming

- Replaced synchronous database vacuum calls with async `avacuum` method.

---

Modify tests to support async database operations

- Updated test examples to use async context management for database interactions.
- Refactored integration tests to ensure proper async cleanup of database connections.
- Adjusted regression tests to count async transaction invocations correctly.
2026-02-16 01:05:48 +01:00

1523 lines
57 KiB
JavaScript

import { comfyConfirm } from "../../app/dialogs.js";
import { comfyToast } from "../../app/toast.js";
import { createStatusIndicator, setupStatusPolling, triggerScan, updateStatus } from "../status/StatusDot.js";
import {
createGridContainer,
loadAssets,
loadAssetsFromList,
prepareGridForScopeSwitch,
disposeGrid,
refreshGrid,
captureAnchor,
restoreAnchor,
bindGridScanListeners,
disposeGridScanListeners,
} from "../grid/GridView.js";
import {
get,
post,
getCollectionAssets,
toggleWatcher,
setWatcherScope,
getDuplicateAlerts,
startDuplicatesAnalysis,
mergeDuplicateTags,
deleteAssets
} from "../../api/client.js";
import { ENDPOINTS } from "../../api/endpoints.js";
import { createSidebar, showAssetInSidebar, closeSidebar } from "../../components/SidebarView.js";
import { createRatingBadge, createTagsBadge } from "../../components/Badges.js";
import { loadMajoorSettings } from "../../app/settings.js";
import { createPopoverManager } from "./views/popoverManager.js";
import { createCollectionsPopoverView } from "../collections/views/collectionsPopoverView.js";
import { createCollectionsController } from "../collections/controllers/collectionsController.js";
import { createPanelState } from "./state/panelState.js";
import { createHeaderView } from "./views/headerView.js";
import { createCustomPopoverView } from "./views/customPopoverView.js";
import { createFilterPopoverView } from "./views/filterPopoverView.js";
import { createSortPopoverView } from "./views/sortPopoverView.js";
import { createSearchView } from "./views/searchView.js";
import { createPinnedFoldersPopoverView } from "./views/pinnedFoldersPopoverView.js";
import { createSummaryBarView } from "./views/summaryBarView.js";
import { createAgendaCalendar } from "../filters/calendar/AgendaCalendar.js";
import { normalizeQuery } from "./controllers/query.js";
import { createGridController } from "./controllers/gridController.js";
import { createScopeController } from "./controllers/scopeController.js";
import { createContextController } from "./controllers/contextController.js";
import { createCustomRootsController } from "./controllers/customRootsController.js";
import { bindFilters } from "./controllers/filtersController.js";
import { createSortController } from "./controllers/sortController.js";
import { bindSidebarOpen } from "./controllers/sidebarController.js";
import { createPanelHotkeysController } from "./controllers/panelHotkeysController.js";
import { createRatingHotkeysController } from "./controllers/ratingHotkeysController.js";
import { bindGridContextMenu } from "../contextmenu/GridContextMenu.js";
let _activeGridContainer = null;
export function getActiveGridContainer() {
// First try the cached reference
if (_activeGridContainer && _activeGridContainer.isConnected) {
return _activeGridContainer;
}
// Fallback: try to find it in the DOM by ID or class
try {
const grid = document.getElementById("mjr-assets-grid") || document.querySelector(".mjr-grid");
if (grid && grid.isConnected) {
return grid;
}
} catch {}
return null;
}
export async function renderAssetsManager(container, { useComfyThemeUI = true } = {}) {
const panelLifecycleAC = typeof AbortController !== "undefined" ? new AbortController() : null;
if (useComfyThemeUI) {
container.classList.add("mjr-assets-manager");
} else {
container.classList.remove("mjr-assets-manager");
}
// Cleanup previous instance before re-render to prevent listener leaks
try {
container._eventCleanup?.();
} catch {}
try {
container._mjrPanelState?._mjrDispose?.();
} catch {}
try {
container._mjrPopoverManager?.dispose?.();
} catch {}
try {
container._mjrHotkeys?.dispose?.();
} catch {}
try {
container._mjrRatingHotkeys?.dispose?.();
} catch {}
try {
container._mjrSidebarController?.dispose?.();
} catch {}
try {
container._mjrGridContextMenuUnbind?.();
} catch {}
container.replaceChildren();
container.classList.add("mjr-am-container");
const popovers = createPopoverManager(container);
try {
container._mjrPopoverManager = popovers;
} catch {}
const state = createPanelState();
try {
container._mjrPanelState = state;
} catch {}
const headerView = createHeaderView();
const { header, headerActions, tabButtons, customMenuBtn, filterBtn, sortBtn, collectionsBtn, pinnedFoldersBtn } = headerView;
const { customPopover, customSelect, customAddBtn, customRemoveBtn } = createCustomPopoverView();
const {
filterPopover,
kindSelect,
wfCheckbox,
workflowTypeSelect,
ratingSelect,
minSizeInput,
maxSizeInput,
resolutionPresetSelect,
minWidthInput,
minHeightInput,
dateRangeSelect,
dateExactInput,
agendaContainer
} = createFilterPopoverView();
const { sortPopover, sortMenu } = createSortPopoverView();
const { collectionsPopover, collectionsMenu } = createCollectionsPopoverView();
const { pinnedFoldersPopover, pinnedFoldersMenu } = createPinnedFoldersPopoverView();
headerActions.appendChild(customPopover);
const { searchSection, searchInputEl } = createSearchView({
filterBtn,
sortBtn,
collectionsBtn,
pinnedFoldersBtn,
filterPopover,
sortPopover,
collectionsPopover,
pinnedFoldersPopover
});
// Restore persistent search query
if (state.searchQuery) {
searchInputEl.value = state.searchQuery;
}
searchInputEl.addEventListener("input", (e) => {
state.searchQuery = e.target.value;
}, { signal: panelLifecycleAC?.signal });
const { bar: summaryBar, update: updateSummaryBar } = createSummaryBarView();
const folderBreadcrumb = document.createElement("div");
folderBreadcrumb.className = "mjr-folder-breadcrumb";
folderBreadcrumb.style.cssText = "display:none; align-items:center; gap:6px; padding:4px 8px; margin:2px 0 4px 0; font-size:12px; opacity:0.85; overflow:auto; white-space:nowrap;";
const content = document.createElement("div");
content.classList.add("mjr-am-content");
let gridContainer = null;
let sidebarController = null;
container.tabIndex = -1;
const statusSection = createStatusIndicator();
const statusDot = statusSection.querySelector("#mjr-status-dot");
const statusText = statusSection.querySelector("#mjr-status-text");
const capabilitiesSection = statusSection.querySelector("#mjr-status-capabilities");
const browseSection = document.createElement("div");
browseSection.classList.add("mjr-am-browse");
browseSection.style.cssText = `
flex: 1;
display: flex;
flex-direction: row;
overflow: hidden;
`;
const gridWrapper = document.createElement("div");
gridWrapper.style.cssText = `
flex: 1;
overflow: auto;
position: relative;
`;
// NOTE: Scroll restoration is done AFTER grid loads (see initialLoadPromise below).
// Restoring scroll on an empty grid would be useless.
let _scrollTimer = null;
let _lastUserInteractionAt = 0;
const markUserInteraction = () => {
try {
_lastUserInteractionAt = Date.now();
} catch {
_lastUserInteractionAt = 0;
}
};
gridWrapper.addEventListener("scroll", () => {
markUserInteraction();
if (_scrollTimer) clearTimeout(_scrollTimer);
_scrollTimer = setTimeout(() => {
try {
state.scrollTop = gridWrapper.scrollTop;
} catch {}
}, 100);
}, { passive: true, signal: panelLifecycleAC?.signal });
gridContainer = createGridContainer();
gridWrapper.appendChild(gridContainer);
_activeGridContainer = gridContainer;
const summaryBarDisposers = [];
const registerSummaryDispose = (fn) => {
if (typeof fn === "function") summaryBarDisposers.push(fn);
};
gridContainer._mjrSummaryBarDispose = () => {
for (const fn of summaryBarDisposers.splice(0)) {
try {
fn();
} catch {}
}
};
try {
const handler = () => {
try {
updateSummaryBar?.({ state, gridContainer });
} catch {}
};
gridContainer.addEventListener("mjr:grid-stats", handler, { signal: panelLifecycleAC?.signal });
registerSummaryDispose(() => {
try {
gridContainer.removeEventListener("mjr:grid-stats", handler);
} catch {}
});
} catch {}
try {
bindGridScanListeners();
} catch {}
// Bind grid context menu
container._mjrGridContextMenuUnbind = bindGridContextMenu({
gridContainer,
getState: () => state,
onRequestOpenViewer: (asset) => {
gridContainer?.dispatchEvent?.(new CustomEvent("mjr:open-viewer", { detail: { asset } }));
},
});
const settings = loadMajoorSettings();
const sidebar = createSidebar(settings.sidebar?.position || "right");
let _sidebarPosition = "right";
const applySidebarPosition = (nextPosition) => {
const pos = String(nextPosition || "right").toLowerCase() === "left" ? "left" : "right";
if (_sidebarPosition === pos) return;
_sidebarPosition = pos;
// Preserve grid scroll where possible.
let scrollTop = 0;
let scrollLeft = 0;
try {
scrollTop = Number(gridWrapper?.scrollTop || 0) || 0;
scrollLeft = Number(gridWrapper?.scrollLeft || 0) || 0;
} catch {}
try {
sidebar.dataset.position = pos;
} catch {}
try {
if (pos === "left") {
browseSection.appendChild(sidebar);
browseSection.appendChild(gridWrapper);
browseSection.insertBefore(sidebar, browseSection.firstChild);
} else {
browseSection.appendChild(gridWrapper);
browseSection.appendChild(sidebar);
}
} catch {}
try {
requestAnimationFrame(() => {
try {
gridWrapper.scrollTop = scrollTop;
gridWrapper.scrollLeft = scrollLeft;
} catch {}
});
} catch {
try {
gridWrapper.scrollTop = scrollTop;
gridWrapper.scrollLeft = scrollLeft;
} catch {}
}
};
const applyWatcherForScope = async (nextScope) => {
const scope = String(nextScope || state.scope || "output").toLowerCase();
const settings = loadMajoorSettings();
const wantsEnabled = !!settings?.watcher?.enabled;
// Custom scope is a live filesystem browser; watcher must stay disabled there.
if (scope === "custom") {
try {
await toggleWatcher(false);
} catch {}
return;
}
const shouldEnable = wantsEnabled;
try {
await toggleWatcher(shouldEnable);
} catch {}
if (!shouldEnable) return;
try {
await setWatcherScope({
scope,
customRootId: ""
});
} catch {}
};
// Initial position (no reload needed).
try {
_sidebarPosition = ""; // force apply
applySidebarPosition(settings.sidebar?.position || "right");
} catch {}
// Live updates when ComfyUI settings change.
const onSettingsChanged = (_e) => {
const changedKey = String(_e?.detail?.key || "").trim();
const isInitialSync = changedKey === "__init__";
const shouldRefreshGrid =
isInitialSync
|| changedKey === ""
|| changedKey.startsWith("grid.")
|| changedKey.startsWith("infiniteScroll.")
|| changedKey.startsWith("siblings.")
|| changedKey.startsWith("search.")
|| changedKey.startsWith("rtHydrate.")
|| changedKey.startsWith("workflowMinimap.");
const shouldRefreshSidebar = isInitialSync || changedKey === "" || changedKey.startsWith("sidebar.");
const shouldApplyWatcher = changedKey === "" || changedKey.startsWith("watcher.");
try {
const s = loadMajoorSettings();
const hover = String(s?.ui?.cardHoverColor || "").trim();
const selected = String(s?.ui?.cardSelectionColor || "").trim();
const ratingColor = String(s?.ui?.ratingColor || "").trim();
const tagColor = String(s?.ui?.tagColor || "").trim();
if (/^#[0-9a-fA-F]{3,8}$/.test(hover)) {
document.documentElement.style.setProperty("--mjr-card-hover-color", hover);
}
if (/^#[0-9a-fA-F]{3,8}$/.test(selected)) {
document.documentElement.style.setProperty("--mjr-card-selection-color", selected);
}
if (/^#[0-9a-fA-F]{3,8}$/.test(ratingColor)) {
document.documentElement.style.setProperty("--mjr-rating-color", ratingColor);
}
if (/^#[0-9a-fA-F]{3,8}$/.test(tagColor)) {
document.documentElement.style.setProperty("--mjr-tag-color", tagColor);
}
// 1. Sidebar position
applySidebarPosition(s.sidebar?.position || "right");
// 2. Grid visuals (badges, details, sizes)
// Refresh only when a relevant setting changed to avoid reload storms.
if (shouldRefreshGrid) {
refreshGrid(gridContainer);
}
if (shouldRefreshSidebar && state.sidebarOpen && state.activeAssetId) {
sidebarController?.refreshActiveAsset?.();
}
} catch {}
try {
if (!shouldApplyWatcher) return;
applyWatcherForScope(state.scope);
} catch {}
};
try {
onSettingsChanged({ detail: { key: "__init__" } });
} catch {}
try {
window.addEventListener?.("mjr-settings-changed", onSettingsChanged, { signal: panelLifecycleAC?.signal });
} catch {}
try {
const onEnrichmentStatus = (e) => {
try {
if (panelLifecycleAC?.signal?.aborted) return;
} catch {}
const detail = e?.detail || {};
const active = !!detail?.active;
// Ensure card workflow/status dots reflect final metadata once enrichment ends.
if (!active) {
gridController.reloadGrid().catch(() => {});
}
};
window.addEventListener?.("mjr-enrichment-status", onEnrichmentStatus, { signal: panelLifecycleAC?.signal });
} catch {}
content.appendChild(statusSection);
content.appendChild(searchSection);
content.appendChild(folderBreadcrumb);
content.appendChild(summaryBar);
content.appendChild(browseSection);
container.appendChild(header);
try {
container._mjrVersionUpdateCleanup?.();
} catch {}
container._mjrVersionUpdateCleanup = header._mjrVersionUpdateCleanup;
container.appendChild(content);
const getQuery = () => normalizeQuery(searchInputEl);
const gridController = createGridController({
gridContainer,
loadAssets,
loadAssetsFromList,
getCollectionAssets,
disposeGrid,
getQuery,
state
});
let scopeController = null;
let sortController = null;
let contextController = null;
let _duplicatesAlert = null;
let _dupPollTimer = null;
let _autoLoadTimer = null;
const folderBackStack = [];
const folderForwardStack = [];
const normPath = (value) => String(value || "").trim().replaceAll("\\", "/");
const isWindowsDrive = (part) => /^[a-zA-Z]:$/.test(String(part || "").trim());
const currentFolderPath = () => normPath(state.currentFolderRelativePath || state.subfolder || "");
const setFolderPath = (next) => {
const v = normPath(next);
state.currentFolderRelativePath = v;
state.subfolder = v;
try {
if (gridContainer?.dataset) gridContainer.dataset.mjrSubfolder = v;
} catch {}
};
const resetToBrowserRoot = async () => {
state.customRootId = "";
setFolderPath("");
folderForwardStack.length = 0;
notifyContextChanged();
await gridController.reloadGrid();
};
const navigateFolder = async (target, { pushHistory = true, clearForward = true } = {}) => {
const prev = currentFolderPath();
const next = normPath(target);
if (prev === next) return;
if (pushHistory) folderBackStack.push(prev);
if (clearForward) folderForwardStack.length = 0;
setFolderPath(next);
notifyContextChanged();
await gridController.reloadGrid();
};
const parentFolderPath = (pathValue) => {
const cur = normPath(pathValue);
if (!cur) return "";
const parts = cur.split("/").filter(Boolean);
if (!parts.length) return "";
if (parts.length === 1) {
return isWindowsDrive(parts[0]) ? `${parts[0]}/` : "";
}
parts.pop();
if (parts.length === 1 && isWindowsDrive(parts[0])) return `${parts[0]}/`;
return parts.join("/");
};
const renderFolderBreadcrumb = () => {
const isCustom = String(state.scope || "") === "custom";
const rel = String(state.currentFolderRelativePath || state.subfolder || "").trim().replaceAll("\\", "/");
const selectedRootId = String(state.customRootId || "").trim();
const selectedRootLabel = (() => {
if (!selectedRootId) return "";
try {
const opt = customSelect?.selectedOptions?.[0] || null;
return String(opt?.textContent || "").trim();
} catch {
return "";
}
})();
if (!isCustom) {
folderBackStack.length = 0;
folderForwardStack.length = 0;
folderBreadcrumb.style.display = "none";
folderBreadcrumb.replaceChildren();
return;
}
folderBreadcrumb.style.display = "flex";
folderBreadcrumb.replaceChildren();
const backBtn = document.createElement("button");
backBtn.type = "button";
backBtn.textContent = "Back";
backBtn.className = "mjr-btn-link";
const canBackToBrowserRoot = !!String(state.customRootId || "").trim() && !rel;
backBtn.disabled = folderBackStack.length === 0 && !canBackToBrowserRoot;
backBtn.style.cssText = "background:none;border:none;padding:0 4px 0 0;font:inherit;color:var(--mjr-accent, #7aa2ff);cursor:pointer;";
backBtn.addEventListener("click", async () => {
const prev = folderBackStack.pop();
if (prev != null) {
folderForwardStack.push(currentFolderPath());
await navigateFolder(prev, { pushHistory: false, clearForward: false });
return;
}
if (canBackToBrowserRoot) {
await resetToBrowserRoot();
}
}, { signal: panelLifecycleAC?.signal });
folderBreadcrumb.appendChild(backBtn);
const upBtn = document.createElement("button");
upBtn.type = "button";
upBtn.textContent = "Up";
upBtn.className = "mjr-btn-link";
upBtn.disabled = !rel;
upBtn.style.cssText = "background:none;border:none;padding:0 4px 0 0;font:inherit;color:var(--mjr-accent, #7aa2ff);cursor:pointer;";
upBtn.addEventListener("click", async () => {
const parent = parentFolderPath(currentFolderPath());
await navigateFolder(parent);
}, { signal: panelLifecycleAC?.signal });
folderBreadcrumb.appendChild(upBtn);
const sep0 = document.createElement("span");
sep0.textContent = "|";
sep0.style.opacity = "0.5";
folderBreadcrumb.appendChild(sep0);
const mk = (label, target, isCurrent = false) => {
const el = document.createElement("button");
el.type = "button";
el.textContent = label;
el.className = "mjr-btn-link";
el.style.cssText = `background:none;border:none;padding:0;color:${isCurrent ? "var(--mjr-text, inherit)" : "var(--mjr-accent, #7aa2ff)"};cursor:${isCurrent ? "default" : "pointer"};font:inherit;`;
if (!isCurrent) {
el.addEventListener("click", async () => {
if (label === "Computer" && String(state.customRootId || "").trim()) {
await resetToBrowserRoot();
return;
}
await navigateFolder(target);
}, { signal: panelLifecycleAC?.signal });
} else {
el.disabled = true;
}
return el;
};
folderBreadcrumb.appendChild(mk("Computer", "", !rel));
if (selectedRootId) {
const sepRoot = document.createElement("span");
sepRoot.textContent = "/";
sepRoot.style.opacity = "0.6";
folderBreadcrumb.appendChild(sepRoot);
// Root node: click returns to root of selected custom folder.
folderBreadcrumb.appendChild(mk(selectedRootLabel || selectedRootId, "", !rel));
}
if (!rel) return;
const parts = rel.split("/").filter(Boolean);
let acc = "";
for (let i = 0; i < parts.length; i++) {
const sep = document.createElement("span");
sep.textContent = "/";
sep.style.opacity = "0.6";
folderBreadcrumb.appendChild(sep);
if (!acc) {
acc = isWindowsDrive(parts[i]) ? `${parts[i]}/` : parts[i];
} else {
acc = `${acc}/${parts[i]}`.replace(/\/{2,}/g, "/");
}
folderBreadcrumb.appendChild(mk(parts[i], acc, i === parts.length - 1));
}
};
const notifyContextChanged = () => {
try {
contextController?.update?.();
} catch {}
try {
renderFolderBreadcrumb();
} catch {}
};
const refreshDuplicateAlerts = async () => {
const scope = String(state.scope || "output").toLowerCase();
const customRootId = String(state.customRootId || "").trim();
if (scope === "custom" && !customRootId) {
_duplicatesAlert = null;
notifyContextChanged();
return;
}
try {
const result = await getDuplicateAlerts({
scope,
customRootId,
maxGroups: 4,
maxPairs: 4
}, { signal: panelLifecycleAC?.signal });
if (!result?.ok) return;
const data = result.data || {};
_duplicatesAlert = {
exactCount: Array.isArray(data.exact_groups) ? data.exact_groups.length : 0,
similarCount: Array.isArray(data.similar_pairs) ? data.similar_pairs.length : 0,
firstGroup: Array.isArray(data.exact_groups) ? data.exact_groups[0] : null,
firstPair: Array.isArray(data.similar_pairs) ? data.similar_pairs[0] : null
};
notifyContextChanged();
} catch {}
};
// Allow context menus to request a refresh (e.g. after "Remove from collection").
let _reloadGridHandler = null;
let _globalReloadGridHandler = null;
try {
_reloadGridHandler = () => {
try {
gridController.reloadGrid();
} catch {}
};
gridContainer.addEventListener("mjr:reload-grid", _reloadGridHandler, { signal: panelLifecycleAC?.signal });
const onCustomSubfolderChanged = async (e) => {
const next = String(e?.detail?.subfolder || "").trim().replaceAll("\\", "/");
const prev = currentFolderPath();
if (next !== prev) {
folderBackStack.push(prev);
folderForwardStack.length = 0;
setFolderPath(next);
notifyContextChanged();
try {
await gridController.reloadGrid();
} catch {}
}
};
gridContainer.addEventListener("mjr:custom-subfolder-changed", onCustomSubfolderChanged, { signal: panelLifecycleAC?.signal });
try {
gridContainer._mjrHasCustomSubfolderHandler = true;
} catch {}
const onDuplicateBadgeFocus = (e) => {
try {
const detail = e?.detail || {};
const filenameKey = String(detail?.filenameKey || "").trim().toLowerCase();
if (!filenameKey || !gridContainer) return;
const cards = Array.from(gridContainer.querySelectorAll(".mjr-asset-card"))
.filter((card) => String(card?.dataset?.mjrFilenameKey || "").trim().toLowerCase() === filenameKey);
if (!cards.length) return;
const ids = cards
.map((card) => String(card?.dataset?.mjrAssetId || "").trim())
.filter(Boolean);
const activeId = String(cards[0]?.dataset?.mjrAssetId || ids[0] || "").trim();
if (ids.length && typeof gridContainer?._mjrSetSelection === "function") {
gridContainer._mjrSetSelection(ids, activeId);
}
cards[0]?.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
markUserInteraction();
notifyContextChanged();
const n = Number(detail?.count || ids.length || cards.length || 0);
try {
comfyToast(`Name collision in view: ${n} item(s) selected`, "info", 1800);
} catch {}
} catch {}
};
gridContainer.addEventListener("mjr:badge-duplicates-focus", onDuplicateBadgeFocus, { signal: panelLifecycleAC?.signal });
registerSummaryDispose(() => {
try {
if (_reloadGridHandler) gridContainer.removeEventListener("mjr:reload-grid", _reloadGridHandler);
} catch {}
try {
gridContainer.removeEventListener("mjr:custom-subfolder-changed", onCustomSubfolderChanged);
} catch {}
try {
gridContainer._mjrHasCustomSubfolderHandler = false;
} catch {}
try {
gridContainer.removeEventListener("mjr:badge-duplicates-focus", onDuplicateBadgeFocus);
} catch {}
_reloadGridHandler = null;
});
} catch {}
try {
_globalReloadGridHandler = () => {
try {
if (panelLifecycleAC?.signal?.aborted) return;
} catch {}
try {
if (globalThis?._mjrMaintenanceActive) return;
} catch {}
queuedReload().catch(() => {});
};
window.addEventListener("mjr:reload-grid", _globalReloadGridHandler, { signal: panelLifecycleAC?.signal });
} catch {}
const customRootsController = createCustomRootsController({
state,
customSelect,
customRemoveBtn,
comfyConfirm,
comfyToast,
get,
post,
ENDPOINTS,
reloadGrid: gridController.reloadGrid,
onRootChanged: async () => {
try {
folderBackStack.length = 0;
folderForwardStack.length = 0;
} catch {}
try {
await applyWatcherForScope(state.scope);
} catch {}
try {
await refreshDuplicateAlerts();
} catch {}
}
});
customRootsController.bind({ customAddBtn, customRemoveBtn });
try {
window.addEventListener("mjr:custom-roots-changed", async (e) => {
const preferredId = String(e?.detail?.preferredId || "").trim() || null;
await customRootsController.refreshCustomRoots(preferredId);
}, { signal: panelLifecycleAC?.signal });
} catch {}
scopeController = createScopeController({
state,
tabButtons,
customMenuBtn,
customPopover,
popovers,
refreshCustomRoots: customRootsController.refreshCustomRoots,
reloadGrid: gridController.reloadGrid,
onChanged: () => {
state.currentFolderRelativePath = state.subfolder || "";
notifyContextChanged();
},
onScopeChanged: async () => {
try {
await applyWatcherForScope(state.scope);
} catch {}
try {
await refreshDuplicateAlerts();
} catch {}
},
onBeforeReload: async () => {
// Prevent stale cards overlap during fast scope switches.
try {
prepareGridForScopeSwitch(gridContainer);
} catch {}
// Force an immediate counters refresh for this scope instead of waiting polling tick.
try {
await updateStatus(
statusDot,
statusText,
capabilitiesSection,
{ scope: state.scope, customRootId: state.customRootId },
null,
{ signal: panelLifecycleAC?.signal || null }
);
} catch {}
}
});
if (!header._mjrTabListenersBound) {
Object.values(tabButtons).forEach((btn) => {
btn.addEventListener("click", () => scopeController.setScope(btn.dataset.scope), { signal: panelLifecycleAC?.signal });
});
header._mjrTabListenersBound = true;
}
popovers.setDismissWhitelist([
customPopover,
filterPopover,
sortPopover,
collectionsPopover,
pinnedFoldersPopover,
customMenuBtn,
filterBtn,
sortBtn,
collectionsBtn,
pinnedFoldersBtn
]);
customMenuBtn.addEventListener("click", (e) => {
e.stopPropagation();
popovers.close(filterPopover);
popovers.close(sortPopover);
popovers.toggle(customPopover, customMenuBtn);
}, { signal: panelLifecycleAC?.signal });
filterBtn.addEventListener("click", (e) => {
e.stopPropagation();
popovers.close(customPopover);
popovers.close(sortPopover);
popovers.toggle(filterPopover, filterBtn);
}, { signal: panelLifecycleAC?.signal });
sortController = createSortController({
state,
sortBtn,
sortMenu,
sortPopover,
popovers,
reloadGrid: gridController.reloadGrid,
onChanged: () => {
notifyContextChanged();
}
});
sortController.bind({
onBeforeToggle: () => {
popovers.close(customPopover);
popovers.close(filterPopover);
}
});
const collectionsController = createCollectionsController({
state,
collectionsBtn,
collectionsMenu,
collectionsPopover,
popovers,
reloadGrid: gridController.reloadGrid,
onChanged: () => {
notifyContextChanged();
}
});
collectionsController.bind();
const createPinnedMenuItem = (label, { right = null, danger = false } = {}) => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "mjr-menu-item";
if (danger) btn.style.color = "var(--error-text, #f44336)";
btn.style.cssText = `
display:flex;
align-items:center;
justify-content:space-between;
width:100%;
gap:10px;
padding:9px 10px;
border-radius:9px;
border:1px solid rgba(120,190,255,0.18);
background:linear-gradient(135deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
transition: border-color 120ms ease, background 120ms ease;
`;
const left = document.createElement("span");
left.textContent = String(label || "");
left.style.cssText = "font-weight:600; color:var(--fg-color, #e6edf7);";
const rightWrap = document.createElement("span");
rightWrap.style.cssText = "display:flex; align-items:center; gap:8px;";
if (right) {
const hint = document.createElement("span");
hint.textContent = String(right);
hint.style.cssText = "opacity:0.68; font-size:11px;";
rightWrap.appendChild(hint);
}
btn.appendChild(left);
btn.appendChild(rightWrap);
btn.addEventListener("mouseenter", () => {
btn.style.borderColor = "rgba(145,205,255,0.4)";
btn.style.background = "linear-gradient(135deg, rgba(80,140,255,0.18), rgba(32,100,200,0.14))";
}, { signal: panelLifecycleAC?.signal });
btn.addEventListener("mouseleave", () => {
btn.style.borderColor = "rgba(120,190,255,0.18)";
btn.style.background = "linear-gradient(135deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01))";
}, { signal: panelLifecycleAC?.signal });
return btn;
};
const renderPinnedFoldersMenu = async () => {
try {
pinnedFoldersMenu.replaceChildren();
} catch {}
let roots = [];
try {
const res = await get(ENDPOINTS.CUSTOM_ROOTS, panelLifecycleAC?.signal ? { signal: panelLifecycleAC.signal } : undefined);
roots = res?.ok && Array.isArray(res.data) ? res.data : [];
} catch {}
if (!roots.length) {
const empty = document.createElement("div");
empty.className = "mjr-muted";
empty.style.cssText = "padding:10px 12px; opacity:0.75;";
empty.textContent = "No pinned folders";
pinnedFoldersMenu.appendChild(empty);
return;
}
for (const r of roots) {
const id = String(r?.id || "").trim();
if (!id) continue;
const label = String(r?.name || r?.path || id).trim();
const path = String(r?.path || "").trim();
const row = document.createElement("div");
row.style.cssText = "display:flex; align-items:stretch; width:100%;";
const openBtn = createPinnedMenuItem(label, { right: path || null });
openBtn.style.flex = "1";
openBtn.addEventListener("click", async () => {
state.scope = "custom";
state.customRootId = id;
state.customRootLabel = label;
state.subfolder = "";
state.currentFolderRelativePath = "";
try {
if (customSelect) customSelect.value = id;
} catch {}
try {
scopeController?.setActiveTabStyles?.();
} catch {}
try {
await applyWatcherForScope(state.scope);
} catch {}
try {
await refreshDuplicateAlerts();
} catch {}
try {
notifyContextChanged();
} catch {}
popovers.close(pinnedFoldersPopover);
await gridController.reloadGrid();
}, { signal: panelLifecycleAC?.signal });
const unpinBtn = document.createElement("button");
unpinBtn.type = "button";
unpinBtn.className = "mjr-menu-item";
unpinBtn.title = "Unpin folder";
unpinBtn.style.cssText =
"width:42px; justify-content:center; padding:0; border:1px solid rgba(255,95,95,0.35); border-radius:9px; margin-left:6px; background: linear-gradient(135deg, rgba(255,70,70,0.16), rgba(160,20,20,0.12));";
const trash = document.createElement("i");
trash.className = "pi pi-times";
trash.style.cssText = "opacity:0.88; color: rgba(255,130,130,0.96);";
unpinBtn.appendChild(trash);
unpinBtn.addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
const ok = await comfyConfirm(`Unpin folder "${label}"?`);
if (!ok) return;
const res = await post(ENDPOINTS.CUSTOM_ROOTS_REMOVE, { id });
if (!res?.ok) {
comfyToast(res?.error || "Failed to unpin folder", "error");
return;
}
if (String(state.customRootId || "") === id) {
state.customRootId = "";
state.customRootLabel = "";
state.subfolder = "";
state.currentFolderRelativePath = "";
}
try {
await customRootsController.refreshCustomRoots();
} catch {}
await renderPinnedFoldersMenu();
await gridController.reloadGrid();
}, { signal: panelLifecycleAC?.signal });
row.appendChild(openBtn);
row.appendChild(unpinBtn);
pinnedFoldersMenu.appendChild(row);
}
};
pinnedFoldersBtn?.addEventListener("click", async (e) => {
e.stopPropagation();
try {
await renderPinnedFoldersMenu();
} catch {}
popovers.close(customPopover);
popovers.close(filterPopover);
popovers.close(sortPopover);
popovers.close(collectionsPopover);
popovers.toggle(pinnedFoldersPopover, pinnedFoldersBtn);
}, { signal: panelLifecycleAC?.signal });
// Calendar UI (separate module) for day indicators.
let agendaCalendar = null;
try {
try {
kindSelect.value = state.kindFilter || "";
wfCheckbox.checked = !!state.workflowOnly;
workflowTypeSelect.value = String(state.workflowType || "").trim().toUpperCase();
ratingSelect.value = String(Number(state.minRating || 0) || 0);
minSizeInput.value = Number(state.minSizeMB || 0) > 0 ? String(state.minSizeMB) : "";
maxSizeInput.value = Number(state.maxSizeMB || 0) > 0 ? String(state.maxSizeMB) : "";
minWidthInput.value = Number(state.minWidth || 0) > 0 ? String(state.minWidth) : "";
minHeightInput.value = Number(state.minHeight || 0) > 0 ? String(state.minHeight) : "";
const w = Number(state.minWidth || 0) || 0;
const h = Number(state.minHeight || 0) || 0;
if (w >= 3840 && h >= 2160) resolutionPresetSelect.value = "uhd";
else if (w >= 2560 && h >= 1440) resolutionPresetSelect.value = "qhd";
else if (w >= 1920 && h >= 1080) resolutionPresetSelect.value = "fhd";
else if (w >= 1280 && h >= 720) resolutionPresetSelect.value = "hd";
else resolutionPresetSelect.value = "";
dateRangeSelect.value = state.dateRangeFilter || "";
} catch {}
agendaCalendar = createAgendaCalendar({
container: agendaContainer,
hiddenInput: dateExactInput,
state,
onRequestReloadGrid: () => gridController.reloadGrid()
});
} catch {}
const disposeFilters = bindFilters({
state,
kindSelect,
wfCheckbox,
workflowTypeSelect,
ratingSelect,
minSizeInput,
maxSizeInput,
resolutionPresetSelect,
minWidthInput,
minHeightInput,
dateRangeSelect,
dateExactInput,
reloadGrid: gridController.reloadGrid,
lifecycleSignal: panelLifecycleAC?.signal || null,
onFiltersChanged: () => {
try {
agendaCalendar?.refresh?.();
} catch {}
notifyContextChanged();
}
});
// Context controller: pills + button highlighting.
try {
contextController = createContextController({
state,
gridContainer,
filterBtn,
sortBtn,
collectionsBtn,
searchInputEl,
kindSelect,
wfCheckbox,
workflowTypeSelect,
ratingSelect,
minSizeInput,
maxSizeInput,
minWidthInput,
minHeightInput,
resolutionPresetSelect,
dateRangeSelect,
dateExactInput,
scopeController,
sortController,
updateSummaryBar,
reloadGrid: () => gridController.reloadGrid(),
getExtraContext: () => ({ duplicatesAlert: _duplicatesAlert }),
extraActions: {
resetBrowserHistory: () => {
try {
folderBackStack.length = 0;
folderForwardStack.length = 0;
} catch {}
},
onDuplicateAlertClick: async (alert) => {
const current = alert || _duplicatesAlert || {};
const grp = current.firstGroup;
if (grp && Array.isArray(grp.assets) && grp.assets.length >= 2) {
const keep = grp.assets[0] || {};
const dupIds = grp.assets.slice(1).map((x) => Number(x?.id || 0)).filter((x) => x > 0);
if (dupIds.length) {
const mergeOk = await comfyConfirm(`Exact duplicates detected (${grp.assets.length}). Merge tags into "${keep?.filename || keep?.id}"?`);
if (mergeOk) {
const mergeRes = await mergeDuplicateTags(Number(keep?.id || 0), dupIds);
if (mergeRes?.ok) comfyToast("Tags merged", "success", 2200);
else comfyToast(`Tag merge failed: ${mergeRes?.error || "error"}`, "warning", 3500);
}
const delOk = await comfyConfirm(`Delete ${dupIds.length} exact duplicate(s)?`);
if (delOk) {
const delRes = await deleteAssets(dupIds);
if (delRes?.ok) {
comfyToast("Duplicates deleted", "success", 2200);
await gridController.reloadGrid();
} else {
comfyToast(`Delete failed: ${delRes?.error || "error"}`, "warning", 3500);
}
}
await refreshDuplicateAlerts();
return;
}
}
const startOk = await comfyConfirm("Start duplicate analysis in background?");
if (!startOk) return;
const runRes = await startDuplicatesAnalysis(500);
if (runRes?.ok) comfyToast("Duplicate analysis started", "info", 2200);
else comfyToast(`Analysis not started: ${runRes?.error || "error"}`, "warning", 3500);
await refreshDuplicateAlerts();
}
}
});
} catch {}
// Summary bar updates: selection changes, load changes, scope/collection changes.
try {
notifyContextChanged();
const onStats = () => notifyContextChanged();
const onSelectionChanged = (e) => {
markUserInteraction();
try {
const detail = e?.detail || {};
const ids = Array.isArray(detail.selectedIds) ? detail.selectedIds.map(String).filter(Boolean) : [];
state.selectedAssetIds = ids;
state.activeAssetId = String(detail.activeId || ids[0] || "");
} catch {}
notifyContextChanged();
};
gridContainer.addEventListener("mjr:grid-stats", onStats, { signal: panelLifecycleAC?.signal });
gridContainer.addEventListener("mjr:selection-changed", onSelectionChanged, { signal: panelLifecycleAC?.signal });
document.addEventListener?.("mjr-settings-changed", onStats, { signal: panelLifecycleAC?.signal });
// Observe DOM changes for card count / selection class changes.
let raf = null;
const schedule = () => {
if (raf) cancelAnimationFrame(raf);
raf = requestAnimationFrame(() => {
raf = null;
notifyContextChanged();
});
};
const mo = new MutationObserver(() => schedule());
mo.observe(gridContainer, { childList: true, subtree: true, attributes: true, attributeFilter: ["class"] });
gridContainer._mjrSummaryBarObserver = mo;
registerSummaryDispose(() => {
try {
gridContainer.removeEventListener("mjr:grid-stats", onStats);
} catch {}
try {
gridContainer.removeEventListener("mjr:selection-changed", onSelectionChanged);
} catch {}
try {
document.removeEventListener?.("mjr-settings-changed", onStats);
} catch {}
try {
mo.disconnect();
} catch {}
});
} catch {}
sidebarController = bindSidebarOpen({
gridContainer,
sidebar,
createRatingBadge,
createTagsBadge,
showAssetInSidebar,
closeSidebar,
state
});
const hotkeys = createPanelHotkeysController({
onTriggerScan: (ctx) => {
triggerScan(statusDot, statusText, capabilitiesSection, ctx || {});
},
onToggleDetails: () => {
sidebarController?.toggleDetails?.();
},
onFocusSearch: () => {
try {
searchInputEl?.focus?.();
// Move cursor to end for immediate typing.
const len = searchInputEl?.value?.length || 0;
searchInputEl?.setSelectionRange?.(len, len);
} catch {}
},
onClearSearch: () => {
try {
if (!searchInputEl) return;
searchInputEl.value = "";
searchInputEl.dispatchEvent?.(new Event("input", { bubbles: true }));
} catch {}
},
getScanContext: () => ({
scope: state.scope,
customRootId: state.customRootId
})
});
const ratingHotkeys = createRatingHotkeysController({
gridContainer,
createRatingBadge
});
let searchTimeout = null;
let lastSearchValue = "";
if (container._eventCleanup) {
try {
container._eventCleanup();
} catch {}
}
hotkeys.bind(content);
ratingHotkeys.bind();
try {
window._mjrHotkeysState = window._mjrHotkeysState || {};
window._mjrHotkeysState.scope = "grid";
} catch {}
// Attach controllers to container for cleanup on re-render
try {
container._mjrHotkeys = hotkeys;
container._mjrRatingHotkeys = ratingHotkeys;
container._mjrSidebarController = sidebarController;
} catch {}
container._eventCleanup = () => {
try {
panelLifecycleAC?.abort?.();
} catch {}
try {
statusSection?._mjrStatusPollDispose?.();
} catch {}
try {
container._mjrVersionUpdateCleanup?.();
} catch {}
container._mjrVersionUpdateCleanup = null;
try {
container._mjrPanelState?._mjrDispose?.();
} catch {}
try {
container._mjrPanelState = null;
} catch {}
try {
if (_scrollTimer) clearTimeout(_scrollTimer);
_scrollTimer = null;
} catch {}
try {
hotkeys.dispose();
} catch {}
try {
ratingHotkeys.dispose();
} catch {}
try {
if (window._mjrHotkeysState?.scope === "grid") {
window._mjrHotkeysState.scope = null;
}
} catch {}
try {
agendaCalendar?.dispose?.();
} catch {}
try {
disposeFilters?.();
} catch {}
try {
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = null;
} catch {}
try {
sidebar?.dispose?.();
} catch {}
try {
sidebarController?.dispose?.();
} catch {}
try {
if (_dupPollTimer) clearInterval(_dupPollTimer);
_dupPollTimer = null;
} catch {}
try {
if (_autoLoadTimer) clearTimeout(_autoLoadTimer);
_autoLoadTimer = null;
} catch {}
try {
gridContainer?._mjrSummaryBarDispose?.();
} catch {}
try {
gridContainer?._mjrSummaryBarObserver?.disconnect?.();
} catch {}
try {
gridContainer?._mjrGridContextMenuUnbind?.();
} catch {}
try {
disposeGridScanListeners();
} catch {}
try {
if (gridContainer) disposeGrid(gridContainer);
} catch {}
if (_activeGridContainer === gridContainer) {
_activeGridContainer = null;
}
gridContainer = null;
try {
popovers.dispose();
} catch {}
try {
if (container._mjrPopoverManager === popovers) {
container._mjrPopoverManager = null;
}
} catch {}
};
let lastKnownScan = null;
let lastKnownTotalAssets = null;
let hasSeenFirstCounters = false;
let pendingReloadCount = 0;
let isReloading = false;
let _lastAutoReloadAt = 0;
const queuedReload = async () => {
if (!gridContainer) return;
pendingReloadCount++;
if (isReloading) return;
isReloading = true;
try {
while (pendingReloadCount > 0) {
pendingReloadCount = 0;
const anchor = captureAnchor(gridContainer);
await gridController.reloadGrid();
try {
await restoreAnchor(gridContainer, anchor);
} catch {}
}
} finally {
isReloading = false;
}
};
const handleCountersUpdate = async (counters) => {
if (!hasSeenFirstCounters) {
hasSeenFirstCounters = true;
lastKnownScan = counters.last_scan_end;
lastKnownTotalAssets = Number(counters.total_assets ?? null);
return;
}
const hasNewScan = counters.last_scan_end && counters.last_scan_end !== lastKnownScan;
const totalAssets = Number(counters.total_assets ?? null);
// Fallback for "new files appeared" without a full scan end update (e.g. incremental indexing).
// Only auto-refresh when browsing output with default filters to avoid disrupting searches.
const isDefaultBrowse =
state.scope === "output" &&
!state.collectionId &&
String(getQuery?.() ?? "*").trim() === "*" &&
!state.kindFilter &&
!state.workflowOnly &&
!(Number(state.minRating || 0) > 0) &&
!(Number(state.minSizeMB || 0) > 0) &&
!(Number(state.maxSizeMB || 0) > 0) &&
!(Number(state.minWidth || 0) > 0) &&
!(Number(state.minHeight || 0) > 0) &&
!String(state.workflowType || "").trim() &&
!state.dateRangeFilter &&
!state.dateExactFilter;
const totalDelta =
Number.isFinite(totalAssets) && Number.isFinite(lastKnownTotalAssets)
? totalAssets - lastKnownTotalAssets
: 0;
const hasNewTotal = totalDelta >= 3;
// Keep baseline current even if we skip auto-reload; avoids repeated trigger loops.
if (Number.isFinite(totalAssets)) {
lastKnownTotalAssets = totalAssets;
}
if (!hasNewScan && !(isDefaultBrowse && hasNewTotal)) return;
// Global throttle against reload storms from frequent watcher/enrichment updates.
try {
const now = Date.now();
if (now - Number(_lastAutoReloadAt || 0) < 8000) return;
} catch {}
// Avoid disruptive auto-reload while user is actively interacting.
try {
const recentInteraction = (Date.now() - Number(_lastUserInteractionAt || 0)) < 1500;
if (recentInteraction) return;
} catch {}
lastKnownScan = counters.last_scan_end;
await queuedReload();
try {
_lastAutoReloadAt = Date.now();
} catch {
_lastAutoReloadAt = 0;
}
};
setupStatusPolling(
statusDot,
statusText,
statusSection,
capabilitiesSection,
handleCountersUpdate,
() => ({ scope: state.scope, customRootId: state.customRootId })
);
scopeController.setActiveTabStyles();
renderFolderBreadcrumb();
// Initial loading of custom roots (restores selection from state if applicable)
customRootsController.refreshCustomRoots().catch(() => {});
searchInputEl.addEventListener("input", (e) => {
const value = e?.target?.value ?? "";
if (value === lastSearchValue) return;
lastSearchValue = value;
notifyContextChanged();
if (searchTimeout) clearTimeout(searchTimeout);
// Immediate search for empty / browse-all queries.
if (value.length === 0 || value === "*") {
gridController.reloadGrid();
return;
}
// Debounce for normal searches.
searchTimeout = setTimeout(() => {
searchTimeout = null;
gridController.reloadGrid();
}, 200);
}, { signal: panelLifecycleAC?.signal });
searchInputEl.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
gridController.reloadGrid();
notifyContextChanged();
}
}, { signal: panelLifecycleAC?.signal });
const initialLoadPromise = gridController.reloadGrid();
try {
await refreshDuplicateAlerts();
} catch {}
try {
_dupPollTimer = setInterval(() => {
try {
if (panelLifecycleAC?.signal?.aborted) return;
} catch {}
refreshDuplicateAlerts().catch(() => {});
}, 15000);
} catch {}
// Restore scroll, selection visuals, and sidebar AFTER grid loads.
const restoreUIState = async () => {
try {
await initialLoadPromise;
} catch {}
// Wait one frame for DOM to settle before restoring.
requestAnimationFrame(() => {
// 1. Restore scroll position
if (state.scrollTop > 0) {
try {
gridWrapper.scrollTop = state.scrollTop;
} catch {}
}
// 2. Restore selection visuals on cards
if (state.selectedAssetIds?.length || state.activeAssetId) {
try {
const selected = (state.selectedAssetIds || []).map(String);
const activeId = String(state.activeAssetId || selected[0] || "");
selected.forEach(id => {
const card = gridContainer.querySelector(`.mjr-asset-card[data-mjr-asset-id="${CSS?.escape ? CSS.escape(id) : id}"]`);
if (card) {
card.classList.add("is-selected");
card.setAttribute?.("aria-selected", "true");
}
});
if (activeId) {
const activeCard = gridContainer.querySelector(`.mjr-asset-card[data-mjr-asset-id="${CSS?.escape ? CSS.escape(activeId) : activeId}"]`);
if (activeCard) {
activeCard.classList.add("is-active");
activeCard.setAttribute?.("aria-selected", "true");
// Scroll selection into view if not visible after scroll restore
if (state.scrollTop === 0) {
activeCard.scrollIntoView?.({ block: "nearest", behavior: "instant" });
}
}
}
} catch {}
}
// 3. Restore sidebar if it was open
if (state.sidebarOpen && state.activeAssetId) {
try {
sidebarController?.toggleDetails?.();
} catch {}
}
});
};
restoreUIState();
const checkAndAutoLoad = async () => {
try {
await initialLoadPromise;
} catch {}
const hasCards = gridContainer.querySelector(".mjr-card") != null;
if (hasCards) return;
if (state.scope !== "output") return;
if (getQuery() !== "*") return;
try {
const statusData = await get(`${ENDPOINTS.HEALTH_COUNTERS}?scope=output`, { signal: panelLifecycleAC?.signal });
if (statusData?.ok && (statusData.data?.total_assets || 0) > 0) {
await loadAssets(gridContainer);
}
} catch {}
};
_autoLoadTimer = setTimeout(checkAndAutoLoad, 2000);
return { gridContainer };
}