v0.1.0-pre.052
This commit is contained in:
160
kb-app-demo-desktop/frontend/demo_core_extraction.html
Normal file
160
kb-app-demo-desktop/frontend/demo_core_extraction.html
Normal 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... 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">© 2026 SASEDEV</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="ts/demo_core_extraction.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
166
kb-app-demo-desktop/frontend/demo_decode_replay.html
Normal file
166
kb-app-demo-desktop/frontend/demo_decode_replay.html
Normal 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 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">© 2026 SASEDEV</div>
|
||||
</footer>
|
||||
<script type="module" src="ts/demo_decode_replay.ts" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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>
|
||||
|
||||
185
kb-app-demo-desktop/frontend/ts/demo_core_extraction.ts
Normal file
185
kb-app-demo-desktop/frontend/ts/demo_core_extraction.ts
Normal 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}`);
|
||||
});
|
||||
});
|
||||
340
kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts
Normal file
340
kb-app-demo-desktop/frontend/ts/demo_decode_replay.ts
Normal 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 l’autorisation 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 n’est 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}`);
|
||||
});
|
||||
});
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user