517 lines
22 KiB
TypeScript
517 lines
22 KiB
TypeScript
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
|
||
// version: 4
|
||
|
||
import "bootstrap";
|
||
import ResizeObserver from "resize-observer-polyfill";
|
||
import "simplebar";
|
||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||
import type { BackfillRequestPreviewDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts";
|
||
import type { BackfillStartRequestDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.ts";
|
||
import type { BackfillStartResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartResponseDto.ts";
|
||
import type { BackfillDeskOptionsDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillDeskOptionsDto.ts";
|
||
import type { BackfillHttpRouteOptionDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillHttpRouteOptionDto.ts";
|
||
import type { ShellStatusDto } from "./bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts";
|
||
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
|
||
import { invokeKsp } from "./invoke";
|
||
|
||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||
installFrontendConsoleBridge("main");
|
||
|
||
type ViewId = "backfill" | "diagnostics";
|
||
|
||
const viewTitles: Record<ViewId, string> = {
|
||
backfill: "Backfill",
|
||
diagnostics: "Diagnostics",
|
||
};
|
||
|
||
const commitmentLabels: Record<string, string> = {
|
||
confirmed: "confirmed",
|
||
finalized: "finalized",
|
||
};
|
||
|
||
const scopeLabels: Record<string, string> = {
|
||
latest_address: "Adresse — historique le plus récent",
|
||
before_address: "Adresse — avant une signature",
|
||
after_address: "Adresse — après une signature",
|
||
explicit_signatures: "Signatures explicites",
|
||
};
|
||
|
||
let compositionReadyForStart = false;
|
||
let runStartAccepted = false;
|
||
|
||
|
||
function isViewId(value: string | undefined): value is ViewId {
|
||
return value === "backfill" || value === "diagnostics";
|
||
}
|
||
|
||
function activateView(viewId: ViewId, source: "startup" | "user"): void {
|
||
document.querySelectorAll<HTMLElement>("[data-view-panel]").forEach(panel => {
|
||
panel.hidden = panel.dataset.viewPanel !== viewId;
|
||
});
|
||
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
||
const active = button.dataset.view === viewId;
|
||
button.classList.toggle("active", active);
|
||
button.setAttribute("aria-current", active ? "page" : "false");
|
||
});
|
||
const header = document.querySelector<HTMLElement>("#headerViewTitle");
|
||
if (header) {
|
||
header.textContent = viewTitles[viewId];
|
||
}
|
||
document.title = `Backfill Desk — ${viewTitles[viewId]}`;
|
||
frontendTrace("main", "Backfill Desk view DOM updated", { viewId, source });
|
||
}
|
||
|
||
function bindFrontendInteractions(): void {
|
||
document.addEventListener(
|
||
"click",
|
||
event => {
|
||
const source = event.target;
|
||
if (!(source instanceof Element)) {
|
||
return;
|
||
}
|
||
const control = source.closest<HTMLElement>('button, a, input, select, textarea, [role="button"], [role="tab"], [data-view]');
|
||
if (!control) {
|
||
return;
|
||
}
|
||
frontendTrace("main", "Backfill Desk frontend control clicked", {
|
||
controlId: control.id || null,
|
||
role: control.getAttribute("role"),
|
||
tagName: control.tagName.toLowerCase(),
|
||
viewId: control.dataset.view ?? null,
|
||
});
|
||
},
|
||
true,
|
||
);
|
||
frontendTrace("main", "Backfill Desk frontend control interaction logger installed");
|
||
}
|
||
|
||
function bindNavigation(): void {
|
||
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
||
button.addEventListener("click", () => {
|
||
const viewId = button.dataset.view;
|
||
frontendDebug("main", "Backfill Desk navigation tab clicked", { viewId: viewId ?? "unknown" });
|
||
if (isViewId(viewId)) {
|
||
activateView(viewId, "user");
|
||
}
|
||
});
|
||
});
|
||
frontendTrace("main", "Backfill Desk navigation handlers installed");
|
||
}
|
||
|
||
function populateCodeSelect(selectId: string, values: string[], labels: Record<string, string>): void {
|
||
const select = document.querySelector<HTMLSelectElement>(`#${selectId}`);
|
||
if (!select) {
|
||
return;
|
||
}
|
||
const previous = select.value;
|
||
select.replaceChildren();
|
||
for (const value of values) {
|
||
const option = document.createElement("option");
|
||
option.value = value;
|
||
option.textContent = labels[value] ?? value;
|
||
select.append(option);
|
||
}
|
||
select.disabled = values.length === 0;
|
||
if (values.includes(previous)) {
|
||
select.value = previous;
|
||
}
|
||
}
|
||
|
||
function applyNumberInputContract(inputId: string, maximum: number, defaultValue: number): void {
|
||
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
|
||
if (!input) {
|
||
return;
|
||
}
|
||
input.min = "1";
|
||
input.max = maximum.toString();
|
||
if (input.value === "") {
|
||
input.value = defaultValue.toString();
|
||
}
|
||
}
|
||
|
||
function refreshStartButton(): void {
|
||
const startButton = document.querySelector<HTMLButtonElement>("#startBackfillRequest");
|
||
if (startButton) {
|
||
startButton.disabled = !compositionReadyForStart || runStartAccepted;
|
||
}
|
||
}
|
||
|
||
function renderCampaignContract(options: BackfillDeskOptionsDto): void {
|
||
populateCodeSelect("backfillScopeKind", options.scopeKinds, scopeLabels);
|
||
populateCodeSelect("backfillCommitment", options.commitments, commitmentLabels);
|
||
applyNumberInputContract("backfillPageSize", options.limits.maxPageSize, options.limits.defaultPageSize);
|
||
applyNumberInputContract("backfillMaxPages", options.limits.maxPages, options.limits.defaultMaxPages);
|
||
applyNumberInputContract("backfillMaxCandidates", options.limits.maxCandidates, options.limits.defaultMaxCandidates);
|
||
applyNumberInputContract("backfillHydrationConcurrency", options.limits.maxHydrationConcurrency, options.limits.defaultHydrationConcurrency);
|
||
const signatureHelp = document.querySelector<HTMLElement>("#backfillExplicitSignaturesHelp");
|
||
if (signatureHelp) {
|
||
signatureHelp.textContent = `Une signature par ligne, ${options.limits.minSignatureTextBytes}–${options.limits.maxSignatureTextBytes} octets encodés ; maximum ${options.limits.maxCandidates}.`;
|
||
}
|
||
const validateButton = document.querySelector<HTMLButtonElement>("#validateBackfillRequest");
|
||
if (validateButton) {
|
||
validateButton.disabled = !options.compositionReady || options.httpRoutes.length === 0;
|
||
}
|
||
updateCampaignScopeFields();
|
||
frontendTrace("main", "Backfill Desk campaign contract rendered", {
|
||
commitmentCount: options.commitments.length,
|
||
compositionReady: options.compositionReady,
|
||
scopeCount: options.scopeKinds.length,
|
||
});
|
||
}
|
||
|
||
function currentScopeKind(): string {
|
||
const select = document.querySelector<HTMLSelectElement>("#backfillScopeKind");
|
||
return select?.value ?? "";
|
||
}
|
||
|
||
function updateCampaignScopeFields(): void {
|
||
const scopeKind = currentScopeKind();
|
||
const addressGroup = document.querySelector<HTMLElement>("#backfillAddressGroup");
|
||
const anchorGroup = document.querySelector<HTMLElement>("#backfillAnchorGroup");
|
||
const signaturesGroup = document.querySelector<HTMLElement>("#backfillExplicitSignaturesGroup");
|
||
const minContextSlotGroup = document.querySelector<HTMLElement>("#backfillMinContextSlotGroup");
|
||
const addressScope = scopeKind === "latest_address" || scopeKind === "before_address" || scopeKind === "after_address";
|
||
const anchoredScope = scopeKind === "before_address" || scopeKind === "after_address";
|
||
const explicitScope = scopeKind === "explicit_signatures";
|
||
if (addressGroup) {
|
||
addressGroup.hidden = !addressScope;
|
||
}
|
||
if (anchorGroup) {
|
||
anchorGroup.hidden = !anchoredScope;
|
||
}
|
||
if (signaturesGroup) {
|
||
signaturesGroup.hidden = !explicitScope;
|
||
}
|
||
if (minContextSlotGroup) {
|
||
minContextSlotGroup.hidden = explicitScope;
|
||
}
|
||
frontendTrace("main", "Backfill Desk campaign scope fields updated", { scopeKind: scopeKind || "none" });
|
||
}
|
||
|
||
function invalidateCampaignValidation(source: "input" | "scope" | "route"): void {
|
||
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
|
||
if (status) {
|
||
status.hidden = true;
|
||
status.textContent = "";
|
||
status.classList.remove("alert-success", "alert-danger");
|
||
status.classList.add("alert-secondary");
|
||
}
|
||
const summary = document.querySelector<HTMLElement>("#backfillRequestPreview");
|
||
if (summary) {
|
||
summary.hidden = true;
|
||
}
|
||
frontendTrace("main", "Backfill Desk campaign validation invalidated", { source });
|
||
}
|
||
|
||
function numericInputValue(inputId: string): number {
|
||
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
|
||
if (!input || !Number.isSafeInteger(input.valueAsNumber) || input.valueAsNumber < 0) {
|
||
return 0;
|
||
}
|
||
return input.valueAsNumber;
|
||
}
|
||
|
||
function rawOptionalInput(inputId: string, enabled: boolean): string | null {
|
||
if (!enabled) {
|
||
return null;
|
||
}
|
||
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
|
||
if (!input || input.value === "") {
|
||
return null;
|
||
}
|
||
return input.value;
|
||
}
|
||
|
||
function explicitSignatureLines(): string[] {
|
||
const textarea = document.querySelector<HTMLTextAreaElement>("#backfillExplicitSignatures");
|
||
if (!textarea || currentScopeKind() !== "explicit_signatures") {
|
||
return [];
|
||
}
|
||
return textarea.value.split(/\r?\n/).filter(line => line.length > 0);
|
||
}
|
||
|
||
function buildBackfillStartRequest(): BackfillStartRequestDto {
|
||
const scopeKind = currentScopeKind();
|
||
const role = document.querySelector<HTMLSelectElement>("#backfillHttpRoute")?.value ?? "";
|
||
const commitment = document.querySelector<HTMLSelectElement>("#backfillCommitment")?.value ?? "";
|
||
const addressScope = scopeKind === "latest_address" || scopeKind === "before_address" || scopeKind === "after_address";
|
||
const anchoredScope = scopeKind === "before_address" || scopeKind === "after_address";
|
||
const minContextSlotEnabled = scopeKind !== "explicit_signatures";
|
||
return {
|
||
address: rawOptionalInput("backfillAddress", addressScope),
|
||
anchorSignature: rawOptionalInput("backfillAnchorSignature", anchoredScope),
|
||
commitment,
|
||
explicitSignatures: explicitSignatureLines(),
|
||
hydrationConcurrency: numericInputValue("backfillHydrationConcurrency"),
|
||
httpRole: role,
|
||
maxCandidates: numericInputValue("backfillMaxCandidates"),
|
||
maxPages: numericInputValue("backfillMaxPages"),
|
||
minContextSlot: rawOptionalInput("backfillMinContextSlot", minContextSlotEnabled),
|
||
pageSize: numericInputValue("backfillPageSize"),
|
||
scopeKind,
|
||
};
|
||
}
|
||
|
||
function renderRequestPreview(preview: BackfillRequestPreviewDto): void {
|
||
const values: Record<string, string> = {
|
||
validatedNetwork: preview.network,
|
||
validatedHttpRole: preview.httpRole,
|
||
validatedCommitment: preview.commitment,
|
||
validatedScopeKind: preview.scopeKind,
|
||
validatedExplicitSignatureCount: preview.explicitSignatureCount.toString(),
|
||
validatedBounds: `${preview.pageSize} / ${preview.maxPages} / ${preview.maxCandidates} / ${preview.hydrationConcurrency}`,
|
||
validatedMinContextSlot: preview.minContextSlotPresent ? "présent" : "absent",
|
||
};
|
||
for (const [id, value] of Object.entries(values)) {
|
||
const element = document.querySelector<HTMLElement>(`#${id}`);
|
||
if (element) {
|
||
element.textContent = value;
|
||
}
|
||
}
|
||
const summary = document.querySelector<HTMLElement>("#backfillRequestPreview");
|
||
if (summary) {
|
||
summary.hidden = false;
|
||
}
|
||
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
|
||
if (status) {
|
||
status.hidden = false;
|
||
status.classList.remove("alert-secondary", "alert-danger");
|
||
status.classList.add("alert-success");
|
||
status.textContent = "Requête valide. Aucun job n'a été démarré.";
|
||
}
|
||
frontendTrace("main", "Backfill Desk validated request preview rendered", {
|
||
commitment: preview.commitment,
|
||
httpRole: preview.httpRole,
|
||
scopeKind: preview.scopeKind,
|
||
});
|
||
}
|
||
|
||
function renderRequestValidationError(caughtError: unknown): void {
|
||
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
|
||
if (!status) {
|
||
return;
|
||
}
|
||
let message = "Requête refusée par le backend.";
|
||
if (typeof caughtError === "object" && caughtError !== null) {
|
||
const candidate = caughtError as { code?: unknown; domain?: unknown; message?: unknown };
|
||
if (typeof candidate.domain === "string" && typeof candidate.code === "string" && typeof candidate.message === "string") {
|
||
message = `${candidate.domain}/${candidate.code}: ${candidate.message}`;
|
||
}
|
||
}
|
||
status.hidden = false;
|
||
status.classList.remove("alert-secondary", "alert-success");
|
||
status.classList.add("alert-danger");
|
||
status.textContent = message;
|
||
}
|
||
|
||
async function startBackfillRequest(): Promise<void> {
|
||
const request = buildBackfillStartRequest();
|
||
frontendDebug("main", "Backfill Desk campaign Start requested", {
|
||
commitment: request.commitment,
|
||
httpRole: request.httpRole,
|
||
scopeKind: request.scopeKind,
|
||
});
|
||
try {
|
||
const started = await invokeKsp<BackfillStartResponseDto>("main", "backfill_start", { request });
|
||
runStartAccepted = true;
|
||
refreshStartButton();
|
||
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
|
||
if (status) {
|
||
status.hidden = false;
|
||
status.classList.remove("alert-secondary", "alert-danger");
|
||
status.classList.add("alert-success");
|
||
status.textContent = `Job ${started.jobId} démarré (${started.state}). Le monitoring détaillé arrive en pre.009.`;
|
||
}
|
||
frontendInfo("main", "Backfill Desk campaign Start accepted", { jobId: started.jobId, state: started.state });
|
||
} catch (caughtError) {
|
||
renderRequestValidationError(caughtError);
|
||
frontendWarn("main", "Backfill Desk campaign Start failed");
|
||
}
|
||
}
|
||
|
||
async function validateBackfillRequest(): Promise<void> {
|
||
const request = buildBackfillStartRequest();
|
||
frontendDebug("main", "Backfill Desk campaign validation requested", {
|
||
commitment: request.commitment,
|
||
httpRole: request.httpRole,
|
||
scopeKind: request.scopeKind,
|
||
});
|
||
try {
|
||
const preview = await invokeKsp<BackfillRequestPreviewDto>("main", "backfill_validate_request", { request });
|
||
renderRequestPreview(preview);
|
||
frontendDebug("main", "Backfill Desk campaign validation completed", {
|
||
commitment: preview.commitment,
|
||
httpRole: preview.httpRole,
|
||
scopeKind: preview.scopeKind,
|
||
});
|
||
} catch (caughtError) {
|
||
renderRequestValidationError(caughtError);
|
||
frontendWarn("main", "Backfill Desk campaign validation failed");
|
||
}
|
||
}
|
||
|
||
function bindCampaignForm(): void {
|
||
const scope = document.querySelector<HTMLSelectElement>("#backfillScopeKind");
|
||
if (scope) {
|
||
scope.addEventListener("change", () => {
|
||
frontendDebug("main", "Backfill Desk campaign scope changed", { scopeKind: scope.value || "none" });
|
||
updateCampaignScopeFields();
|
||
invalidateCampaignValidation("scope");
|
||
});
|
||
}
|
||
const form = document.querySelector<HTMLFormElement>("#backfillCampaignForm");
|
||
if (form) {
|
||
form.addEventListener("input", () => invalidateCampaignValidation("input"));
|
||
form.addEventListener("submit", event => {
|
||
event.preventDefault();
|
||
void validateBackfillRequest();
|
||
});
|
||
}
|
||
const startButton = document.querySelector<HTMLButtonElement>("#startBackfillRequest");
|
||
if (startButton) {
|
||
startButton.addEventListener("click", () => {
|
||
void startBackfillRequest();
|
||
});
|
||
}
|
||
frontendTrace("main", "Backfill Desk campaign form handlers installed");
|
||
}
|
||
|
||
function routeLabel(route: BackfillHttpRouteOptionDto): string {
|
||
if (route.pooled) {
|
||
return `Pool multi-provider — ${route.providers.join(" + ")}`;
|
||
}
|
||
return route.providers.length === 1 ? `Provider — ${route.providers[0]}` : route.role;
|
||
}
|
||
|
||
function renderBackfillOptions(options: BackfillDeskOptionsDto): void {
|
||
compositionReadyForStart = options.compositionReady;
|
||
renderCampaignContract(options);
|
||
refreshStartButton();
|
||
const select = document.querySelector<HTMLSelectElement>("#backfillHttpRoute");
|
||
if (select) {
|
||
const previous = select.value;
|
||
select.replaceChildren();
|
||
for (const route of options.httpRoutes) {
|
||
const option = document.createElement("option");
|
||
option.value = route.role;
|
||
option.textContent = routeLabel(route);
|
||
select.append(option);
|
||
}
|
||
select.disabled = !options.transportReady || options.httpRoutes.length === 0;
|
||
const reusable = options.httpRoutes.some(route => route.role === previous);
|
||
if (reusable) {
|
||
select.value = previous;
|
||
}
|
||
}
|
||
const values: Record<string, string> = {
|
||
backfillNetwork: options.configuredNetworks.length === 1 ? options.configuredNetworks[0] : options.configuredNetworks.join(", ") || "indisponible",
|
||
backfillTransportReady: options.transportReady ? "ready" : "non ready",
|
||
backfillStoreReady: options.storeReady ? "ready" : "non ready",
|
||
backfillCompositionReady: options.compositionReady ? "ready" : "non ready",
|
||
};
|
||
for (const [id, value] of Object.entries(values)) {
|
||
const element = document.querySelector<HTMLElement>(`#${id}`);
|
||
if (element) {
|
||
element.textContent = value;
|
||
}
|
||
}
|
||
const diagnostic = document.querySelector<HTMLElement>("#backfillOptionsDiagnostic");
|
||
if (diagnostic) {
|
||
const error = options.transportDiagnostic ?? options.storeDiagnostic;
|
||
diagnostic.hidden = error === null;
|
||
diagnostic.textContent = error === null ? "" : `${error.domain}/${error.code}: ${error.message}`;
|
||
}
|
||
frontendTrace("main", "Backfill Desk HTTP route options rendered", {
|
||
compositionReady: options.compositionReady,
|
||
routeCount: options.httpRoutes.length,
|
||
transportReady: options.transportReady,
|
||
});
|
||
}
|
||
|
||
async function loadBackfillOptions(source: "startup" | "user"): Promise<void> {
|
||
frontendDebug("main", "Backfill Desk HTTP route options load started", { source });
|
||
const options = await invokeKsp<BackfillDeskOptionsDto>("main", "backfill_options");
|
||
renderBackfillOptions(options);
|
||
frontendDebug("main", "Backfill Desk HTTP route options load completed", { source, routeCount: options.httpRoutes.length });
|
||
}
|
||
|
||
function bindBackfillRouteSelection(): void {
|
||
const select = document.querySelector<HTMLSelectElement>("#backfillHttpRoute");
|
||
if (!select) {
|
||
return;
|
||
}
|
||
select.addEventListener("change", () => {
|
||
frontendDebug("main", "Backfill Desk HTTP route selection changed", { role: select.value || "none" });
|
||
invalidateCampaignValidation("route");
|
||
});
|
||
frontendTrace("main", "Backfill Desk HTTP route selection handler installed");
|
||
}
|
||
|
||
function renderRuntimeStatus(status: ShellStatusDto): void {
|
||
const values: Record<string, string> = {
|
||
runtimeVersion: status.applicationVersion,
|
||
runtimeShellPhase: status.shellPhase,
|
||
runtimeConfigDocuments: status.configDocumentCount.toString(),
|
||
runtimeLoggingProfile: status.activeLoggingProfile ?? "fallback transitoire",
|
||
runtimeLoggingFallback: status.fallbackLoggingActive ? "oui" : "non",
|
||
};
|
||
for (const [id, value] of Object.entries(values)) {
|
||
const element = document.querySelector<HTMLElement>(`#${id}`);
|
||
if (element) {
|
||
element.textContent = value;
|
||
}
|
||
}
|
||
const badge = document.querySelector<HTMLElement>("#appVersionBadge");
|
||
if (badge) {
|
||
badge.textContent = status.applicationVersion;
|
||
}
|
||
const diagnostic = document.querySelector<HTMLElement>("#runtimeDiagnostic");
|
||
if (diagnostic) {
|
||
diagnostic.hidden = status.startupDiagnostic === null;
|
||
diagnostic.textContent = status.startupDiagnostic === null ? "" : `${status.startupDiagnostic.domain}/${status.startupDiagnostic.code}: ${status.startupDiagnostic.message}`;
|
||
}
|
||
frontendTrace("main", "Backfill Desk runtime status rendered", {
|
||
fallbackLoggingActive: status.fallbackLoggingActive,
|
||
configDocumentCount: status.configDocumentCount,
|
||
});
|
||
}
|
||
|
||
async function loadRuntimeStatus(source: "startup" | "user"): Promise<void> {
|
||
frontendDebug("main", "Backfill Desk runtime status load started", { source });
|
||
const status = await invokeKsp<ShellStatusDto>("main", "get_runtime_status");
|
||
renderRuntimeStatus(status);
|
||
frontendDebug("main", "Backfill Desk runtime status load completed", { source });
|
||
}
|
||
|
||
function bindRuntimeStatusRefresh(): void {
|
||
const button = document.querySelector<HTMLButtonElement>("#refreshRuntimeStatus");
|
||
if (!button) {
|
||
return;
|
||
}
|
||
button.addEventListener("click", () => {
|
||
frontendDebug("main", "Backfill Desk runtime status refresh button clicked");
|
||
void loadRuntimeStatus("user").catch(() => frontendWarn("main", "Backfill Desk runtime status refresh failed"));
|
||
});
|
||
frontendTrace("main", "Backfill Desk runtime status refresh handler installed");
|
||
}
|
||
|
||
async function initializeMain(): Promise<void> {
|
||
const windowLabel = getCurrentWindow().label;
|
||
frontendInfo("main", "Backfill Desk main frontend loaded", { windowLabel });
|
||
bindFrontendInteractions();
|
||
bindNavigation();
|
||
bindRuntimeStatusRefresh();
|
||
bindBackfillRouteSelection();
|
||
bindCampaignForm();
|
||
activateView("backfill", "startup");
|
||
try {
|
||
await Promise.all([loadRuntimeStatus("startup"), loadBackfillOptions("startup")]);
|
||
} catch {
|
||
frontendWarn("main", "Backfill Desk startup runtime status load failed");
|
||
}
|
||
}
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
void initializeMain();
|
||
});
|