diff --git a/ui/features/status/StatusDot.ts b/ui/features/status/StatusDot.ts index 468f4134..ea1c95d2 100644 --- a/ui/features/status/StatusDot.ts +++ b/ui/features/status/StatusDot.ts @@ -14,16 +14,13 @@ import { restoreDbBackup, vectorBackfill, unloadVectorModels, - getSecuritySettings, - getTrashStatus, - emptyTrash, } from "../../api/client.js"; import { ENDPOINTS } from "../../api/endpoints.js"; import { APP_CONFIG } from "../../app/config.js"; import { comfyConfirm } from "../../app/dialogs.js"; import { comfyToast, recordToastHistory } from "../../app/toast.js"; import { t } from "../../app/i18n.js"; -import { runEmptyTrashAction } from "./emptyTrashAction.js"; +import { resolveTrashClient, runEmptyTrashAction } from "./emptyTrashAction.js"; import { getRuntimeEnrichmentState, setRuntimeEnrichmentState, @@ -120,12 +117,14 @@ function appendStatusProgressBars(statusText: any, items: any[] = []) { const percent = Math.max(0, Math.min(100, Number(item.percent) || 0)); const row = document.createElement("div"); row.className = "mjr-status-progress-row"; - row.style.cssText = "display:grid;grid-template-columns:minmax(72px,0.9fr) 1.8fr auto;gap:7px;align-items:center;"; + row.style.cssText = + "display:grid;grid-template-columns:minmax(72px,0.9fr) 1.8fr auto;gap:7px;align-items:center;"; const label = document.createElement("span"); label.className = "mjr-status-progress-label"; label.textContent = String(item.label || ""); - label.style.cssText = "font-size:10px;opacity:0.72;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"; + label.style.cssText = + "font-size:10px;opacity:0.72;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"; const bar = document.createElement("div"); bar.className = "mjr-status-progress-bar"; @@ -151,7 +150,10 @@ function appendStatusProgressBars(statusText: any, items: any[] = []) { statusText.appendChild(wrap); } -function buildStatusProgressItems(counters: any, { totalAssets = 0, withWorkflows = 0, withGenerationData = 0 } = {}) { +function buildStatusProgressItems( + counters: any, + { totalAssets = 0, withWorkflows = 0, withGenerationData = 0 } = {}, +) { const total = Math.max(0, Number(totalAssets || counters?.total_assets || 0) || 0); if (total <= 0) return []; const pct = (value: any, denominator = total) => { @@ -300,7 +302,9 @@ function applyStatusHighlight(section: any, tone = "neutral", options: Record)[tone] || messages.info, level, 1800, { noHistory: true }); + comfyToast((messages as Record)[tone] || messages.info, level, 1800, { + noHistory: true, + }); } catch (e) { console.debug?.(e); } @@ -523,7 +527,9 @@ export function createStatusIndicator(options: Record = {}): any { _hasBackupChoices = backupSelect.childNodes.length > 0; backupSelect.disabled = !_hasBackupChoices; if (_hasBackupChoices && !matched && backupSelect.childNodes[0]) { - backupSelect.value = String((backupSelect.childNodes[0] as HTMLOptionElement)?.value || ""); + backupSelect.value = String( + (backupSelect.childNodes[0] as HTMLOptionElement)?.value || "", + ); } if (restoreDbBtn) restoreDbBtn.disabled = !_hasBackupChoices; } catch { @@ -1245,18 +1251,25 @@ export function createStatusIndicator(options: Record = {}): any { }; void (async () => { try { - const res = await getTrashStatus(); + const client = await resolveTrashClient(); + if (typeof client.getTrashStatus !== "function") { + emptyTrashBtn.style.display = "none"; + return; + } + const res = await client.getTrashStatus(); if (res?.ok && res?.data?.supported === false) { emptyTrashBtn.style.display = "none"; return; } - const sec = await getSecuritySettings(); - if (sec?.ok && sec?.data?.prefs && sec.data.prefs.allow_delete === false) { - emptyTrashBtn.disabled = true; - emptyTrashBtn.title = t( - "status.emptyTrashDisabled", - "Empty trash is disabled. Enable 'allow_delete' in settings or set MAJOOR_ALLOW_DELETE=1.", - ); + if (typeof client.getSecuritySettings === "function") { + const sec = await client.getSecuritySettings(); + if (sec?.ok && sec?.data?.prefs && sec.data.prefs.allow_delete === false) { + emptyTrashBtn.disabled = true; + emptyTrashBtn.title = t( + "status.emptyTrashDisabled", + "Empty trash is disabled. Enable 'allow_delete' in settings or set MAJOOR_ALLOW_DELETE=1.", + ); + } } } catch (e) { console.debug?.(e); @@ -1271,9 +1284,10 @@ export function createStatusIndicator(options: Record = {}): any { memoryPurgeBtn.disabled = true; emptyTrashBtn.textContent = t("btn.emptyTrashRunning", "Emptying..."); try { + const client = await resolveTrashClient(); await runEmptyTrashAction({ - getStatus: getTrashStatus, - empty: emptyTrash, + getStatus: client.getTrashStatus, + empty: client.emptyTrash, confirm: (message: string, title: string) => Promise.resolve(comfyConfirm(message, title)) as Promise, toast: (message, level, duration, options) => @@ -1678,14 +1692,16 @@ export async function triggerScan( ? t("scope.custom", "Custom") : desiredScope === "workflow" ? t("scope.workflow", "Workflow") - : t("scope.output", "Outputs"); + : t("scope.output", "Outputs"); let detail = ""; if (desiredScope === "input") detail = roots?.input_directory ? ` (${roots.input_directory})` : ""; else if (desiredScope === "custom") { const list = Array.isArray(roots?.custom_roots) ? roots.custom_roots : []; - const picked = desiredCustomRootId ? list.find((r: any) => r?.id === desiredCustomRootId) : null; + const picked = desiredCustomRootId + ? list.find((r: any) => r?.id === desiredCustomRootId) + : null; detail = picked?.path ? ` (${picked.path})` : ""; } else if (desiredScope === "output") detail = roots?.output_directory ? ` (${roots.output_directory})` : ""; @@ -1793,7 +1809,11 @@ function createCapabilityBadge(label: any, available: any, hint: any) { return badge; } -function renderCapabilities(section: any, toolAvailability: Record = {}, toolPaths: Record = {}) { +function renderCapabilities( + section: any, + toolAvailability: Record = {}, + toolPaths: Record = {}, +) { if (!section) return; section.replaceChildren(); if (!toolAvailability || Object.keys(toolAvailability).length === 0) { @@ -1818,7 +1838,11 @@ function renderCapabilities(section: any, toolAvailability: Record section.appendChild(wrapper); } -function renderToolsStatusLine(section: any, toolStatus: any = null, fallbackAvailability: Record = {}) { +function renderToolsStatusLine( + section: any, + toolStatus: any = null, + fallbackAvailability: Record = {}, +) { if (!section) return; const parent = section.parentElement; if (!parent) return; @@ -1848,7 +1872,11 @@ function renderToolsStatusLine(section: any, toolStatus: any = null, fallbackAva container.textContent = segments.join(" | "); } -function createCapabilityNode({ label, hint }: { label: any; hint: any }, available: any, path: any) { +function createCapabilityNode( + { label, hint }: { label: any; hint: any }, + available: any, + path: any, +) { const container = document.createElement("div"); container.style.cssText = ` display: flex; @@ -1917,7 +1945,7 @@ function buildIndexHealthLine(counters: any, desiredScope: any) { ? t("scope.custom", "Custom") : desiredScope === "workflow" ? t("scope.workflow", "Workflow") - : t("scope.output", "Outputs"); + : t("scope.output", "Outputs"); if (!Number.isFinite(totalAssets) || totalAssets <= 0) { return `${t("status.indexHealthEmpty", "Index health: empty")} (${scopeLabel})`; @@ -2093,13 +2121,13 @@ export async function updateStatus( } else if (vectorDegraded) { healthTone = "warning"; } - const isWorkflowScope = desiredScope === "workflow"; - const displayTone = - isCustomBrowserMode && healthTone !== "error" - ? "browser" - : isWorkflowScope && healthTone !== "error" - ? "workflow" - : healthTone; + const isWorkflowScope = desiredScope === "workflow"; + const displayTone = + isCustomBrowserMode && healthTone !== "error" + ? "browser" + : isWorkflowScope && healthTone !== "error" + ? "workflow" + : healthTone; let watcherInfo = counters.watcher; try { if (!watcherInfo || typeof watcherInfo.enabled !== "boolean") { @@ -2120,10 +2148,10 @@ export async function updateStatus( scope: t("scope.workflow", "Workflow"), }) : isCustomBrowserMode - ? t("status.watcher.disabledScoped", "Watcher: disabled ({scope})", { - scope: t("scope.customBrowser", "Browser"), - }) - : formatWatcherLine(watcherInfo, desiredScope); + ? t("status.watcher.disabledScoped", "Watcher: disabled ({scope})", { + scope: t("scope.customBrowser", "Browser"), + }) + : formatWatcherLine(watcherInfo, desiredScope); const enrichmentLine = enrichmentActive ? t("status.enrichmentQueue", "Metadata enrichment queue: {count}", { count: enrichmentQueueLength, @@ -2142,7 +2170,7 @@ export async function updateStatus( ? t("scope.customBrowser", "Browser") : desiredScope === "workflow" ? t("scope.workflow", "Workflow") - : t("scope.output", "Outputs"); + : t("scope.output", "Outputs"); if (healthTone === "error") { statusDot.style.background = "var(--mjr-status-error, #f44336)"; @@ -2320,7 +2348,12 @@ export async function updateStatus( return null; } -function createRetryButton(statusDot: any, statusText: any, capabilitiesSection = null, scanTarget = null) { +function createRetryButton( + statusDot: any, + statusText: any, + capabilitiesSection = null, + scanTarget = null, +) { const button = document.createElement("button"); button.textContent = t("btn.retryServices"); button.style.cssText = ` diff --git a/ui/features/status/emptyTrashAction.ts b/ui/features/status/emptyTrashAction.ts index be9d0396..45b72b80 100644 --- a/ui/features/status/emptyTrashAction.ts +++ b/ui/features/status/emptyTrashAction.ts @@ -17,6 +17,36 @@ export interface EmptyTrashDeps { export type EmptyTrashOutcome = "empty" | "removed" | "cancelled" | "unsupported" | "error"; +/** + * Resolve the trash client functions lazily so a partially mocked API module + * (e.g. in other suites' tests) never throws or logs during panel init. + * Missing functions are reported as undefined and treated as "unsupported". + */ +let _trashClientCache: Record | null = null; + +export async function resolveTrashClient(): Promise> { + if (_trashClientCache) return _trashClientCache; + const resolved: Record = {}; + try { + const mod: any = await import("../../api/client.js"); + const available: string[] = Object.keys(mod); + for (const key of ["getTrashStatus", "emptyTrash", "getSecuritySettings"]) { + if (available.includes(key)) { + try { + const value = mod[key]; + resolved[key] = typeof value === "function" ? value : undefined; + } catch { + resolved[key] = undefined; + } + } + } + } catch { + // Module unavailable — leave everything undefined (treated as unsupported). + } + _trashClientCache = resolved; + return resolved; +} + function _translateDefault( deps: EmptyTrashDeps, key: string,