v0.1.0-pre.050

This commit is contained in:
2026-07-25 21:04:56 +02:00
parent 8286dc0919
commit 50fd7467f8
23 changed files with 3129 additions and 27 deletions

View File

@@ -0,0 +1,60 @@
// file: kb-app-demo-desktop/frontend/ts/demo_sql_diag.ts
// version: 2
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import { jsonText, renderJsonViewer, renderTables, setText } from "./demo_sql_tables";
import type { DemoSqlDiagPayload } from "./bindings/kb_app_demo_desktop/demo_sql_diag/DemoSqlDiagPayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
let latestPayload: DemoSqlDiagPayload | null = null;
async function refreshDiagnostics(): Promise<void> {
setText("#sqlDiagStatus", "Chargement...");
try {
const payload = await invoke<DemoSqlDiagPayload>("load_demo_sql_diag");
setText("#sqlDiagConfigPath", payload.configPath);
setText("#sqlDiagProfile", payload.activeProfileName);
setText("#sqlDiagBackend", payload.backend);
setText("#sqlDiagDsn", payload.maskedDsn);
setText("#sqlDiagSchema", payload.currentSchema ?? "—");
setText("#sqlDiagHealth", payload.healthStatus);
setText("#sqlDiagMigration", payload.migrationStatus);
setText("#sqlDiagStatus", "OK");
renderTables("#sqlDiagTableBody", payload.tables);
latestPayload = payload;
renderJsonViewer("#sqlDiagJson", payload);
frontendDebug("kb_app_demo.frontend.demo_sql_diag", "SQL diagnostics refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#sqlDiagStatus", `Erreur : ${message}`);
latestPayload = null;
renderJsonViewer("#sqlDiagJson", { error: message });
frontendError("kb_app_demo.frontend.demo_sql_diag", `SQL diagnostics loading failed: ${message}`);
}
}
async function copyDiagnostics(): Promise<void> {
if (latestPayload) {
await navigator.clipboard.writeText(jsonText(latestPayload));
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_sql_diag");
frontendDebug("kb_app_demo.frontend.demo_sql_diag", "SQL diagnostics demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLButtonElement>("#refreshSqlDiagButton")?.addEventListener("click", () => {
void refreshDiagnostics();
});
document.querySelector<HTMLButtonElement>("#copySqlDiagButton")?.addEventListener("click", () => {
void copyDiagnostics();
});
void refreshDiagnostics();
});

View File

@@ -0,0 +1,42 @@
// file: kb-app-demo-desktop/frontend/ts/demo_sql_pg_core.ts
// version: 3
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import { renderJsonViewer, renderTables, setText } from "./demo_sql_tables";
import type { DemoSqlPgCorePayload } from "./bindings/kb_app_demo_desktop/demo_sql_pg_core/DemoSqlPgCorePayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
async function refreshCore(): Promise<void> {
setText("#sqlCoreStatus", "Chargement...");
try {
const payload = await invoke<DemoSqlPgCorePayload>("load_demo_sql_pg_core");
setText("#sqlCoreProfile", payload.activeProfileName);
setText("#sqlCoreDsn", payload.maskedDsn);
setText("#sqlCoreStatus", "OK");
renderTables("#sqlCoreTableBody", payload.tables);
renderJsonViewer("#sqlCoreJson", payload);
frontendDebug("kb_app_demo.frontend.demo_sql_pg_core", "PostgreSQL core diagnostics refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#sqlCoreStatus", `Erreur : ${message}`);
renderJsonViewer("#sqlCoreJson", { error: message });
frontendError("kb_app_demo.frontend.demo_sql_pg_core", `PostgreSQL core diagnostics loading failed: ${message}`);
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_sql_pg_core");
frontendDebug("kb_app_demo.frontend.demo_sql_pg_core", "PostgreSQL core demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLButtonElement>("#refreshSqlCoreButton")?.addEventListener("click", () => {
void refreshCore();
});
void refreshCore();
});

View File

@@ -0,0 +1,42 @@
// file: kb-app-demo-desktop/frontend/ts/demo_sql_pg_raw.ts
// version: 3
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import { renderJsonViewer, renderTables, setText } from "./demo_sql_tables";
import type { DemoSqlPgRawPayload } from "./bindings/kb_app_demo_desktop/demo_sql_pg_raw/DemoSqlPgRawPayload";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
async function refreshRaw(): Promise<void> {
setText("#sqlRawStatus", "Chargement...");
try {
const payload = await invoke<DemoSqlPgRawPayload>("load_demo_sql_pg_raw");
setText("#sqlRawProfile", payload.activeProfileName);
setText("#sqlRawDsn", payload.maskedDsn);
setText("#sqlRawStatus", "OK");
renderTables("#sqlRawTableBody", payload.tables);
renderJsonViewer("#sqlRawJson", payload);
frontendDebug("kb_app_demo.frontend.demo_sql_pg_raw", "PostgreSQL raw diagnostics refreshed");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
setText("#sqlRawStatus", `Erreur : ${message}`);
renderJsonViewer("#sqlRawJson", { error: message });
frontendError("kb_app_demo.frontend.demo_sql_pg_raw", `PostgreSQL raw diagnostics loading failed: ${message}`);
}
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo.frontend.demo_sql_pg_raw");
frontendDebug("kb_app_demo.frontend.demo_sql_pg_raw", "PostgreSQL raw demo window loaded");
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
document.querySelector<HTMLButtonElement>("#refreshSqlRawButton")?.addEventListener("click", () => {
void refreshRaw();
});
void refreshRaw();
});

View File

@@ -0,0 +1,800 @@
// file: kb-app-demo-desktop/frontend/ts/demo_sql_replay_candidates.ts
// version: 7
import * as bootstrap from "bootstrap";
import "simplebar";
import ResizeObserver from "resize-observer-polyfill";
import DataTable, { type Api } from "datatables.net-bs5";
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
import "datatables.net-select-bs5";
import "datatables.net-select-bs5/css/select.bootstrap5.css";
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, installFrontendConsoleBridge } from "./frontend_log";
import type { DemoSqlReplayEntityRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRequest";
import type { DemoSqlReplayEntityRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayEntityRow";
import type { DemoSqlReplayKnownProgramOption } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayKnownProgramOption";
import type { DemoSqlReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayOptionsPayload";
import type { DemoSqlReplayProgramRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRequest";
import type { DemoSqlReplayProgramRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayProgramRow";
import type { DemoSqlReplayTransactionRequest } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRequest";
import type { DemoSqlReplayTransactionRow } from "./bindings/kb_app_demo_desktop/demo_sql_replay_candidates/DemoSqlReplayTransactionRow";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
type EntityKindCode = "mint" | "owner" | "account_key";
type EntityViewConfig = {
kind: EntityKindCode;
label: string;
pluralLabel: string;
tableSelector: string;
containsSelector: string;
limitSelector: string;
summarySelector: string;
loadButtonSelector: string;
resetButtonSelector: string;
useButtonSelector: string;
copyButtonSelector: string;
exportButtonSelector: string;
fileName: string;
};
const tracingTarget = "kb_app_demo.frontend.demo_sql_replay_candidates";
const entityViewConfigs: EntityViewConfig[] = [
{
kind: "mint",
label: "mint",
pluralLabel: "mints",
tableSelector: "#mintCandidateTable",
containsSelector: "#mintContainsInput",
limitSelector: "#mintLimitInput",
summarySelector: "#mintResultSummary",
loadButtonSelector: "#loadMintsButton",
resetButtonSelector: "#resetMintsButton",
useButtonSelector: "#useSelectedMintsButton",
copyButtonSelector: "#copyMintsButton",
exportButtonSelector: "#exportMintsCsvButton",
fileName: "replay_mints.csv",
},
{
kind: "owner",
label: "owner",
pluralLabel: "owners",
tableSelector: "#ownerCandidateTable",
containsSelector: "#ownerContainsInput",
limitSelector: "#ownerLimitInput",
summarySelector: "#ownerResultSummary",
loadButtonSelector: "#loadOwnersButton",
resetButtonSelector: "#resetOwnersButton",
useButtonSelector: "#useSelectedOwnersButton",
copyButtonSelector: "#copyOwnersButton",
exportButtonSelector: "#exportOwnersCsvButton",
fileName: "replay_owners.csv",
},
{
kind: "account_key",
label: "compte",
pluralLabel: "comptes",
tableSelector: "#accountCandidateTable",
containsSelector: "#accountContainsInput",
limitSelector: "#accountLimitInput",
summarySelector: "#accountResultSummary",
loadButtonSelector: "#loadAccountsButton",
resetButtonSelector: "#resetAccountsButton",
useButtonSelector: "#useSelectedAccountsButton",
copyButtonSelector: "#copyAccountsButton",
exportButtonSelector: "#exportAccountsCsvButton",
fileName: "replay_account_keys.csv",
},
];
let transactionTable: Api<DemoSqlReplayTransactionRow> | null = null;
let programTable: Api<DemoSqlReplayProgramRow> | null = null;
let mintTable: Api<DemoSqlReplayEntityRow> | null = null;
let ownerTable: Api<DemoSqlReplayEntityRow> | null = null;
let accountTable: Api<DemoSqlReplayEntityRow> | null = null;
let busyOperationCount = 0;
let maximumReplayCandidateLimit = 5_000;
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 optionalInputValue(selector: string): string | null {
const value = element<HTMLInputElement | HTMLSelectElement>(selector).value.trim();
return value.length === 0 ? null : value;
}
function requiredInputValue(selector: string): string {
const value = optionalInputValue(selector);
if (value === null) {
throw new Error(`Valeur obligatoire manquante pour ${selector}`);
}
return value;
}
function positiveIntegerValue(selector: string): number {
const value = Number.parseInt(requiredInputValue(selector), 10);
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(`Valeur entière positive invalide pour ${selector}`);
}
return value;
}
function boundedPositiveIntegerValue(selector: string): number {
const value = positiveIntegerValue(selector);
if (value > maximumReplayCandidateLimit) {
throw new Error(
`La limite doit être comprise entre 1 et ${maximumReplayCandidateLimit} pour ${selector}`,
);
}
return value;
}
function optionalNonNegativeIntegerValue(selector: string): number | null {
const raw = optionalInputValue(selector);
if (raw === null) {
return null;
}
const value = Number.parseInt(raw, 10);
if (!Number.isSafeInteger(value) || value < 0) {
throw new Error(`Valeur entière positive ou nulle invalide pour ${selector}`);
}
return value;
}
function escapeHtml(value: unknown): string {
const text = String(value ?? "");
return text
.split("&").join("&amp;")
.split("<").join("&lt;")
.split(">").join("&gt;")
.split('"').join("&quot;")
.split("'").join("&#039;");
}
function codeDisplay(value: unknown, type: string): string {
if (type !== "display") {
return String(value ?? "");
}
return `<code class="text-break">${escapeHtml(value)}</code>`;
}
function compactIdentifier(value: string, leadingLength: number = 12, trailingLength: number = 10): string {
if (value.length <= leadingLength + trailingLength + 1) {
return value;
}
return `${value.slice(0, leadingLength)}${value.slice(-trailingLength)}`;
}
function copyableCodeDisplay(label: string): (value: unknown, type: string) => string {
return (value: unknown, type: string): string => {
const text = String(value ?? "");
if (type !== "display") {
return text;
}
const escapedText = escapeHtml(text);
const compactText = escapeHtml(compactIdentifier(text));
const escapedLabel = escapeHtml(label);
return `<span class="d-inline-flex align-items-center gap-1 text-nowrap"><code data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedText}">${compactText}</code><button class="btn btn-sm btn-link text-secondary p-0 replay-cell-copy" type="button" data-bs-toggle="tooltip" data-bs-placement="top" title="Copier ${escapedLabel}" aria-label="Copier ${escapedLabel}" data-copy-value="${escapedText}" data-copy-label="${escapedLabel}"><i class="fa-regular fa-copy" aria-hidden="true"></i></button></span>`;
};
}
function badgeDisplay(value: unknown, type: string): string {
if (type !== "display") {
return String(value ?? "");
}
const text = String(value ?? "—");
const className = text === "succeeded" || text === "core_extracted"
? "text-bg-success"
: text === "failed"
? "text-bg-danger"
: text === "running" || text === "received"
? "text-bg-warning"
: "text-bg-secondary";
return `<span class="badge ${className}">${escapeHtml(text)}</span>`;
}
function coreStatusText(row: DemoSqlReplayTransactionRow): string {
if (!row.hasCoreTransaction) {
return "absent";
}
if (row.transactionFailed === true) {
return "échec on-chain";
}
return "présent";
}
function coreDisplay(_value: unknown, type: string, row: DemoSqlReplayTransactionRow): string | number {
if (type === "display") {
if (!row.hasCoreTransaction) {
return '<span class="badge text-bg-secondary">absent</span>';
}
if (row.transactionFailed === true) {
return '<span class="badge text-bg-danger" title="La transaction Solana a échoué on-chain, mais son graphe core a été extrait.">échec on-chain</span>';
}
return '<span class="badge text-bg-success">présent</span>';
}
if (type === "filter") {
return coreStatusText(row);
}
if (row.transactionFailed === true) {
return 0;
}
return row.hasCoreTransaction ? 1 : 2;
}
function occurrenceRatioDisplay(_value: unknown, type: string, row: DemoSqlReplayEntityRow): string | number {
const ratio = row.transactionCount > 0 ? row.occurrenceCount / row.transactionCount : 0;
if (type === "display" || type === "filter") {
return ratio.toFixed(2);
}
return ratio;
}
function slotSpanDisplay(_value: unknown, type: string, row: DemoSqlReplayEntityRow): string | number {
const span = Math.max(0, row.maxSlot - row.minSlot);
if (type === "display" || type === "filter") {
return String(span);
}
return span;
}
function commonLanguage(emptyTable: string, infoLabel: string, zeroRecords: string): object {
return {
emptyTable,
info: `_START_ à _END_ sur _TOTAL_ ${infoLabel}`,
infoEmpty: `0 ${infoLabel}`,
infoFiltered: "(filtré depuis _MAX_)",
lengthMenu: "Afficher _MENU_",
search: "Filtre local :",
zeroRecords,
paginate: { first: "Premier", last: "Dernier", next: "Suivant", previous: "Précédent" },
select: { rows: { _: "%d lignes sélectionnées", 0: "", 1: "1 ligne sélectionnée" } },
};
}
function createTransactionTable(): Api<DemoSqlReplayTransactionRow> {
return new DataTable<DemoSqlReplayTransactionRow>("#transactionCandidateTable", {
data: [],
columns: [
{
data: null,
className: "replay-select-column text-center",
width: "1%",
orderable: false,
searchable: false,
render: DataTable.render.select(),
},
{ data: "signature", render: copyableCodeDisplay("la signature") },
{ data: "slot" },
{ data: "rawProcessingState", render: badgeDisplay },
{ data: "hasCoreTransaction", render: coreDisplay },
{ data: "ledgerStatus", render: badgeDisplay },
{ data: "processorVersion", defaultContent: "—", render: codeDisplay },
{ data: "attemptCount" },
{ data: "outerInstructionCount" },
{ data: "innerInstructionCount" },
{ data: "outerProgramCount" },
{ data: "innerProgramCount" },
{ data: "updatedAt" },
],
order: [[2, "desc"]],
pageLength: 10,
lengthMenu: [10, 25, 50, 100, 250],
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
language: commonLanguage(
"Aucune transaction chargée",
"transactions",
"Aucune transaction ne correspond au filtre local",
),
drawCallback: () => initializeTooltips(),
});
}
function createProgramTable(): Api<DemoSqlReplayProgramRow> {
return new DataTable<DemoSqlReplayProgramRow>("#programCandidateTable", {
data: [],
columns: [
{
data: null,
className: "replay-select-column text-center",
width: "1%",
orderable: false,
searchable: false,
render: DataTable.render.select(),
},
{ data: "programCode", defaultContent: "—", render: codeDisplay },
{ data: "programId", render: copyableCodeDisplay("le program ID") },
{ data: "transactionCount" },
{ data: "outerInstructionCount" },
{ data: "innerInstructionCount" },
{ data: "logCount" },
{ data: "minSlot" },
{ data: "maxSlot" },
],
order: [[3, "desc"]],
pageLength: 10,
lengthMenu: [10, 25, 50, 100, 250],
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
language: commonLanguage(
"Aucun programme chargé",
"programmes",
"Aucun programme ne correspond au filtre local",
),
drawCallback: () => initializeTooltips(),
});
}
function createEntityTable(config: EntityViewConfig): Api<DemoSqlReplayEntityRow> {
return new DataTable<DemoSqlReplayEntityRow>(config.tableSelector, {
data: [],
columns: [
{
data: null,
className: "replay-select-column text-center",
width: "1%",
orderable: false,
searchable: false,
render: DataTable.render.select(),
},
{ data: "entityValue", render: copyableCodeDisplay(config.label) },
{ data: "transactionCount" },
{ data: "occurrenceCount" },
{ data: null, render: occurrenceRatioDisplay },
{ data: "minSlot" },
{ data: "maxSlot" },
{ data: null, render: slotSpanDisplay },
],
order: [[2, "desc"]],
pageLength: 10,
lengthMenu: [10, 25, 50, 100, 250],
select: { style: "multi", selector: "td:first-child", headerCheckbox: "select-all" },
language: commonLanguage(
`Aucun ${config.label} chargé`,
config.pluralLabel,
`Aucun ${config.label} ne correspond au filtre local`,
),
drawCallback: () => initializeTooltips(),
});
}
function entityTable(kind: EntityKindCode): Api<DemoSqlReplayEntityRow> | null {
if (kind === "mint") {
return mintTable;
}
if (kind === "owner") {
return ownerTable;
}
return accountTable;
}
function replaceRows<T>(table: Api<T>, rows: T[]): void {
table.clear();
table.rows.add(rows);
table.draw();
}
function resetTableState<T>(table: Api<T> | null): void {
if (table === null) {
return;
}
table.search("");
table.columns().search("");
table.rows().deselect();
table.draw();
}
function selectedOrFilteredRows<T>(table: Api<T>): T[] {
const selected = table.rows({ selected: true, search: "applied" }).data().toArray();
if (selected.length > 0) {
return selected;
}
return table.rows({ search: "applied" }).data().toArray();
}
function selectedOrSingleFilteredRow<T>(table: Api<T>, label: string): T {
const selected = table.rows({ selected: true }).data().toArray();
if (selected.length === 1) {
return selected[0];
}
if (selected.length > 1) {
throw new Error(`Plusieurs ${label} sont cochés. Conservez une seule sélection.`);
}
const filtered = table.rows({ search: "applied" }).data().toArray();
if (filtered.length === 1) {
return filtered[0];
}
throw new Error(`Cochez exactement un ${label}, ou réduisez le filtre local à une seule ligne.`);
}
function setBusy(busy: boolean, message: string): void {
busyOperationCount = busy ? busyOperationCount + 1 : Math.max(0, busyOperationCount - 1);
const operationInProgress = busyOperationCount > 0;
const badge = element<HTMLElement>("#replayCandidateStatusBadge");
badge.textContent = operationInProgress ? (busy ? message : "Chargement en cours") : "Prêt";
badge.className = operationInProgress ? "badge text-bg-warning" : "badge text-bg-success";
document.querySelectorAll<HTMLButtonElement>("button").forEach(button => {
if (button.id !== "openCoreExtractionButton") {
button.disabled = operationInProgress;
}
});
}
function buildTransactionRequest(): DemoSqlReplayTransactionRequest {
const entityKind = optionalInputValue("#transactionEntityKindSelect");
const entityValue = optionalInputValue("#transactionEntityValueInput");
if ((entityKind === null) !== (entityValue === null)) {
throw new Error("Le type dentité et sa valeur doivent être renseignés ensemble.");
}
return {
signatureContains: optionalInputValue("#transactionSignatureContainsInput"),
minSlot: optionalNonNegativeIntegerValue("#transactionMinSlotInput"),
maxSlot: optionalNonNegativeIntegerValue("#transactionMaxSlotInput"),
rawProcessingState: optionalInputValue("#transactionRawStateSelect"),
ledgerStatus: optionalInputValue("#transactionLedgerStatusSelect"),
programId: optionalInputValue("#transactionProgramIdInput"),
programScope: requiredInputValue("#transactionProgramScopeSelect"),
entityKind,
entityValue,
limit: boundedPositiveIntegerValue("#transactionLimitInput"),
newestFirst: element<HTMLInputElement>("#transactionNewestFirstInput").checked,
};
}
async function resetTransactionFilters(): Promise<void> {
element<HTMLInputElement>("#transactionSignatureContainsInput").value = "";
element<HTMLInputElement>("#transactionMinSlotInput").value = "";
element<HTMLInputElement>("#transactionMaxSlotInput").value = "";
element<HTMLSelectElement>("#transactionRawStateSelect").value = "";
element<HTMLSelectElement>("#transactionLedgerStatusSelect").value = "";
element<HTMLInputElement>("#transactionProgramIdInput").value = "";
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
element<HTMLSelectElement>("#transactionEntityKindSelect").value = "";
element<HTMLInputElement>("#transactionEntityValueInput").value = "";
element<HTMLInputElement>("#transactionLimitInput").value = "500";
element<HTMLInputElement>("#transactionNewestFirstInput").checked = true;
resetTableState(transactionTable);
await loadTransactions();
}
async function resetProgramFilters(): Promise<void> {
element<HTMLSelectElement>("#knownProgramSelect").value = "";
element<HTMLInputElement>("#programContainsInput").value = "";
element<HTMLInputElement>("#programLimitInput").value = "500";
resetTableState(programTable);
await loadPrograms();
}
async function resetEntityFilters(config: EntityViewConfig): Promise<void> {
element<HTMLInputElement>(config.containsSelector).value = "";
element<HTMLInputElement>(config.limitSelector).value = "500";
resetTableState(entityTable(config.kind));
await loadEntity(config);
}
async function loadTransactions(): Promise<void> {
if (transactionTable === null) {
return;
}
try {
setBusy(true, "Chargement transactions");
const request = buildTransactionRequest();
const rows = await invoke<DemoSqlReplayTransactionRow[]>("load_demo_sql_replay_transactions", { request });
replaceRows(transactionTable, rows);
element<HTMLElement>("#transactionResultSummary").textContent = `${rows.length} transaction(s) chargée(s) depuis PostgreSQL.`;
frontendDebug(tracingTarget, `loaded ${rows.length} replay transaction candidates`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>("#transactionResultSummary").textContent = `Erreur : ${message}`;
frontendError(tracingTarget, `transaction candidate loading failed: ${message}`);
} finally {
setBusy(false, "Prêt");
}
}
async function loadPrograms(): Promise<void> {
if (programTable === null) {
return;
}
try {
setBusy(true, "Chargement programmes");
const request: DemoSqlReplayProgramRequest = {
programIdContains: optionalInputValue("#programContainsInput"),
limit: boundedPositiveIntegerValue("#programLimitInput"),
};
const rows = await invoke<DemoSqlReplayProgramRow[]>("load_demo_sql_replay_programs", { request });
replaceRows(programTable, rows);
element<HTMLElement>("#programResultSummary").textContent = `${rows.length} programme(s) chargé(s), avec occurrences outer, inner et logs liés.`;
frontendDebug(tracingTarget, `loaded ${rows.length} replay program summaries`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>("#programResultSummary").textContent = `Erreur : ${message}`;
frontendError(tracingTarget, `program summary loading failed: ${message}`);
} finally {
setBusy(false, "Prêt");
}
}
async function loadEntity(config: EntityViewConfig): Promise<void> {
const table = entityTable(config.kind);
if (table === null) {
return;
}
try {
setBusy(true, `Chargement ${config.pluralLabel}`);
const request: DemoSqlReplayEntityRequest = {
entityKind: config.kind,
entityValueContains: optionalInputValue(config.containsSelector),
limit: boundedPositiveIntegerValue(config.limitSelector),
};
const rows = await invoke<DemoSqlReplayEntityRow[]>("load_demo_sql_replay_entities", { request });
replaceRows(table, rows);
element<HTMLElement>(config.summarySelector).textContent = `${rows.length} ${config.pluralLabel} chargé(s) depuis PostgreSQL.`;
frontendDebug(tracingTarget, `loaded ${rows.length} replay ${config.kind} summaries`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>(config.summarySelector).textContent = `Erreur : ${message}`;
frontendError(tracingTarget, `${config.kind} summary loading failed: ${message}`);
} finally {
setBusy(false, "Prêt");
}
}
async function copyLines(lines: string[], label: string): Promise<void> {
if (lines.length === 0) {
throw new Error(`Aucune ${label} à copier.`);
}
await navigator.clipboard.writeText(lines.join("\n"));
element<HTMLElement>("#replayCandidateStatusBadge").textContent = `${lines.length} ${label} copiée(s)`;
}
function csvCell(value: unknown): string {
const text = value === null || value === undefined ? "" : String(value);
return `"${text.split('"').join('""')}"`;
}
function csvContent(headers: string[], rows: unknown[][]): string {
const lines = [headers, ...rows].map(row => row.map(csvCell).join(";"));
return `${lines.join("\r\n")}\r\n`;
}
async function saveCsv(fileName: string, content: string, rowCount: number): Promise<void> {
if (rowCount === 0) {
throw new Error("Aucune ligne à exporter.");
}
const path = await invoke<string>("export_demo_sql_replay_csv", { fileName, content });
const badge = element<HTMLElement>("#replayCandidateStatusBadge");
badge.textContent = `${rowCount} ligne(s) exportée(s) : ${path}`;
badge.className = "badge text-bg-success";
frontendDebug(tracingTarget, `CSV export written: ${path}`);
}
async function exportTransactionsCsv(): Promise<void> {
const rows = transactionRowsForExport();
const content = csvContent(
["signature", "slot", "raw_processing_state", "retention_state", "core_status", "ledger_status", "processor_version", "attempt_count", "outer_instruction_count", "inner_instruction_count", "outer_program_count", "inner_program_count", "updated_at"],
rows.map(row => [row.signature, row.slot, row.rawProcessingState, row.retentionState, coreStatusText(row), row.ledgerStatus, row.processorVersion, row.attemptCount, row.outerInstructionCount, row.innerInstructionCount, row.outerProgramCount, row.innerProgramCount, row.updatedAt]),
);
await saveCsv("replay_transactions.csv", content, rows.length);
}
async function exportProgramsCsv(): Promise<void> {
const rows = programRowsForExport();
const content = csvContent(
["program_code", "program_id", "transaction_count", "outer_instruction_count", "inner_instruction_count", "log_count", "min_slot", "max_slot"],
rows.map(row => [row.programCode, row.programId, row.transactionCount, row.outerInstructionCount, row.innerInstructionCount, row.logCount, row.minSlot, row.maxSlot]),
);
await saveCsv("replay_programs.csv", content, rows.length);
}
async function exportEntityCsv(config: EntityViewConfig): Promise<void> {
const rows = entityRowsForExport(config.kind);
const content = csvContent(
["entity_kind", "entity_value", "transaction_count", "occurrence_count", "occurrences_per_transaction", "min_slot", "max_slot", "slot_span"],
rows.map(row => [row.entityKind, row.entityValue, row.transactionCount, row.occurrenceCount, row.transactionCount > 0 ? row.occurrenceCount / row.transactionCount : 0, row.minSlot, row.maxSlot, Math.max(0, row.maxSlot - row.minSlot)]),
);
await saveCsv(config.fileName, content, rows.length);
}
function transactionRowsForExport(): DemoSqlReplayTransactionRow[] {
if (transactionTable === null) {
return [];
}
return selectedOrFilteredRows(transactionTable);
}
function programRowsForExport(): DemoSqlReplayProgramRow[] {
if (programTable === null) {
return [];
}
return selectedOrFilteredRows(programTable);
}
function entityRowsForExport(kind: EntityKindCode): DemoSqlReplayEntityRow[] {
const table = entityTable(kind);
if (table === null) {
return [];
}
return selectedOrFilteredRows(table);
}
function showTransactionsTab(): void {
bootstrap.Tab.getOrCreateInstance(element<HTMLButtonElement>("#transactionsTabButton")).show();
}
async function useSelectedProgram(): Promise<void> {
if (programTable === null) {
return;
}
const row = selectedOrSingleFilteredRow(programTable, "programme");
element<HTMLInputElement>("#transactionProgramIdInput").value = row.programId;
element<HTMLSelectElement>("#transactionProgramScopeSelect").value = "any";
showTransactionsTab();
await loadTransactions();
}
async function useSelectedEntity(config: EntityViewConfig): Promise<void> {
const table = entityTable(config.kind);
if (table === null) {
return;
}
const row = selectedOrSingleFilteredRow(table, config.label);
element<HTMLSelectElement>("#transactionEntityKindSelect").value = config.kind;
element<HTMLInputElement>("#transactionEntityValueInput").value = row.entityValue;
showTransactionsTab();
await loadTransactions();
}
async function openCoreExtraction(): Promise<void> {
try {
await invoke("open_demo_core_extraction_window");
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError(tracingTarget, `core extraction window opening failed: ${message}`);
}
}
async function loadOptions(): Promise<void> {
const options = await invoke<DemoSqlReplayOptionsPayload>("demo_sql_replay_options");
if (!Number.isSafeInteger(options.maximumLimit) || options.maximumLimit <= 0) {
throw new Error("La limite maximale des candidats replay retournée par le backend est invalide.");
}
maximumReplayCandidateLimit = options.maximumLimit;
element<HTMLElement>("#replayCandidateProfileBadge").textContent = `Profil ${options.activeProfileName}`;
element<HTMLElement>("#replayCandidateDsn").textContent = options.maskedDsn;
element<HTMLElement>("#replayCandidateMaximumLimit").textContent = String(options.maximumLimit);
document.querySelectorAll<HTMLInputElement>("input[type=number][max]").forEach(input => {
input.max = String(options.maximumLimit);
});
populateKnownPrograms(options.knownPrograms);
}
function populateKnownPrograms(programs: DemoSqlReplayKnownProgramOption[]): void {
const select = element<HTMLSelectElement>("#knownProgramSelect");
const datalist = element<HTMLDataListElement>("#knownProgramDatalist");
select.replaceChildren(new Option("Tous les programmes connus", ""));
datalist.replaceChildren();
for (const program of programs) {
const label = `${program.code}${program.programId}`;
select.add(new Option(label, program.programId));
const option = document.createElement("option");
option.value = program.programId;
option.label = program.code;
datalist.append(option);
}
}
function initializeTooltips(): void {
document.querySelectorAll<HTMLElement>('[data-bs-toggle="tooltip"]').forEach(target => {
bootstrap.Tooltip.getOrCreateInstance(target);
});
}
function adjustVisibleTables(): void {
transactionTable?.columns.adjust();
programTable?.columns.adjust();
mintTable?.columns.adjust();
ownerTable?.columns.adjust();
accountTable?.columns.adjust();
}
function installInlineCopyHandler(): void {
document.addEventListener("click", event => {
const source = event.target;
if (!(source instanceof Element)) {
return;
}
const button = source.closest<HTMLButtonElement>("button[data-copy-value]");
if (button === null) {
return;
}
event.preventDefault();
event.stopPropagation();
const value = button.dataset.copyValue ?? "";
const label = button.dataset.copyLabel ?? "valeur";
void copyLines([value], label).catch(error => reportUiError(`copy ${label}`, error));
});
}
function reportUiError(action: string, caughtError: unknown): void {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>("#replayCandidateStatusBadge").textContent = `Erreur : ${message}`;
element<HTMLElement>("#replayCandidateStatusBadge").className = "badge text-bg-danger";
frontendError(tracingTarget, `${action} failed: ${message}`);
}
function installHandlers(): void {
element<HTMLButtonElement>("#loadTransactionsButton").addEventListener("click", () => void loadTransactions());
element<HTMLButtonElement>("#resetTransactionsButton").addEventListener("click", () => {
void resetTransactionFilters().catch(error => reportUiError("reset transaction filters", error));
});
element<HTMLButtonElement>("#loadProgramsButton").addEventListener("click", () => void loadPrograms());
element<HTMLButtonElement>("#resetProgramsButton").addEventListener("click", () => {
void resetProgramFilters().catch(error => reportUiError("reset program filters", error));
});
element<HTMLButtonElement>("#openCoreExtractionButton").addEventListener("click", () => void openCoreExtraction());
element<HTMLButtonElement>("#copySignaturesButton").addEventListener("click", () => {
void copyLines(transactionRowsForExport().map(row => row.signature), "signature").catch(error => reportUiError("copy signatures", error));
});
element<HTMLButtonElement>("#exportTransactionsCsvButton").addEventListener("click", () => {
void exportTransactionsCsv().catch(error => reportUiError("export transactions CSV", error));
});
element<HTMLButtonElement>("#copyProgramsButton").addEventListener("click", () => {
void copyLines(programRowsForExport().map(row => row.programId), "program ID").catch(error => reportUiError("copy programs", error));
});
element<HTMLButtonElement>("#exportProgramsCsvButton").addEventListener("click", () => {
void exportProgramsCsv().catch(error => reportUiError("export programs CSV", error));
});
element<HTMLButtonElement>("#useSelectedProgramButton").addEventListener("click", () => {
void useSelectedProgram().catch(error => reportUiError("use selected program", error));
});
for (const config of entityViewConfigs) {
element<HTMLButtonElement>(config.loadButtonSelector).addEventListener("click", () => void loadEntity(config));
element<HTMLButtonElement>(config.resetButtonSelector).addEventListener("click", () => {
void resetEntityFilters(config).catch(error => reportUiError(`reset ${config.pluralLabel} filters`, error));
});
element<HTMLButtonElement>(config.copyButtonSelector).addEventListener("click", () => {
void copyLines(entityRowsForExport(config.kind).map(row => row.entityValue), config.label).catch(error => reportUiError(`copy ${config.pluralLabel}`, error));
});
element<HTMLButtonElement>(config.exportButtonSelector).addEventListener("click", () => {
void exportEntityCsv(config).catch(error => reportUiError(`export ${config.pluralLabel} CSV`, error));
});
element<HTMLButtonElement>(config.useButtonSelector).addEventListener("click", () => {
void useSelectedEntity(config).catch(error => reportUiError(`use selected ${config.label}`, error));
});
}
element<HTMLSelectElement>("#knownProgramSelect").addEventListener("change", event => {
const target = event.currentTarget as HTMLSelectElement;
element<HTMLInputElement>("#programContainsInput").value = target.value;
});
document.querySelectorAll<HTMLButtonElement>('[data-bs-toggle="tab"]').forEach(button => {
button.addEventListener("shown.bs.tab", adjustVisibleTables);
});
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge(tracingTarget);
frontendDebug(tracingTarget, "SQL replay candidate browser loaded");
transactionTable = createTransactionTable();
programTable = createProgramTable();
mintTable = createEntityTable(entityViewConfigs[0]);
ownerTable = createEntityTable(entityViewConfigs[1]);
accountTable = createEntityTable(entityViewConfigs[2]);
initializeTooltips();
installHandlers();
installInlineCopyHandler();
void loadOptions()
.then(() => Promise.all([
loadTransactions(),
loadPrograms(),
loadEntity(entityViewConfigs[0]),
loadEntity(entityViewConfigs[1]),
loadEntity(entityViewConfigs[2]),
]))
.catch(caughtError => reportUiError("initial loading", caughtError));
});

View File

@@ -0,0 +1,68 @@
// file: kb-app-demo-desktop/frontend/ts/demo_sql_tables.ts
// version: 2
import "@andypf/json-viewer";
import type { DemoSqlTableSnapshot } from "./bindings/kb_app_demo_desktop/demo_sql/DemoSqlTableSnapshot";
export function setText(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
export function renderTables(selector: string, tables: DemoSqlTableSnapshot[]): void {
const tbody = document.querySelector<HTMLTableSectionElement>(selector);
if (!tbody) {
return;
}
tbody.textContent = "";
for (const table of tables) {
const row = document.createElement("tr");
row.appendChild(cell(table.tableName, true));
row.appendChild(cell(table.domain, false));
row.appendChild(cell(table.role, false));
row.appendChild(cell(table.exists ? "oui" : "non", false));
row.appendChild(cell(table.rowCount === null ? "—" : String(table.rowCount), false));
row.appendChild(cell(table.minSlot === null ? "—" : String(table.minSlot), false));
row.appendChild(cell(table.maxSlot === null ? "—" : String(table.maxSlot), false));
row.appendChild(cell(table.latestCreatedAt ?? "—", false));
tbody.appendChild(row);
}
}
export function jsonText(value: unknown): string {
return JSON.stringify(value, null, 2);
}
export function renderJsonViewer(selector: string, value: unknown): void {
const container = document.querySelector<HTMLElement>(selector);
if (!container) {
return;
}
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("expand-icon-type", "arrow");
viewer.setAttribute("show-copy", "true");
viewer.setAttribute("show-size", "true");
viewer.setAttribute("expand-empty", "false");
viewer.setAttribute("data", jsonText(value));
container.textContent = "";
container.appendChild(viewer);
}
function cell(value: string, code: boolean): HTMLTableCellElement {
const td = document.createElement("td");
if (code) {
const codeElement = document.createElement("code");
codeElement.textContent = value;
td.appendChild(codeElement);
} else {
td.textContent = value;
}
return td;
}

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/main.ts
// version: 2
// version: 3
import * as bootstrap from "bootstrap";
import MarkdownIt from "markdown-it";
@@ -60,3 +60,19 @@ if (openDemoBackfillLink) {
void invoke("open_demo_backfill_window");
});
}
const sqlDemoLinks: ReadonlyArray<readonly [string, string]> = [
["openDemoSqlDiagLink", "open_demo_sql_diag_window"],
["openDemoSqlPgRawLink", "open_demo_sql_pg_raw_window"],
["openDemoSqlPgCoreLink", "open_demo_sql_pg_core_window"],
["openDemoSqlReplayCandidatesLink", "open_demo_sql_replay_candidates_window"],
];
for (const [elementId, command] of sqlDemoLinks) {
const link = document.querySelector<HTMLAnchorElement>(`#${elementId}`);
if (link) {
link.addEventListener("click", event => {
event.preventDefault();
void invoke(command);
});
}
}