v0.1.0-pre.052

This commit is contained in:
2026-07-25 21:57:01 +02:00
parent 184fe6da88
commit 930056d422
19 changed files with 2782 additions and 121 deletions

View File

@@ -0,0 +1,160 @@
<!-- file: kb_app_demo/frontend/demo_core_extraction.html -->
<!-- version: 4 -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Khadhroony Bot2 - Extraction canonical vers core</title>
<link rel="stylesheet" href="sass/main.scss" />
</head>
<body class="bg-body-tertiary">
<header class="app-header">
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
<div class="container my-0">
<div class="navbar-brand d-flex align-items-center" aria-label="Khadhroony Bot3">
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Extraction core</span>
</a>
<div class="ms-auto d-flex align-items-center gap-2">
<span id="coreExtractionVersionBadge" class="badge text-bg-info">Version —</span>
<span id="coreExtractionStatusBadge" class="badge text-bg-secondary">Initialisation</span>
<button id="openRawDiagnosticsButton" class="btn btn-sm btn-outline-secondary" type="button">Diagnostic raw</button>
<button id="openCoreDiagnosticsButton" class="btn btn-sm btn-outline-secondary" type="button">Diagnostic core</button>
<button id="openReplayCandidatesButton" class="btn btn-sm btn-outline-primary" type="button">Candidats replay</button>
<button id="cancelCoreExtractionButton" class="btn btn-sm btn-outline-danger" type="button" disabled>Arrêter</button>
</div>
</div>
</nav>
</header>
<main class="app-main">
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container py-4">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h1 class="h3 card-title">Extraction transactionnelle canonical vers core</h1>
<p class="text-body-secondary mb-0">Chaque transaction canonique sélectionnée est validée, normalisée dans les tables core et enregistrée dans le ledger de traitement au sein d'une seule transaction PostgreSQL.</p>
</div>
</div>
<div class="accordion shadow-sm" id="coreExtractionAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="coreExtractionSettingsHeading">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#coreExtractionSettingsCollapse" aria-expanded="true" aria-controls="coreExtractionSettingsCollapse">Paramètres communs</button>
</h2>
<div id="coreExtractionSettingsCollapse" class="accordion-collapse collapse show" aria-labelledby="coreExtractionSettingsHeading" data-bs-parent="#coreExtractionAccordion">
<div class="accordion-body">
<div class="row g-3 align-items-end">
<div class="col-6 col-lg-4">
<label class="form-label" for="coreExtractionLimitInput">Transactions maximales</label>
<input id="coreExtractionLimitInput" class="form-control" type="number" min="1" />
</div>
<div class="col-6 col-lg-4">
<label class="form-label" for="coreExtractionConcurrencyInput">Concurrence</label>
<input id="coreExtractionConcurrencyInput" class="form-control" type="number" min="1" />
</div>
<div class="col-12 col-lg-4">
<div class="form-check form-switch mb-2">
<input id="coreExtractionForceReplayInput" class="form-check-input" type="checkbox" role="switch" />
<label class="form-check-label" for="coreExtractionForceReplayInput">Forcer le remplacement des lignes déjà à jour</label>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="coreExtractionSignaturesHeading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#coreExtractionSignaturesCollapse" aria-expanded="false" aria-controls="coreExtractionSignaturesCollapse">Signatures explicites</button>
</h2>
<div id="coreExtractionSignaturesCollapse" class="accordion-collapse collapse" aria-labelledby="coreExtractionSignaturesHeading" data-bs-parent="#coreExtractionAccordion">
<div class="accordion-body">
<label class="form-label" for="coreExtractionSignaturesTextarea">Signatures canoniques déjà stockées, une par ligne</label>
<textarea id="coreExtractionSignaturesTextarea" class="form-control font-monospace mb-3" rows="10" placeholder="5abc...&#10;4def..."></textarea>
<button class="btn btn-primary core-extraction-start-button" type="button" data-core-extraction-mode="signatures">Extraire les signatures</button>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="coreExtractionPendingHeading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#coreExtractionPendingCollapse" aria-expanded="false" aria-controls="coreExtractionPendingCollapse">Transactions en attente</button>
</h2>
<div id="coreExtractionPendingCollapse" class="accordion-collapse collapse" aria-labelledby="coreExtractionPendingHeading" data-bs-parent="#coreExtractionAccordion">
<div class="accordion-body">
<p class="text-body-secondary">Sélectionne les transactions canoniques dont l'état de traitement est <code>received</code>, dans l'ordre des slots.</p>
<button class="btn btn-primary core-extraction-start-button" type="button" data-core-extraction-mode="pending">Extraire les transactions en attente</button>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="coreExtractionSlotsHeading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#coreExtractionSlotsCollapse" aria-expanded="false" aria-controls="coreExtractionSlotsCollapse">Plage de slots</button>
</h2>
<div id="coreExtractionSlotsCollapse" class="accordion-collapse collapse" aria-labelledby="coreExtractionSlotsHeading" data-bs-parent="#coreExtractionAccordion">
<div class="accordion-body">
<div class="row g-3 mb-3">
<div class="col-12 col-md-6">
<label class="form-label" for="coreExtractionMinSlotInput">Slot minimum inclusif</label>
<input id="coreExtractionMinSlotInput" class="form-control" type="number" min="0" />
</div>
<div class="col-12 col-md-6">
<label class="form-label" for="coreExtractionMaxSlotInput">Slot maximum inclusif</label>
<input id="coreExtractionMaxSlotInput" class="form-control" type="number" min="0" />
</div>
</div>
<button class="btn btn-primary core-extraction-start-button" type="button" data-core-extraction-mode="slot_range">Extraire la plage de slots</button>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="coreExtractionProgramHeading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#coreExtractionProgramCollapse" aria-expanded="false" aria-controls="coreExtractionProgramCollapse">Programme déjà indexé dans core</button>
</h2>
<div id="coreExtractionProgramCollapse" class="accordion-collapse collapse" aria-labelledby="coreExtractionProgramHeading" data-bs-parent="#coreExtractionAccordion">
<div class="accordion-body">
<p class="text-body-secondary">Sélectionne les transactions raw ayant déjà au moins une instruction core résolue pour ce programme. Ce mode sert aux replays après changement de version ou de hash.</p>
<label class="form-label" for="coreExtractionProgramIdInput">Program ID</label>
<input id="coreExtractionProgramIdInput" class="form-control font-monospace mb-3" />
<button class="btn btn-primary core-extraction-start-button" type="button" data-core-extraction-mode="program_id">Extraire par programme</button>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-header" id="coreExtractionLogHeading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#coreExtractionLogCollapse" aria-expanded="false" aria-controls="coreExtractionLogCollapse">Journal et résultat</button>
</h2>
<div id="coreExtractionLogCollapse" class="accordion-collapse collapse" aria-labelledby="coreExtractionLogHeading" data-bs-parent="#coreExtractionAccordion">
<div class="accordion-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h3 class="h6 mb-0">Journal</h3><button id="clearCoreExtractionLogButton" class="btn btn-sm btn-outline-secondary" type="button">Effacer</button>
</div>
<textarea id="coreExtractionLogOutput" class="form-control font-monospace mb-3" rows="15" readonly>Aucune campagne exécutée.</textarea>
<h3 class="h6">Résumé JSON</h3>
<textarea id="coreExtractionSummaryOutput" class="form-control font-monospace" rows="16" readonly>Aucun résultat.</textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center">
<div class="row flex-grow-1 align-items-center">
<div class="col-12 text-center text-small my-1 my-md-0">&copy; 2026 SASEDEV</div>
</div>
</div>
</footer>
<script type="module" src="ts/demo_core_extraction.ts" defer></script>
</body>
</html>

View File

@@ -0,0 +1,166 @@
<!-- file: kb_app_demo/frontend/demo_decode_replay.html -->
<!-- version: 4 -->
<!doctype html>
<html lang="fr" data-bs-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Décodage et matérialisation</title>
<link rel="icon" href="imgs/favicon.png" />
<link rel="stylesheet" href="sass/main.scss" />
</head>
<body class="bg-body-tertiary">
<header class="app-header">
<nav class="navbar h-100 py-0 bg-light text-dark">
<div class="container-fluid px-4">
<div class="navbar-brand d-flex align-items-center" aria-label="Khadhroony Bot3"><img alt="Logo Khadhroony" src="imgs/logo.png" class="app-logo" /><span class="ps-2 fs-4 fw-bold text-primary font-logo">Décodage contextualisé</span></div>
<div class="d-flex align-items-center gap-2">
<span id="decodeReplayVersionBadge" class="badge text-bg-secondary">Version</span>
<span id="decodeReplayStatusBadge" class="badge text-bg-success">Prêt</span>
<button id="cancelDecodeReplayButton" class="btn btn-sm btn-outline-danger" type="button" disabled>Arrêter</button>
</div>
</div>
</nav>
</header>
<main class="app-main">
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container-fluid px-4 py-4">
<div class="row g-4">
<div class="col-12 col-xl-7">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h1 class="h4 mb-3">Sélection bornée</h1>
<div class="row g-3">
<div class="col-12 col-lg-6">
<label class="form-label" for="decodeReplayProgramIdInput">Program ID exact</label>
<input id="decodeReplayProgramIdInput" class="form-control font-monospace" placeholder="Vide = tous les programmes des décodeurs sélectionnés" />
</div>
<div class="col-12 col-lg-3">
<label class="form-label" for="decodeReplayStateSelect">État instruction</label>
<select id="decodeReplayStateSelect" class="form-select">
<option value="incomplete_signatures">signatures incomplètes (pending + failed + replay_requested + unsupported)</option>
<option value="actionable">pending + failed + replay_requested</option>
<option value="pending">pending</option>
<option value="failed">failed</option>
<option value="replay_requested">replay_requested</option>
<option value="decoded">decoded</option>
<option value="ignored">ignored</option>
<option value="materialized">materialized</option>
</select>
</div>
<div class="col-6 col-lg-3">
<label class="form-label" for="decodeReplayLimitInput">Limite</label>
<input id="decodeReplayLimitInput" class="form-control" type="number" min="1" />
</div>
<div class="col-12 col-lg-6">
<label class="form-label" for="decodeReplaySignaturesTextarea">Signatures explicites, une par ligne</label>
<textarea id="decodeReplaySignaturesTextarea" class="form-control font-monospace" rows="7" placeholder="Copier depuis demo_sql_replay_candidates"></textarea>
</div>
<div class="col-12 col-lg-6">
<label class="form-label" for="decodeReplayPathsTextarea">Instruction paths exacts, un par ligne</label>
<textarea id="decodeReplayPathsTextarea" class="form-control font-monospace" rows="7" placeholder="0&#10;1/0"></textarea>
</div>
</div>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-body">
<h2 class="h5 mb-3">Dispatch et replay</h2>
<div id="decodeReplayDecoderList" class="mb-3"></div>
<div class="row g-3 align-items-end">
<div class="col-6 col-lg-3">
<label class="form-label" for="decodeReplayConcurrencyInput">Concurrence</label>
<input id="decodeReplayConcurrencyInput" class="form-control" type="number" min="1" />
</div>
<div class="col-12 col-lg-9">
<div class="form-check form-switch">
<input id="decodeReplayAllCompatibleInput" class="form-check-input" type="checkbox" role="switch" />
<label class="form-check-label" for="decodeReplayAllCompatibleInput">Exécuter tous les décodeurs compatibles</label>
</div>
<div class="form-check form-switch">
<input id="decodeReplayForceInput" class="form-check-input" type="checkbox" role="switch" disabled />
<label class="form-check-label" for="decodeReplayForceInput">Force replay du processor/version/input</label>
</div>
<div class="form-check form-switch">
<input id="decodeReplayAllSignaturesInput" class="form-check-input" type="checkbox" role="switch" />
<label class="form-check-label" for="decodeReplayAllSignaturesInput">Toutes les signatures correspondant aux filtres, dans la limite indiquée</label>
</div>
<div id="decodeReplayForceScopeHelp" class="form-text mb-2">Sans signatures explicites, cocher Toutes les signatures pour autoriser un force replay borné. Le filtre détat est alors ignoré, mais les programmes, paths et la limite restent appliqués.</div>
<div class="form-check form-switch">
<input id="decodeReplayMaterializeInput" class="form-check-input" type="checkbox" role="switch" disabled />
<label class="form-check-label" for="decodeReplayMaterializeInput">Appeler les matérialiseurs disponibles après persistance du décodage</label>
</div>
<div id="decodeReplayMaterializeHelp" class="form-text">Chargement de la disponibilité des matérialiseurs…</div>
</div>
<div class="col-12">
<button id="startDecodeReplayButton" class="btn btn-primary" type="button">Lancer le replay de décodage</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-5">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<div class="d-flex flex-wrap gap-2 mb-3">
<button id="openReplayCandidatesFromDecodeButton" class="btn btn-sm btn-outline-primary" type="button">Candidats replay</button>
<button id="openCoreDiagnosticsFromDecodeButton" class="btn btn-sm btn-outline-secondary" type="button">Diagnostic core</button>
<button id="loadDecodeDiagnosticsButton" class="btn btn-sm btn-outline-secondary" type="button">Decode / ledger / couverture</button>
</div>
<h2 class="h5">Journal</h2>
<textarea id="decodeReplayLogOutput" class="form-control font-monospace mb-3" rows="14" readonly>Aucune campagne exécutée.</textarea>
<h2 class="h5">Résumé</h2>
<textarea id="decodeReplaySummaryOutput" class="form-control font-monospace mb-3" rows="15" readonly>Aucun résultat.</textarea>
<h2 class="h5">Diagnostics</h2>
<textarea id="decodeReplayDiagnosticsOutput" class="form-control font-monospace" rows="18" readonly>Diagnostics non chargés.</textarea>
</div>
</div>
</div>
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex flex-wrap align-items-end gap-3 mb-3">
<div class="flex-grow-1">
<label class="form-label" for="annotationSignatureInput">Journal des annotations — signature contient</label>
<input id="annotationSignatureInput" class="form-control font-monospace" placeholder="Filtre facultatif" />
</div>
<div>
<label class="form-label" for="annotationLimitInput">Limite</label>
<input id="annotationLimitInput" class="form-control" type="number" min="1" max="500" value="100" />
</div>
<button id="loadAnnotationsButton" class="btn btn-outline-primary" type="button">Charger les annotations</button>
</div>
<p id="annotationJournalStatus" class="text-body-secondary mb-3">Annotations non chargées.</p>
<div class="table-responsive">
<table class="table table-sm table-hover align-middle mb-0">
<thead>
<tr>
<th>Slot</th>
<th>Signature</th>
<th>Path</th>
<th>Génération</th>
<th>Texte</th>
<th>Octets</th>
<th>Signataires vérifiés</th>
<th>SHA-256 / provenance</th>
</tr>
</thead>
<tbody id="annotationJournalBody"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center justify-content-center">&copy; 2026 SASEDEV</div>
</footer>
<script type="module" src="ts/demo_decode_replay.ts" defer></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
<!-- file: kb-app-demo-desktop/frontend/main.html -->
<!-- version: 5 -->
<!-- version: 6 -->
<!DOCTYPE html>
<html lang="fr">
<head>
@@ -28,6 +28,9 @@
<li><a id="openDemoHttpLink" class="dropdown-item" href="#">HTTP JSON-RPC</a></li>
<li><a id="openDemoBackfillLink" class="dropdown-item" href="#">Backfill HTTP</a></li>
<li><hr class="dropdown-divider"></li>
<li><a id="openDemoCoreExtractionLink" class="dropdown-item" href="#">Extraction core</a></li>
<li><a id="openDemoDecodeReplayLink" class="dropdown-item" href="#">Décodage contextualisé</a></li>
<li><hr class="dropdown-divider"></li>
<li><a id="openDemoSqlDiagLink" class="dropdown-item" href="#">SQL diagnostics</a></li>
<li><a id="openDemoSqlPgRawLink" class="dropdown-item" href="#">SQL PostgreSQL raw</a></li>
<li><a id="openDemoSqlPgCoreLink" class="dropdown-item" href="#">SQL PostgreSQL core</a></li>

View File

@@ -0,0 +1,185 @@
// file: kb-app-demo-desktop/frontend/ts/demo_core_extraction.ts
// version: 4
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.ts";
import type { DemoCoreExtractionOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_core_extraction/DemoCoreExtractionOptionsPayload.ts";
import type { DemoCoreExtractionProgressPayload } from "./bindings/kb_app_demo_desktop/demo_core_extraction/DemoCoreExtractionProgressPayload.ts";
import type { DemoCoreExtractionRequest } from "./bindings/kb_app_demo_desktop/demo_core_extraction/DemoCoreExtractionRequest.ts";
import type { DemoCoreExtractionSummaryPayload } from "./bindings/kb_app_demo_desktop/demo_core_extraction/DemoCoreExtractionSummaryPayload.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 | 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;
}
function nullableIntegerValue(selector: string): number | null {
const value = inputValue(selector);
if (value.length === 0) {
return null;
}
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed)) {
throw new Error(`Valeur numérique invalide pour ${selector}`);
}
return parsed;
}
function appendLog(payload: DemoCoreExtractionProgressPayload | { timestamp: string; level: string; message: string }): void {
const progress = "completed" in payload ? ` [${payload.completed}/${payload.total}]` : "";
logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()}${progress} ${payload.message}`);
while (logLines.length > maximumLogLines) {
logLines.shift();
}
const output = element<HTMLTextAreaElement>("#coreExtractionLogOutput");
output.value = logLines.join("\n");
output.scrollTop = output.scrollHeight;
}
function setRunning(value: boolean): void {
running = value;
document.querySelectorAll<HTMLButtonElement>(".core-extraction-start-button").forEach(button => {
button.disabled = value;
});
element<HTMLButtonElement>("#cancelCoreExtractionButton").disabled = !value;
const badge = element<HTMLElement>("#coreExtractionStatusBadge");
badge.textContent = value ? "Extraction en cours" : "Prêt";
badge.className = value ? "badge text-bg-warning" : "badge text-bg-success";
}
function buildRequest(mode: string): DemoCoreExtractionRequest {
return {
mode,
signaturesText: mode === "signatures" ? element<HTMLTextAreaElement>("#coreExtractionSignaturesTextarea").value : null,
programId: mode === "program_id" ? inputValue("#coreExtractionProgramIdInput") : null,
minSlot: mode === "slot_range" ? nullableIntegerValue("#coreExtractionMinSlotInput") : null,
maxSlot: mode === "slot_range" ? nullableIntegerValue("#coreExtractionMaxSlotInput") : null,
limit: integerValue("#coreExtractionLimitInput"),
maxConcurrentExtractions: integerValue("#coreExtractionConcurrencyInput"),
forceReplay: element<HTMLInputElement>("#coreExtractionForceReplayInput").checked,
};
}
async function executeCoreExtraction(mode: string): Promise<void> {
if (running) {
return;
}
try {
const request = buildRequest(mode);
setRunning(true);
element<HTMLTextAreaElement>("#coreExtractionSummaryOutput").value = "Exécution en cours...";
appendLog({ timestamp: new Date().toISOString(), level: "info", message: `Démarrage du mode ${mode}` });
const summary = await invoke<DemoCoreExtractionSummaryPayload>("demo_core_extraction_execute", { request });
element<HTMLTextAreaElement>("#coreExtractionSummaryOutput").value = JSON.stringify(summary, null, 2);
appendLog({
timestamp: new Date().toISOString(),
level: summary.cancelled || Number(summary.failed) > 0 ? "warn" : "info",
message: `Campagne terminée : extracted=${summary.extracted}, skipped=${summary.skipped}, failed=${summary.failed}, cancelled=${summary.cancelledCandidates}, notStarted=${summary.notStarted}`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#coreExtractionSummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", message });
frontendError("kb_app_demo_desktop.frontend.demo_core_extraction", `Core extraction failed: ${message}`);
} finally {
setRunning(false);
}
}
async function cancelCoreExtraction(): Promise<void> {
try {
const accepted = await invoke<boolean>("demo_core_extraction_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.demo_core_extraction", `Cancellation failed: ${message}`);
}
}
async function openDiagnostics(command: "open_demo_sql_pg_raw_window" | "open_demo_sql_pg_core_window" | "open_demo_sql_replay_candidates_window", label: string): Promise<void> {
try {
await invoke(command);
appendLog({ timestamp: new Date().toISOString(), level: "info", message: `${label} ouvert.` });
} 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.demo_core_extraction", `${label} opening failed: ${message}`);
}
}
async function loadOptions(): Promise<void> {
const options = await invoke<DemoCoreExtractionOptionsPayload>("demo_core_extraction_options");
element<HTMLElement>("#coreExtractionVersionBadge").textContent = `Version ${options.processorVersion}`;
element<HTMLInputElement>("#coreExtractionLimitInput").value = String(options.defaultLimit);
element<HTMLInputElement>("#coreExtractionConcurrencyInput").value = String(options.defaultMaxConcurrentExtractions);
setRunning(options.running);
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_core_extraction");
frontendDebug("kb_app_demo_desktop.frontend.demo_core_extraction", "core extraction demo window loaded");
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item));
document.querySelectorAll<HTMLButtonElement>(".core-extraction-start-button").forEach(button => {
button.addEventListener("click", () => {
const mode = button.dataset.coreExtractionMode;
if (mode) {
void executeCoreExtraction(mode);
}
});
});
element<HTMLButtonElement>("#openRawDiagnosticsButton").addEventListener("click", () => {
void openDiagnostics("open_demo_sql_pg_raw_window", "Diagnostic raw");
});
element<HTMLButtonElement>("#openCoreDiagnosticsButton").addEventListener("click", () => {
void openDiagnostics("open_demo_sql_pg_core_window", "Diagnostic core");
});
element<HTMLButtonElement>("#openReplayCandidatesButton").addEventListener("click", () => {
void openDiagnostics("open_demo_sql_replay_candidates_window", "Sélecteur de candidats replay");
});
element<HTMLButtonElement>("#cancelCoreExtractionButton").addEventListener("click", () => {
void cancelCoreExtraction();
});
element<HTMLButtonElement>("#clearCoreExtractionLogButton").addEventListener("click", () => {
logLines.length = 0;
element<HTMLTextAreaElement>("#coreExtractionLogOutput").value = "";
});
void listen<DemoCoreExtractionProgressPayload>("demo-core-extraction-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.demo_core_extraction", `Options loading failed: ${message}`);
});
});

View File

@@ -0,0 +1,340 @@
// file: kb-app-demo-desktop/frontend/ts/demo_decode_replay.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.ts";
import type { DemoDecodeDiagnosticsPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeDiagnosticsPayload.ts";
import type { DemoDecodeReplayOptionsPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplayOptionsPayload.ts";
import type { DemoDecodeReplayProgressPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplayProgressPayload.ts";
import type { DemoDecodeReplayRequest } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplayRequest.ts";
import type { DemoDecodeReplaySummaryPayload } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoDecodeReplaySummaryPayload.ts";
import type { DemoTransactionAnnotationRequest } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoTransactionAnnotationRequest.ts";
import type { DemoTransactionAnnotationRow } from "./bindings/kb_app_demo_desktop/demo_decode_replay/DemoTransactionAnnotationRow.ts";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
const logLines: string[] = [];
let running = false;
let materializationAvailable = 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 | HTMLTextAreaElement | HTMLSelectElement>(selector).value.trim();
}
function integerValue(selector: string): number {
const value = Number.parseInt(inputValue(selector), 10);
if (!Number.isFinite(value) || value < 1) {
throw new Error(`Valeur numérique invalide pour ${selector}`);
}
return value;
}
function appendLog(payload: DemoDecodeReplayProgressPayload | { timestamp: string; level: string; message: string }): void {
const progress = "completed" in payload ? ` [${payload.completed}/${payload.total}]` : "";
const campaign = "campaignId" in payload ? ` [${payload.campaignId}]` : "";
logLines.push(`${payload.timestamp} ${payload.level.toUpperCase()}${campaign}${progress} ${payload.message}`);
while (logLines.length > 1000) {
logLines.shift();
}
const output = element<HTMLTextAreaElement>("#decodeReplayLogOutput");
output.value = logLines.join("\n");
output.scrollTop = output.scrollHeight;
}
function explicitSignatureCount(): number {
return element<HTMLTextAreaElement>("#decodeReplaySignaturesTextarea")
.value
.split(/\r?\n/)
.filter(value => value.trim().length > 0)
.length;
}
function updateReplayScopeControls(): void {
const signaturesPresent = explicitSignatureCount() > 0;
const incompleteSignatures = inputValue("#decodeReplayStateSelect") === "incomplete_signatures";
const allMatchingInput = element<HTMLInputElement>("#decodeReplayAllSignaturesInput");
const forceInput = element<HTMLInputElement>("#decodeReplayForceInput");
const materializeInput = element<HTMLInputElement>("#decodeReplayMaterializeInput");
if (!running && (signaturesPresent || incompleteSignatures) && allMatchingInput.checked) {
allMatchingInput.checked = false;
}
allMatchingInput.disabled = running || signaturesPresent || incompleteSignatures;
const forceScopeMissing = !signaturesPresent && !allMatchingInput.checked;
forceInput.disabled = running || forceScopeMissing || incompleteSignatures;
if (!running && (forceScopeMissing || incompleteSignatures)) {
forceInput.checked = false;
}
materializeInput.disabled = running || !materializationAvailable;
if (!running && !materializationAvailable) {
materializeInput.checked = false;
}
}
function setRunning(value: boolean): void {
running = value;
element<HTMLButtonElement>("#startDecodeReplayButton").disabled = value;
element<HTMLButtonElement>("#cancelDecodeReplayButton").disabled = !value;
const badge = element<HTMLElement>("#decodeReplayStatusBadge");
badge.textContent = value ? "Replay en cours" : "Prêt";
badge.className = value ? "badge text-bg-warning" : "badge text-bg-success";
updateReplayScopeControls();
}
function selectedDecoderNames(): string[] {
return Array.from(document.querySelectorAll<HTMLInputElement>(".decode-replay-decoder-input:checked"))
.map(input => input.value);
}
function buildRequest(): DemoDecodeReplayRequest {
return {
signaturesText: element<HTMLTextAreaElement>("#decodeReplaySignaturesTextarea").value || null,
programId: inputValue("#decodeReplayProgramIdInput") || null,
instructionState: inputValue("#decodeReplayStateSelect"),
instructionPathsText: element<HTMLTextAreaElement>("#decodeReplayPathsTextarea").value || null,
decoderNames: selectedDecoderNames(),
limit: integerValue("#decodeReplayLimitInput"),
maxConcurrentInputs: integerValue("#decodeReplayConcurrencyInput"),
allCompatible: element<HTMLInputElement>("#decodeReplayAllCompatibleInput").checked,
forceReplay: element<HTMLInputElement>("#decodeReplayForceInput").checked,
forceReplayAllMatching: element<HTMLInputElement>("#decodeReplayAllSignaturesInput").checked,
materializeAfterDecode: element<HTMLInputElement>("#decodeReplayMaterializeInput").checked,
};
}
function validationMessage(request: DemoDecodeReplayRequest): string | null {
const signatureCount = request.signaturesText
? request.signaturesText.split(/\r?\n/).filter(value => value.trim().length > 0).length
: 0;
if (request.decoderNames.length === 0) {
return "Sélectionner au moins un décodeur.";
}
if (request.instructionState === "incomplete_signatures" && request.forceReplay) {
return "Le mode signatures incomplètes rejoue son périmètre sans Force replay global.";
}
if (request.forceReplay && signatureCount === 0 && !request.forceReplayAllMatching) {
return "Le force replay nécessite des signatures explicites ou lautorisation Toutes les signatures.";
}
if (request.forceReplayAllMatching && signatureCount > 0) {
return "Le mode « Toutes les signatures » ne peut pas être combiné avec une liste de signatures explicites.";
}
if (request.forceReplayAllMatching && !request.forceReplay) {
return "Le mode « Toutes les signatures » est uniquement disponible avec Force replay.";
}
if (request.materializeAfterDecode && !materializationAvailable) {
return "Aucun matérialiseur nest actuellement enregistré dans cette application.";
}
return null;
}
function reportValidationWarning(message: string): void {
element<HTMLTextAreaElement>("#decodeReplaySummaryOutput").value = JSON.stringify({ validation: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "warn", message });
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Decode replay request rejected locally: ${message}`);
}
async function executeDecodeReplay(): Promise<void> {
if (running) {
return;
}
try {
const request = buildRequest();
const invalid = validationMessage(request);
if (invalid) {
reportValidationWarning(invalid);
return;
}
setRunning(true);
element<HTMLTextAreaElement>("#decodeReplaySummaryOutput").value = "Exécution en cours...";
const signatureCount = request.signaturesText
? request.signaturesText.split(/\r?\n/).filter(value => value.trim().length > 0).length
: 0;
const requestSummary = `signatures=${signatureCount}, programId=${request.programId ?? "auto"}, state=${request.instructionState}, decoders=${request.decoderNames.join(",")}, limit=${request.limit}, concurrency=${request.maxConcurrentInputs}, allCompatible=${request.allCompatible}, forceReplay=${request.forceReplay}, allSignatures=${request.forceReplayAllMatching}, materialize=${request.materializeAfterDecode}`;
appendLog({ timestamp: new Date().toISOString(), level: "info", message: `Démarrage du replay de décodage contextualisé : ${requestSummary}` });
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_execute ${requestSummary}`);
const summary = await invoke<DemoDecodeReplaySummaryPayload>("demo_decode_replay_execute", { request });
element<HTMLTextAreaElement>("#decodeReplaySummaryOutput").value = JSON.stringify(summary, null, 2);
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_execute completed summary=${JSON.stringify(summary)}`);
appendLog({
timestamp: new Date().toISOString(),
level: summary.cancelled || summary.failedInputs > 0 ? "warn" : "info",
message: `Campagne ${summary.campaignId} terminée : completed=${summary.completed}, unmatched=${summary.unmatched}, failed=${summary.failedInputs}, notStarted=${summary.notStarted}`,
});
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#decodeReplaySummaryOutput").value = JSON.stringify({ error: message }, null, 2);
appendLog({ timestamp: new Date().toISOString(), level: "error", message });
frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Decode replay failed: ${message}`);
} finally {
setRunning(false);
}
}
async function cancelDecodeReplay(): Promise<void> {
try {
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_cancel");
const accepted = await invoke<boolean>("demo_decode_replay_cancel");
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_cancel completed accepted=${accepted}`);
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.demo_decode_replay", `Cancellation failed: ${message}`);
}
}
async function loadDiagnostics(): Promise<void> {
try {
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_diagnostics");
const diagnostics = await invoke<DemoDecodeDiagnosticsPayload>("demo_decode_replay_diagnostics");
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_diagnostics completed tables=${diagnostics.tables.length} coverage=${diagnostics.coverage.length}`);
element<HTMLTextAreaElement>("#decodeReplayDiagnosticsOutput").value = JSON.stringify(diagnostics, null, 2);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLTextAreaElement>("#decodeReplayDiagnosticsOutput").value = JSON.stringify({ error: message }, null, 2);
frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Diagnostics loading failed: ${message}`);
}
}
function tableCell(value: string, title?: string): HTMLTableCellElement {
const cell = document.createElement("td");
cell.textContent = value;
if (title) {
cell.title = title;
}
return cell;
}
function shortened(value: string, visible: number): string {
return value.length > visible ? `${value.slice(0, visible)}` : value;
}
function renderAnnotations(rows: DemoTransactionAnnotationRow[]): void {
const body = element<HTMLTableSectionElement>("#annotationJournalBody");
body.replaceChildren();
for (const row of rows) {
const tableRow = document.createElement("tr");
tableRow.append(
tableCell(row.slot),
tableCell(shortened(row.signature, 18), row.signature),
tableCell(row.instructionPath),
tableCell(row.generation, row.programId),
tableCell(row.text),
tableCell(String(row.payloadLengthBytes)),
tableCell(row.verifiedSigners.join(", ") || "—"),
tableCell(`${shortened(row.payloadSha256, 12)} · ${row.decoder}`, `${row.payloadSha256}\n${row.idempotenceKey}\ntransaction_annotations@${row.processorVersion}`),
);
body.append(tableRow);
}
element<HTMLElement>("#annotationJournalStatus").textContent = `${rows.length} annotation(s) committed chargée(s).`;
}
async function loadAnnotations(): Promise<void> {
const request: DemoTransactionAnnotationRequest = {
signatureContains: inputValue("#annotationSignatureInput") || null,
limit: integerValue("#annotationLimitInput"),
};
try {
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations limit=${request.limit}`);
const rows = await invoke<DemoTransactionAnnotationRow[]>("demo_decode_replay_annotations", { request });
renderAnnotations(rows);
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_annotations completed rows=${rows.length}`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
element<HTMLElement>("#annotationJournalStatus").textContent = `Erreur : ${message}`;
frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Annotation journal loading failed: ${message}`);
}
}
async function openWindow(command: "open_demo_sql_pg_core_window" | "open_demo_sql_replay_candidates_window"): Promise<void> {
try {
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke ${command}`);
await invoke(command);
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke ${command} completed`);
} catch (caughtError) {
const message = caughtError instanceof Error ? caughtError.message : String(caughtError);
frontendError("kb_app_demo_desktop.frontend.demo_decode_replay", `Window opening failed: ${message}`);
}
}
async function loadOptions(): Promise<void> {
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "Invoke demo_decode_replay_options");
const options = await invoke<DemoDecodeReplayOptionsPayload>("demo_decode_replay_options");
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", `Invoke demo_decode_replay_options completed options=${JSON.stringify(options)}`);
element<HTMLElement>("#decodeReplayVersionBadge").textContent = `Pipeline ${options.pipelineVersion}`;
element<HTMLInputElement>("#decodeReplayLimitInput").value = String(options.defaultLimit);
element<HTMLInputElement>("#decodeReplayConcurrencyInput").value = String(options.defaultMaxConcurrentInputs);
const container = element<HTMLElement>("#decodeReplayDecoderList");
container.innerHTML = "";
for (const decoder of options.decoders) {
const wrapper = document.createElement("div");
wrapper.className = "form-check border rounded p-3 ps-5 mb-2";
const input = document.createElement("input");
input.className = "form-check-input decode-replay-decoder-input";
input.type = "checkbox";
input.value = decoder.name;
input.id = `decode-${decoder.name}`;
input.checked = true;
const label = document.createElement("label");
label.className = "form-check-label";
label.htmlFor = input.id;
label.textContent = `${decoder.name} v${decoder.version}${decoder.programIds.length} programmes`;
wrapper.append(input, label);
container.append(wrapper);
}
materializationAvailable = options.materializerNames.length > 0;
const materializeHelp = element<HTMLElement>("#decodeReplayMaterializeHelp");
materializeHelp.textContent = materializationAvailable
? `Matérialiseurs disponibles : ${options.materializerNames.join(", ")}`
: "Aucun matérialiseur enregistré : option désactivée.";
setRunning(options.running);
}
document.addEventListener("DOMContentLoaded", () => {
installFrontendConsoleBridge("kb_app_demo_desktop.frontend.demo_decode_replay");
frontendDebug("kb_app_demo_desktop.frontend.demo_decode_replay", "decode replay demo window loaded");
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(item => new bootstrap.Tooltip(item));
element<HTMLTextAreaElement>("#decodeReplaySignaturesTextarea").addEventListener("input", () => updateReplayScopeControls());
element<HTMLSelectElement>("#decodeReplayStateSelect").addEventListener("change", () => updateReplayScopeControls());
element<HTMLInputElement>("#decodeReplayAllSignaturesInput").addEventListener("change", event => {
const input = event.currentTarget as HTMLInputElement;
if (input.checked) {
element<HTMLInputElement>("#decodeReplayForceInput").checked = true;
}
updateReplayScopeControls();
});
element<HTMLInputElement>("#decodeReplayForceInput").addEventListener("change", event => {
const input = event.currentTarget as HTMLInputElement;
if (!input.checked) {
element<HTMLInputElement>("#decodeReplayAllSignaturesInput").checked = false;
}
updateReplayScopeControls();
});
element<HTMLButtonElement>("#startDecodeReplayButton").addEventListener("click", () => void executeDecodeReplay());
element<HTMLButtonElement>("#cancelDecodeReplayButton").addEventListener("click", () => void cancelDecodeReplay());
element<HTMLButtonElement>("#loadDecodeDiagnosticsButton").addEventListener("click", () => void loadDiagnostics());
element<HTMLButtonElement>("#loadAnnotationsButton").addEventListener("click", () => void loadAnnotations());
element<HTMLButtonElement>("#openReplayCandidatesFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_sql_replay_candidates_window"));
element<HTMLButtonElement>("#openCoreDiagnosticsFromDecodeButton").addEventListener("click", () => void openWindow("open_demo_sql_pg_core_window"));
void listen<DemoDecodeReplayProgressPayload>("demo-decode-replay-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.demo_decode_replay", `Options loading failed: ${message}`);
});
});

View File

@@ -1,5 +1,5 @@
// file: kb-app-demo-desktop/frontend/ts/main.ts
// version: 3
// version: 4
import * as bootstrap from "bootstrap";
import MarkdownIt from "markdown-it";
@@ -75,6 +75,20 @@ if (openDemoBackfillLink) {
});
}
const pipelineDemoLinks: ReadonlyArray<readonly [string, string]> = [
["openDemoCoreExtractionLink", "open_demo_core_extraction_window"],
["openDemoDecodeReplayLink", "open_demo_decode_replay_window"],
];
for (const [elementId, command] of pipelineDemoLinks) {
const link = document.querySelector<HTMLAnchorElement>(`#${elementId}`);
if (link) {
link.addEventListener("click", event => {
event.preventDefault();
void invoke(command);
});
}
}
const sqlDemoLinks: ReadonlyArray<readonly [string, string]> = [
["openDemoSqlDiagLink", "open_demo_sql_diag_window"],
["openDemoSqlPgRawLink", "open_demo_sql_pg_raw_window"],