Files
khadhroony-solana-project/crates/ksp-app-backfill-desk/frontend/ts/main.ts
2026-09-02 16:07:57 +02:00

473 lines
20 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: 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";
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",
};
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 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) {
return `Pool multi-provider — ${route.providers.join(" + ")}`;
}
return route.providers.length === 1 ? `Provider — ${route.providers[0]}` : route.role;
}
function renderBackfillOptions(options: BackfillDeskOptionsDto): void {
renderCampaignContract(options);
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();
});