v0.3.7-pre.012

This commit is contained in:
2026-09-02 20:29:50 +02:00
parent 618f940310
commit 4c16607e22
14 changed files with 388 additions and 26 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
// version: 7
// version: 8
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
@@ -14,6 +14,7 @@ import type { BackfillStartRequestDto } from "./bindings/ksp_app_backfill_desk/d
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";
@@ -141,7 +142,56 @@ function refreshStartButton(): void {
}
}
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);
@@ -296,13 +346,7 @@ function renderRequestValidationError(caughtError: unknown): void {
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}`;
}
}
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");
@@ -373,6 +417,16 @@ function bindCampaignForm(): void {
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", () => {
@@ -472,6 +526,11 @@ function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "c
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;
}
@@ -489,6 +548,27 @@ function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "c
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}`,
@@ -552,11 +632,18 @@ async function resumeBackfillRun(): Promise<void> {
state: response.state,
});
await syncBackfillStatus("resume");
} catch (_caughtError) {
} 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");
}
}
@@ -597,7 +684,14 @@ async function bindBackfillStatusMonitoring(): Promise<void> {
});
return syncBackfillStatus("cancel");
})
.catch(() => {
.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;