// file: kb-app-demo-desktop/frontend/ts/demo_backfill.ts // version: 6 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(selector: string): T { const value = document.querySelector(selector); if (!value) { throw new Error(`Missing UI element: ${selector}`); } return value; } function inputValue(selector: string): string { return element(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("#backfillLogOutput").value = logLines.join("\n"); element("#backfillLogOutput").scrollTop = element("#backfillLogOutput").scrollHeight; } function setRunning(value: boolean): void { running = value; document.querySelectorAll(".backfill-start-button").forEach(button => { button.disabled = value; }); element("#cancelBackfillButton").disabled = !value; const badge = element("#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("#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 "L’adresse est obligatoire pour ce mode de backfill."; } if (!decodesToSolanaAddress(request.address.trim())) { return "L’adresse 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 d’ancrage 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(`#${mode}DirectionSelect`).value; const anchor = element(`#${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 { 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("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 { try { const accepted = await invoke("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 { const options = await invoke("demo_backfill_options"); const roleSelect = element("#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("#backfillCommitmentSelect").value = options.defaultCommitment; element("#backfillPageSizeInput").value = String(options.defaultPageSize); element("#backfillMaxPagesInput").value = String(options.defaultMaxPages); element("#backfillConcurrencyInput").value = String(options.defaultMaxConcurrentRequests); element("#backfillRetriesInput").value = String(options.defaultMaxRetries); setRunning(options.running); } function renderBackfillSummary(value: unknown): void { const container = document.querySelector("#backfillSummaryOutput"); if (!container) { return; } container.textContent = ""; const viewer = document.createElement("andypf-json-viewer"); 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: "Aucun résultat" }, null, 2)); container.appendChild(viewer); } async function copyText(value: string): Promise { await navigator.clipboard.writeText(value); } 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(`#${mode}DirectionSelect`); direction.addEventListener("change", () => synchronizeAnchorRequirement(mode)); synchronizeAnchorRequirement(mode); } document.querySelectorAll(".backfill-start-button").forEach(button => { button.addEventListener("click", () => { const mode = button.dataset.backfillMode; if (mode) { void executeBackfill(mode); } }); }); element("#cancelBackfillButton").addEventListener("click", () => { void cancelBackfill(); }); element("#clearBackfillLogButton").addEventListener("click", () => { logLines.length = 0; element("#backfillLogOutput").value = ""; }); void listen("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}`); }); document.querySelector("#copyBackfillLogButton")?.addEventListener("click", async () => { const value = document.querySelector("#backfillLogOutput")?.value ?? ""; await copyText(value); }); document.querySelector("#copyBackfillSummaryButton")?.addEventListener("click", async () => { const value = document.querySelector("#backfillSummaryOutput andypf-json-viewer")?.getAttribute("data") ?? ""; await copyText(value); }); renderBackfillSummary(null); });