fix(ui): keep the empty-trash panel wiring quiet when the client is mocked

This commit is contained in:
Wiley Walarus
2026-11-04 12:49:58 -08:00
parent d33cc87eed
commit 1af8ff149d
2 changed files with 101 additions and 38 deletions
+71 -38
View File
@@ -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<st
? "success"
: "info";
try {
comfyToast((messages as Record<string, any>)[tone] || messages.info, level, 1800, { noHistory: true });
comfyToast((messages as Record<string, any>)[tone] || messages.info, level, 1800, {
noHistory: true,
});
} catch (e) {
console.debug?.(e);
}
@@ -523,7 +527,9 @@ export function createStatusIndicator(options: Record<string, any> = {}): 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<string, any> = {}): 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<string, any> = {}): 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<boolean>,
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<string, any> = {}, toolPaths: Record<string, any> = {}) {
function renderCapabilities(
section: any,
toolAvailability: Record<string, any> = {},
toolPaths: Record<string, any> = {},
) {
if (!section) return;
section.replaceChildren();
if (!toolAvailability || Object.keys(toolAvailability).length === 0) {
@@ -1818,7 +1838,11 @@ function renderCapabilities(section: any, toolAvailability: Record<string, any>
section.appendChild(wrapper);
}
function renderToolsStatusLine(section: any, toolStatus: any = null, fallbackAvailability: Record<string, any> = {}) {
function renderToolsStatusLine(
section: any,
toolStatus: any = null,
fallbackAvailability: Record<string, any> = {},
) {
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 = `
+30
View File
@@ -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<string, any> | null = null;
export async function resolveTrashClient(): Promise<Record<string, any>> {
if (_trashClientCache) return _trashClientCache;
const resolved: Record<string, any> = {};
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,