v0.1.0-pre.047
This commit is contained in:
267
kb-app-demo-desktop/frontend/ts/demo_backfill.ts
Normal file
267
kb-app-demo-desktop/frontend/ts/demo_backfill.ts
Normal 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 "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 {
|
||||
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}`);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user