Files
khadhroony-solana-project/crates/ksp-app-backfill-desk/frontend/ts/main.ts
2026-09-02 20:29:50 +02:00

779 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
// version: 8
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
import "simplebar";
import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { BackfillRunStatusDto } from "./bindings/ksp_app_backfill_desk/backfill_status/BackfillRunStatusDto.ts";
import type { BackfillCancelResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillCancelResponseDto.ts";
import type { BackfillRequestPreviewDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts";
import type { BackfillResumeResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillResumeResponseDto.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 { ProgramIdAutocompleteOptionDto } from "./bindings/ksp_app_backfill_desk/dto_common/ProgramIdAutocompleteOptionDto.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 currentRunStatus: BackfillRunStatusDto | null = null;
let runStartAccepted = false;
let resumeInFlight = 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 renderProgramIdAutocomplete(options: ProgramIdAutocompleteOptionDto[]): void {
const datalist = document.querySelector<HTMLDataListElement>("#backfillProgramIds");
if (!datalist) {
return;
}
datalist.replaceChildren();
for (const program of options) {
const option = document.createElement("option");
option.value = program.programId;
option.label = `${program.name}${program.code}`;
option.dataset.code = program.code;
option.dataset.domain = program.domain;
option.dataset.family = program.family;
option.dataset.name = program.name;
option.dataset.protocol = program.protocol;
datalist.append(option);
}
const help = document.querySelector<HTMLElement>("#backfillProgramIdAutocompleteHelp");
if (help) {
help.textContent = `${options.length} Program ID KSP disponibles comme suggestions ; la saisie reste libre.`;
}
frontendTrace("main", "Backfill Desk Program ID autocomplete dataset rendered", { optionCount: options.length });
}
function selectedProgramAutocompleteEntry(address: string): HTMLOptionElement | null {
const datalist = document.querySelector<HTMLDataListElement>("#backfillProgramIds");
if (!datalist) {
return null;
}
for (const option of Array.from(datalist.options)) {
if (option.value === address) {
return option;
}
}
return null;
}
function safeCommandErrorText(caughtError: unknown, fallback: string): string {
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") {
return `${candidate.domain}/${candidate.code}: ${candidate.message}`;
}
}
return fallback;
}
function renderCampaignContract(options: BackfillDeskOptionsDto): void {
renderProgramIdAutocomplete(options.programIdOptions);
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;
}
const message = safeCommandErrorText(caughtError, "Requête refusée par le backend.");
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}).`;
}
await syncBackfillStatus("start");
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 address = document.querySelector<HTMLInputElement>("#backfillAddress");
if (address) {
address.addEventListener("change", () => {
const selected = selectedProgramAutocompleteEntry(address.value);
frontendTrace("main", "Backfill Desk address autocomplete selection evaluated", {
programCode: selected?.dataset.code ?? null,
registryMatch: selected !== null,
});
});
}
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 renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "cancel" | "event" | "resume" | "startup" | "start" | "user"): void {
const card = document.querySelector<HTMLElement>("#backfillRunStatusCard");
const cancelButton = document.querySelector<HTMLButtonElement>("#cancelBackfillRun");
const resumeButton = document.querySelector<HTMLButtonElement>("#resumeBackfillRun");
currentRunStatus = status;
if (!status) {
runStartAccepted = false;
if (cancelButton) {
cancelButton.disabled = true;
}
if (resumeButton) {
resumeButton.disabled = true;
}
resumeInFlight = false;
refreshStartButton();
if (card) {
card.hidden = true;
}
const terminalSummary = document.querySelector<HTMLElement>("#backfillTerminalSummary");
if (terminalSummary) {
terminalSummary.hidden = true;
terminalSummary.textContent = "";
}
frontendTrace("main", "Backfill Desk latest-value status cleared", { source });
return;
}
runStartAccepted = status.active;
if (cancelButton) {
cancelButton.disabled = !status.active;
}
if (status.active) {
resumeInFlight = false;
}
if (resumeButton) {
resumeButton.disabled = !status.terminal || !status.checkpointPresent || resumeInFlight;
}
refreshStartButton();
if (card) {
card.hidden = false;
}
const terminalSummary = document.querySelector<HTMLElement>("#backfillTerminalSummary");
if (terminalSummary) {
terminalSummary.hidden = !status.terminal;
terminalSummary.classList.remove("alert-success", "alert-warning", "alert-danger", "alert-secondary");
if (status.terminal) {
const failure = status.failureDomain === null || status.failureCode === null ? null : `${status.failureDomain}/${status.failureCode}`;
if (status.state === "completed") {
terminalSummary.classList.add("alert-success");
terminalSummary.textContent = `Campagne terminée — ${status.contiguousCompleted} candidats contigus durablement traités.`;
} else if (status.state === "cancelled") {
terminalSummary.classList.add("alert-warning");
terminalSummary.textContent = `Campagne annulée coopérativement — checkpoint ${status.checkpointPresent ? "disponible" : "absent"}.`;
} else {
terminalSummary.classList.add("alert-danger");
terminalSummary.textContent = `Campagne terminée en échec${failure === null ? "" : `${failure}`}.`;
}
} else {
terminalSummary.classList.add("alert-secondary");
terminalSummary.textContent = "";
}
}
const values: Record<string, string> = {
runStatusJobId: status.jobId,
runStatusLifecycle: status.completion === null ? status.state : `${status.state} / ${status.completion}`,
runStatusPhase: status.phase,
runStatusSequence: status.sequence,
runStatusScope: status.scopeKind,
runStatusBoundary: status.discoveryBoundary ?? "—",
runStatusCandidates: `${status.candidatesSelected} / ${status.candidatesAdmitted} / ${status.candidatesFinished}`,
runStatusFrontier: `${status.contiguousCompleted} (holes ${status.holes})`,
runStatusEntities: `${status.entitiesInserted} / ${status.entitiesExisting} / ${status.entitiesPurged}`,
runStatusObservations: `${status.observationsInserted} / ${status.observationsExisting}`,
runStatusAnomalies: `missing ${status.missing} / conflits ${status.conflicts} / annulés ${status.cancelledCandidates}`,
runStatusConcurrency: status.maximumInFlight.toString(),
runStatusCheckpoint: status.checkpointPresent ? "présent" : "absent",
runStatusFailure: status.failureDomain === null || status.failureCode === null ? "—" : `${status.failureDomain}/${status.failureCode}`,
};
for (const [id, value] of Object.entries(values)) {
const element = document.querySelector<HTMLElement>(`#${id}`);
if (element) {
element.textContent = value;
}
}
frontendTrace("main", "Backfill Desk latest-value status rendered", {
active: status.active,
phase: status.phase,
sequence: status.sequence,
source,
state: status.state,
terminal: status.terminal,
});
}
async function syncBackfillStatus(source: "cancel" | "resume" | "startup" | "start" | "user"): Promise<void> {
frontendDebug("main", "Backfill Desk latest-value status resynchronization started", { source });
const status = await invokeKsp<BackfillRunStatusDto | null>("main", "backfill_status");
renderBackfillRunStatus(status, source);
frontendDebug("main", "Backfill Desk latest-value status resynchronization completed", { source, statusPresent: status !== null });
}
async function resumeBackfillRun(): Promise<void> {
const status = currentRunStatus;
if (!status || !status.terminal || !status.checkpointPresent || resumeInFlight) {
return;
}
const resumeButton = document.querySelector<HTMLButtonElement>("#resumeBackfillRun");
resumeInFlight = true;
if (resumeButton) {
resumeButton.disabled = true;
}
frontendDebug("main", "Backfill Desk in-session Resume requested", { jobId: status.jobId, state: status.state });
try {
const response = await invokeKsp<BackfillResumeResponseDto>("main", "backfill_resume");
const feedback = document.querySelector<HTMLElement>("#backfillResumeFeedback");
if (feedback) {
feedback.hidden = false;
feedback.textContent = `Checkpoint repris dans ${response.jobId} (${response.state}).`;
}
frontendInfo("main", "Backfill Desk in-session Resume accepted", {
accepted: response.accepted,
jobId: response.jobId,
state: response.state,
});
await syncBackfillStatus("resume");
} catch (caughtError) {
resumeInFlight = false;
if (resumeButton && currentRunStatus?.terminal && currentRunStatus.checkpointPresent) {
resumeButton.disabled = false;
}
const feedback = document.querySelector<HTMLElement>("#backfillResumeFeedback");
if (feedback) {
feedback.hidden = false;
feedback.classList.remove("alert-secondary", "alert-success");
feedback.classList.add("alert-danger");
feedback.textContent = safeCommandErrorText(caughtError, "La reprise du checkpoint a été refusée.");
}
frontendWarn("main", "Backfill Desk in-session Resume failed");
}
}
async function bindBackfillStatusMonitoring(): Promise<void> {
await listen<BackfillRunStatusDto>("ksp-backfill-status", event => {
renderBackfillRunStatus(event.payload, "event");
});
const button = document.querySelector<HTMLButtonElement>("#refreshBackfillStatus");
if (button) {
button.addEventListener("click", () => {
frontendDebug("main", "Backfill Desk latest-value status refresh button clicked");
void syncBackfillStatus("user").catch(() => frontendWarn("main", "Backfill Desk latest-value status refresh failed"));
});
}
const cancelButton = document.querySelector<HTMLButtonElement>("#cancelBackfillRun");
if (cancelButton) {
cancelButton.addEventListener("click", () => {
const status = currentRunStatus;
if (!status || !status.active) {
return;
}
cancelButton.disabled = true;
frontendDebug("main", "Backfill Desk cooperative cancellation requested", { jobId: status.jobId, state: status.state });
void invokeKsp<BackfillCancelResponseDto>("main", "backfill_cancel", { jobId: status.jobId })
.then(response => {
const feedback = document.querySelector<HTMLElement>("#backfillCancelFeedback");
if (feedback) {
feedback.hidden = false;
feedback.textContent = response.accepted
? `Annulation coopérative acceptée pour ${response.jobId}.`
: `Aucune nouvelle annulation acceptée pour ${response.jobId} (${response.state}).`;
}
frontendInfo("main", "Backfill Desk cooperative cancellation response received", {
accepted: response.accepted,
jobId: response.jobId,
state: response.state,
});
return syncBackfillStatus("cancel");
})
.catch(caughtError => {
const feedback = document.querySelector<HTMLElement>("#backfillCancelFeedback");
if (feedback) {
feedback.hidden = false;
feedback.classList.remove("alert-secondary", "alert-success");
feedback.classList.add("alert-danger");
feedback.textContent = safeCommandErrorText(caughtError, "La demande d'annulation a été refusée.");
}
frontendWarn("main", "Backfill Desk cooperative cancellation failed");
if (currentRunStatus?.active) {
cancelButton.disabled = false;
}
});
});
}
const resumeButton = document.querySelector<HTMLButtonElement>("#resumeBackfillRun");
if (resumeButton) {
resumeButton.addEventListener("click", () => {
void resumeBackfillRun();
});
}
frontendTrace("main", "Backfill Desk latest-value monitoring, cancellation and Resume handlers 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();
await bindBackfillStatusMonitoring();
activateView("backfill", "startup");
try {
await Promise.all([loadRuntimeStatus("startup"), loadBackfillOptions("startup"), syncBackfillStatus("startup")]);
} catch {
frontendWarn("main", "Backfill Desk startup runtime status load failed");
}
}
document.addEventListener("DOMContentLoaded", () => {
void initializeMain();
});