v0.3.7-pre.007
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
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 { 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";
|
||||
@@ -21,6 +23,19 @@ const viewTitles: Record<ViewId, string> = {
|
||||
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",
|
||||
};
|
||||
|
||||
|
||||
function isViewId(value: string | undefined): value is ViewId {
|
||||
return value === "backfill" || value === "diagnostics";
|
||||
}
|
||||
@@ -79,6 +94,245 @@ function bindNavigation(): void {
|
||||
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 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 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();
|
||||
});
|
||||
}
|
||||
frontendTrace("main", "Backfill Desk campaign form handlers installed");
|
||||
}
|
||||
|
||||
function routeLabel(route: BackfillHttpRouteOptionDto): string {
|
||||
if (route.pooled) {
|
||||
@@ -88,6 +342,7 @@ function routeLabel(route: BackfillHttpRouteOptionDto): string {
|
||||
}
|
||||
|
||||
function renderBackfillOptions(options: BackfillDeskOptionsDto): void {
|
||||
renderCampaignContract(options);
|
||||
const select = document.querySelector<HTMLSelectElement>("#backfillHttpRoute");
|
||||
if (select) {
|
||||
const previous = select.value;
|
||||
@@ -143,6 +398,7 @@ function bindBackfillRouteSelection(): void {
|
||||
}
|
||||
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");
|
||||
}
|
||||
@@ -202,6 +458,7 @@ async function initializeMain(): Promise<void> {
|
||||
bindNavigation();
|
||||
bindRuntimeStatusRefresh();
|
||||
bindBackfillRouteSelection();
|
||||
bindCampaignForm();
|
||||
activateView("backfill", "startup");
|
||||
try {
|
||||
await Promise.all([loadRuntimeStatus("startup"), loadBackfillOptions("startup")]);
|
||||
|
||||
Reference in New Issue
Block a user