v0.1.0-pre.047

This commit is contained in:
2026-07-25 19:59:48 +02:00
parent 417536e4c4
commit 33a0762658
9 changed files with 1120 additions and 12 deletions

View File

@@ -0,0 +1,267 @@
// file: kb-app-demo-desktop/frontend/ts/demo_backfill.ts
// version: 5
import * as bootstrap from "bootstrap";
import "simplebar";
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 {
element<HTMLTextAreaElement>("#backfillSummaryOutput").value = JSON.stringify({ validation: message }, null, 2);
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);
element<HTMLTextAreaElement>("#backfillSummaryOutput").value = "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 });
element<HTMLTextAreaElement>("#backfillSummaryOutput").value = JSON.stringify(summary, null, 2);
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);
element<HTMLTextAreaElement>("#backfillSummaryOutput").value = JSON.stringify({ error: message }, null, 2);
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 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);
}
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<HTMLButtonElement>("#clearBackfillLogButton").addEventListener("click", () => {
logLines.length = 0;
element<HTMLTextAreaElement>("#backfillLogOutput").value = "";
});
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}`);
});
});

View File

@@ -36,3 +36,11 @@ document.addEventListener("DOMContentLoaded", async () => {
await loadReadme(readmeContent);
}
});
const openDemoBackfillLink = document.querySelector<HTMLAnchorElement>("#openDemoBackfillLink");
if (openDemoBackfillLink) {
openDemoBackfillLink.addEventListener("click", event => {
event.preventDefault();
void invoke("open_demo_backfill_window");
});
}