Files
khadhroony-bot3/kb-app-demo-desktop/frontend/ts/demo_backfill.ts
2026-07-28 18:41:30 +02:00

301 lines
13 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: kb-app-demo-desktop/frontend/ts/demo_backfill.ts
// version: 8
import * as bootstrap from "bootstrap";
import "simplebar";
import "@andypf/json-viewer";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import type { DemoBackfillOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_backfill/DemoBackfillOptionsPayload.ts";
import type { DemoBackfillProgressPayload } from "./bindings/kb_app_demo_desktop/demo_backfill/DemoBackfillProgressPayload.ts";
import type { DemoBackfillRequest } from "./bindings/kb_app_demo_desktop/demo_backfill/DemoBackfillRequest.ts";
import type { DemoBackfillSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_backfill/DemoBackfillSummaryPayload.ts";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
const logLines: string[] = [];
const maximumLogLines = 1000;
let running = false;
function element<T extends HTMLElement>(selector: string): T {
const value = document.querySelector<T>(selector);
if (!value) {
throw new Error(`Missing UI element: ${selector}`);
}
return value;
}
function inputValue(selector: string): string {
return element<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>(selector).value.trim();
}
function integerValue(selector: string): number {
const parsed = Number.parseInt(inputValue(selector), 10);
if (!Number.isFinite(parsed)) {
throw new Error(`Valeur numérique invalide pour ${selector}`);
}
return parsed;
}
const base58Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
function decodesToSolanaAddress(value: string): boolean {
if (value.length === 0) {
return false;
}
let numericValue = 0n;
for (const character of value) {
const digit = base58Alphabet.indexOf(character);
if (digit < 0) {
return false;
}
numericValue = numericValue * 58n + BigInt(digit);
}
let decodedNonZeroBytes = 0;
let remaining = numericValue;
while (remaining > 0n) {
decodedNonZeroBytes += 1;
remaining >>= 8n;
}
let leadingZeroBytes = 0;
for (const character of value) {
if (character !== "1") {
break;
}
leadingZeroBytes += 1;
}
return leadingZeroBytes + decodedNonZeroBytes === 32;
}
function appendLog(payload: DemoBackfillProgressPayload | { timestamp: string; level: string; message: string }): void {
const progress = "completed" in payload && payload.completed !== null && payload.total !== null
? ` [${payload.completed}/${payload.total}]`
: "";
logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()}${progress} ${payload.message}`);
while (logLines.length > maximumLogLines) {
logLines.shift();
}
element<HTMLTextAreaElement>("#backfillLogOutput").value = logLines.join("\n");
element<HTMLTextAreaElement>("#backfillLogOutput").scrollTop = element<HTMLTextAreaElement>("#backfillLogOutput").scrollHeight;
}
function setRunning(value: boolean): void {
running = value;
document.querySelectorAll<HTMLButtonElement>(".backfill-start-button").forEach(button => {
button.disabled = value;
});
element<HTMLButtonElement>("#cancelBackfillButton").disabled = !value;
const badge = element<HTMLElement>("#backfillStatusBadge");
badge.textContent = value ? "Backfill en cours" : "Prêt";
badge.className = value ? "badge text-bg-warning" : "badge text-bg-success";
}
function commonRequest(mode: string): DemoBackfillRequest {
return {
role: inputValue("#backfillRoleSelect"),
commitment: inputValue("#backfillCommitmentSelect"),
mode,
signaturesText: null,
address: null,
anchorSignature: null,
direction: null,
limit: 1,
pageSize: integerValue("#backfillPageSizeInput"),
maxPages: integerValue("#backfillMaxPagesInput"),
maxConcurrentRequests: integerValue("#backfillConcurrencyInput"),
maxRetries: integerValue("#backfillRetriesInput"),
};
}
function buildRequest(mode: string): DemoBackfillRequest {
const request = commonRequest(mode);
if (mode === "explicit_signatures") {
request.signaturesText = element<HTMLTextAreaElement>("#explicitSignaturesTextarea").value;
return request;
}
request.address = inputValue(`#${mode}AddressInput`);
const anchorSignature = inputValue(`#${mode}AnchorInput`);
request.anchorSignature = anchorSignature.length > 0 ? anchorSignature : null;
request.direction = inputValue(`#${mode}DirectionSelect`);
request.limit = integerValue(`#${mode}LimitInput`);
return request;
}
function validationMessage(request: DemoBackfillRequest): string | null {
if (request.mode === "explicit_signatures") {
const signatureCount = request.signaturesText
? request.signaturesText.split(/\r?\n/).filter(value => value.trim().length > 0).length
: 0;
if (signatureCount === 0) {
return "Ajouter au moins une signature explicite avant de lancer le backfill.";
}
return null;
}
if (!request.address || request.address.trim().length === 0) {
return "Ladresse est obligatoire pour ce mode de backfill.";
}
if (!decodesToSolanaAddress(request.address.trim())) {
return "Ladresse Solana doit être une valeur Base58 décodant exactement sur 32 octets.";
}
if (request.direction === "after" && (!request.anchorSignature || request.anchorSignature.trim().length === 0)) {
return "La signature dancrage est obligatoire pour rechercher des transactions plus récentes.";
}
return null;
}
function reportValidationWarning(message: string): void {
renderBackfillSummary({ validation: message });
appendLog({ timestamp: new Date().toISOString(), level: "warn", message });
frontendDebug("kb-app-demo-desktop.frontend.backfill", `Backfill request rejected locally: ${message}`);
}
function synchronizeAnchorRequirement(mode: "program" | "token" | "pool"): void {
const direction = element<HTMLSelectElement>(`#${mode}DirectionSelect`).value;
const anchor = element<HTMLInputElement>(`#${mode}AnchorInput`);
const required = direction === "after";
anchor.required = required;
anchor.placeholder = required
? "Signature obligatoire pour rechercher après cette transaction"
: "Vide : commencer depuis les transactions les plus récentes";
}
async function executeBackfill(mode: string): Promise<void> {
if (running) {
return;
}
try {
const request = buildRequest(mode);
const invalid = validationMessage(request);
if (invalid) {
reportValidationWarning(invalid);
return;
}
setRunning(true);
renderBackfillSummary({ status: "Exécution en cours" });
appendLog({ timestamp: new Date().toISOString(), level: "info", message: `Démarrage du mode ${mode}` });
const summary = await invoke<DemoBackfillSummaryPayload>("demo_backfill_execute", { request });
renderBackfillSummary(summary);
appendLog({
timestamp: new Date().toISOString(),
level: summary.cancelled ? "warn" : "info",
message: `Campagne terminée : completed=${summary.candidatesCompleted}, cancelled=${summary.candidatesCancelled}, notStarted=${summary.candidatesNotStarted}, inserted=${summary.canonicalInserted}, existing=${summary.existingSkipped}, missing=${summary.missing}, failed=${summary.failed}`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
renderBackfillSummary({ error: message });
appendLog({ timestamp: new Date().toISOString(), level: "error", message });
frontendError("kb-app-demo-desktop.frontend.backfill", `Backfill failed: ${message}`);
} finally {
setRunning(false);
}
}
async function cancelBackfill(): Promise<void> {
try {
const accepted = await invoke<boolean>("demo_backfill_cancel");
appendLog({
timestamp: new Date().toISOString(),
level: accepted ? "warn" : "info",
message: accepted ? "Demande d'arrêt envoyée." : "Aucune campagne active.",
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb-app-demo-desktop.frontend.backfill", `Cancellation failed: ${message}`);
}
}
async function loadOptions(): Promise<void> {
const options = await invoke<DemoBackfillOptionsPayload>("demo_backfill_options");
const optionsWithPrograms = options as DemoBackfillOptionsPayload & {
programs: Array<{ code: string; programId: string }>;
};
const programOptions = element<HTMLDataListElement>("#programAddressOptions");
programOptions.replaceChildren();
for (const program of optionsWithPrograms.programs) {
const option = document.createElement("option");
option.value = program.programId;
option.label = program.code;
programOptions.append(option);
}
const roleSelect = element<HTMLSelectElement>("#backfillRoleSelect");
roleSelect.replaceChildren();
for (const role of options.roles) {
const option = document.createElement("option");
option.value = role.role;
option.textContent = `${role.role}${role.providers.join(", ")}`;
option.selected = role.role === options.defaultRole;
roleSelect.append(option);
}
if (options.roles.length === 0) {
const option = document.createElement("option");
option.value = "";
option.textContent = "Aucun rôle compatible";
roleSelect.append(option);
}
element<HTMLSelectElement>("#backfillCommitmentSelect").value = options.defaultCommitment;
element<HTMLInputElement>("#backfillPageSizeInput").value = String(options.defaultPageSize);
element<HTMLInputElement>("#backfillMaxPagesInput").value = String(options.defaultMaxPages);
element<HTMLInputElement>("#backfillConcurrencyInput").value = String(options.defaultMaxConcurrentRequests);
element<HTMLInputElement>("#backfillRetriesInput").value = String(options.defaultMaxRetries);
setRunning(options.running);
}
function renderBackfillSummary(value: unknown): void {
const container = document.querySelector<HTMLElement>("#backfillSummaryOutput");
if (!container) {
return;
}
container.textContent = "";
const viewer = document.createElement("andypf-json-viewer");
viewer.classList.add("app-json-viewer-element");
viewer.setAttribute("indent", "2");
viewer.setAttribute("expanded", "1");
viewer.setAttribute("theme", "default-light");
viewer.setAttribute("show-data-types", "true");
viewer.setAttribute("show-toolbar", "false");
viewer.setAttribute("show-copy", "true");
viewer.setAttribute("data", JSON.stringify(value ?? { status: "idle", message: "Aucune campagne exécutée." }, null, 2));
container.appendChild(viewer);
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb-app-demo-desktop.frontend.backfill");
frontendDebug("kb-app-demo-desktop.frontend.backfill", "backfill demo window loaded");
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item));
for (const mode of ["program", "token", "pool"] as const) {
const direction = element<HTMLSelectElement>(`#${mode}DirectionSelect`);
direction.addEventListener("change", () => synchronizeAnchorRequirement(mode));
synchronizeAnchorRequirement(mode);
}
document.querySelectorAll<HTMLButtonElement>(".backfill-start-button").forEach(button => {
button.addEventListener("click", () => {
const mode = button.dataset.backfillMode;
if (mode) {
void executeBackfill(mode);
}
});
});
element<HTMLButtonElement>("#cancelBackfillButton").addEventListener("click", () => {
void cancelBackfill();
});
element<HTMLTextAreaElement>("#backfillLogOutput").addEventListener("app-log-clear", () => {
logLines.length = 0;
});
void listen<DemoBackfillProgressPayload>("demo-backfill-progress", event => {
appendLog(event.payload);
});
void loadOptions().catch(caughtError => {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
appendLog({ timestamp: new Date().toISOString(), level: "error", message });
frontendError("kb-app-demo-desktop.frontend.backfill", `Options loading failed: ${message}`);
});
renderBackfillSummary(null);
});