This commit is contained in:
2026-04-25 18:10:40 +02:00
parent f90ca40202
commit b034fdf1c4
16 changed files with 1088 additions and 52 deletions

View File

@@ -0,0 +1,109 @@
<!-- file: kb_app/frontend/demo_ws_manager.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Khadhroony-BoBoBot — Demo Ws Manager</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">
<a class="navbar-brand d-flex align-items-center" href="/">
<img alt="Logo" src="imgs/logo.png" class="app-logo" />
<span class="ps-2 fs-4 fw-bold text-primary font-logo">Demo Ws Manager</span>
</a>
</div>
</nav>
</header>
<main class="app-main">
<div class="osb-scrollable pt-1 pb-4" data-simplebar>
<div class="container vcentered sketchy-translucid py-4">
<div class="row g-4">
<div class="col-12 col-xxl-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h1 class="h4 mb-3">Pilotage</h1>
<p class="text-body-secondary mb-3">
Démo légère du <code>WsManager</code> : démarrage/arrêt groupé, pilotage par rôle et bus unifié dévénements.
</p>
<div class="d-flex flex-wrap gap-2 mb-4">
<button id="demoWsManagerStartAllButton" type="button" class="btn btn-primary">Start all</button>
<button id="demoWsManagerStopAllButton" type="button" class="btn btn-outline-primary">Stop all</button>
<button id="demoWsManagerRefreshButton" type="button" class="btn btn-outline-secondary">Refresh snapshot</button>
</div>
<div class="mb-3">
<label for="demoWsManagerRoleSelect" class="form-label">Rôle</label>
<select id="demoWsManagerRoleSelect" class="form-select"></select>
</div>
<div class="d-flex flex-wrap gap-2 mb-4">
<button id="demoWsManagerStartRoleButton" type="button" class="btn btn-primary">Start role</button>
<button id="demoWsManagerStopRoleButton" type="button" class="btn btn-outline-primary">Stop role</button>
</div>
<div class="small text-body-secondary">
<div><strong>Managed endpoints:</strong> <span id="demoWsManagerEndpointCountText">0</span></div>
<div><strong>Started endpoints:</strong> <span id="demoWsManagerStartedCountText">0</span></div>
</div>
</div>
</div>
</div>
<div class="col-12 col-xxl-8">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h2 class="h5 mb-3">Snapshot</h2>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead>
<tr>
<th>Endpoint</th>
<th>Provider</th>
<th>Roles</th>
<th>State</th>
<th>Subs.</th>
</tr>
</thead>
<tbody id="demoWsManagerTableBody"></tbody>
</table>
</div>
</div>
</div>
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0">Unified event log</h2>
<button id="demoWsManagerClearLogButton" type="button" class="btn btn-outline-secondary btn-sm">Clear log</button>
</div>
<textarea id="demoWsManagerLogTextarea" class="form-control font-monospace" rows="18" readonly spellcheck="false"></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 col-md-6 text-center text-small my-1 my-md-0">
&copy; 2026 SASEDEV — Demo Ws Manager
</div>
</div>
</div>
</footer>
<script type="module" src="ts/demo_ws_manager.ts" defer></script>
</body>
</html>

View File

@@ -25,6 +25,9 @@
<button id="openDemoHttpButton" class="btn btn-outline-primary">
Demo Http
</button>
<button id="openDemoWsManagerButton" type="button" class="btn btn-outline-primary">
Demo Ws Manager
</button>
</div>
</div>
</nav>
@@ -54,6 +57,10 @@
Les tests PRC Http manuels sont disponibles dans la fenêtre dédiée
<strong>Demo Http</strong>.
</p>
<p class="text-body-secondary mb-3">
La démonstration légère de pilotage multi-clients est disponible dans la fenêtre
<strong>Demo Ws Manager</strong>.
</p>
<div class="d-flex flex-wrap gap-2">
<button id="openDemoWsButtonSecondary" type="button" class="btn btn-primary">
@@ -62,6 +69,9 @@
<button id="openDemoHttpButtonSecondary" type="button" class="btn btn-primary">
Ouvrir Demo Http
</button>
<button id="openDemoWsManagerButtonSecondary" type="button" class="btn btn-primary">
Ouvrir Demo Ws Manager
</button>
</div>
<hr />

View File

@@ -268,6 +268,8 @@ async function refreshPoolSnapshot(
document.addEventListener("DOMContentLoaded", async () => {
void takeoverConsole();
debug("demo_http window loaded");
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
if (sidebarToggle) {
// restaurer létat depuis localStorage
@@ -432,6 +434,4 @@ document.addEventListener("DOMContentLoaded", async () => {
appendLogLine(logTextarea, "[ui] demo_http window loaded");
await refreshPoolSnapshot(poolTableBody, logTextarea, true);
debug("demo_http window loaded");
});

View File

@@ -240,6 +240,9 @@ function applyStatusToUi(
document.addEventListener("DOMContentLoaded", async () => {
void takeoverConsole();
debug("demo_ws window loaded");
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
if (sidebarToggle) {
// restaurer létat depuis localStorage
@@ -514,6 +517,4 @@ document.addEventListener("DOMContentLoaded", async () => {
unlistenStatusEvent();
}
});
debug("demo_ws window loaded");
});

View File

@@ -0,0 +1,237 @@
// file: kb_app/frontend/ts/demo_ws_manager.ts
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 { debug, takeoverConsole } from "@fltsci/tauri-plugin-tracing";
(window as Window & typeof globalThis & { bootstrap?: typeof bootstrap }).bootstrap = bootstrap;
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
type DemoWsManagerEndpointSummary = {
name: string;
resolvedUrl: string;
provider: string;
roles: string[];
connectionState: string;
activeSubscriptionCount: number;
};
type DemoWsManagerSnapshotPayload = {
endpointCount: number;
startedCount: number;
endpoints: DemoWsManagerEndpointSummary[];
};
const endpointCountText = document.querySelector<HTMLSpanElement>("#demoWsManagerEndpointCountText");
const startedCountText = document.querySelector<HTMLSpanElement>("#demoWsManagerStartedCountText");
const roleSelect = document.querySelector<HTMLSelectElement>("#demoWsManagerRoleSelect");
const tableBody = document.querySelector<HTMLTableSectionElement>("#demoWsManagerTableBody");
const logTextarea = document.querySelector<HTMLTextAreaElement>("#demoWsManagerLogTextarea");
const startAllButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStartAllButton");
const stopAllButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStopAllButton");
const refreshButton = document.querySelector<HTMLButtonElement>("#demoWsManagerRefreshButton");
const startRoleButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStartRoleButton");
const stopRoleButton = document.querySelector<HTMLButtonElement>("#demoWsManagerStopRoleButton");
const clearLogButton = document.querySelector<HTMLButtonElement>("#demoWsManagerClearLogButton");
function appendLogLine(line: string): void {
if (!logTextarea) {
return;
}
const prefix = logTextarea.value.length > 0 ? "\n" : "";
logTextarea.value += `${prefix}${line}`;
logTextarea.scrollTop = logTextarea.scrollHeight;
}
function renderSnapshot(snapshot: DemoWsManagerSnapshotPayload): void {
if (endpointCountText) {
endpointCountText.textContent = String(snapshot.endpointCount);
}
if (startedCountText) {
startedCountText.textContent = String(snapshot.startedCount);
}
if (!tableBody) {
return;
}
tableBody.innerHTML = "";
for (const endpoint of snapshot.endpoints) {
const row = document.createElement("tr");
row.innerHTML = `
<td>
<div class="fw-semibold">${endpoint.name}</div>
<div class="small text-body-secondary">${endpoint.resolvedUrl}</div>
</td>
<td>${endpoint.provider}</td>
<td>${endpoint.roles.join(", ")}</td>
<td>${endpoint.connectionState}</td>
<td>${endpoint.activeSubscriptionCount}</td>
`;
tableBody.appendChild(row);
}
}
async function refreshSnapshot(): Promise<void> {
try {
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_get_snapshot");
renderSnapshot(snapshot);
appendLogLine("[ui] refreshed manager snapshot");
} catch (error) {
appendLogLine(`[ui] snapshot error: ${String(error)}`);
}
}
async function loadRoles(): Promise<void> {
if (!roleSelect) {
return;
}
roleSelect.innerHTML = "";
try {
const roles = await invoke<string[]>("demo_ws_manager_list_roles");
for (const role of roles) {
const option = document.createElement("option");
option.value = role;
option.textContent = role;
roleSelect.appendChild(option);
}
} catch (error) {
appendLogLine(`[ui] list roles error: ${String(error)}`);
}
}
async function startAll(): Promise<void> {
try {
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_start_all");
renderSnapshot(snapshot);
} catch (error) {
appendLogLine(`[ui] start all error: ${String(error)}`);
}
}
async function stopAll(): Promise<void> {
try {
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_stop_all");
renderSnapshot(snapshot);
} catch (error) {
appendLogLine(`[ui] stop all error: ${String(error)}`);
}
}
async function startRole(): Promise<void> {
if (!roleSelect || roleSelect.value.trim().length === 0) {
appendLogLine("[ui] no role selected");
return;
}
try {
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_start_role", {
role: roleSelect.value,
});
renderSnapshot(snapshot);
} catch (error) {
appendLogLine(`[ui] start role error: ${String(error)}`);
}
}
async function stopRole(): Promise<void> {
if (!roleSelect || roleSelect.value.trim().length === 0) {
appendLogLine("[ui] no role selected");
return;
}
try {
const snapshot = await invoke<DemoWsManagerSnapshotPayload>("demo_ws_manager_stop_role", {
role: roleSelect.value,
});
renderSnapshot(snapshot);
} catch (error) {
appendLogLine(`[ui] stop role error: ${String(error)}`);
}
}
document.addEventListener("DOMContentLoaded", async () => {
void takeoverConsole();
debug("demo_ws_manager window loaded");
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
if (sidebarToggle) {
// restaurer létat depuis localStorage
if (localStorage.getItem('sidebar-toggle') === 'true') {
document.body.classList.add('sidenav-toggled');
}
sidebarToggle.addEventListener('click', (event) => {
event.preventDefault();
document.body.classList.toggle('sidenav-toggled');
localStorage.setItem('sidebar-toggle', document.body.classList.contains('sidenav-toggled') ? 'true' : 'false');
});
}
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
Array.from(tooltipTriggerList).map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
const toastElList = document.querySelectorAll('.toast');
Array.from(toastElList).map(toastEl => new bootstrap.Toast(toastEl));
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
Array.from(popoverTriggerList).map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
const gobackto = location.pathname + location.search;
document.querySelectorAll<HTMLAnchorElement>('a[data-setlang]').forEach((a) => {
const href = a.getAttribute("href");
if (!href) return; // pas de href => on ignore
const url = new URL(href, location.origin);
url.searchParams.set("gobackto", gobackto);
// conserve une URL relative (path + query)
a.setAttribute("href", url.pathname + "?" + url.searchParams.toString());
});
if (startAllButton) {
startAllButton.addEventListener("click", () => {
void startAll();
});
}
if (stopAllButton) {
stopAllButton.addEventListener("click", () => {
void stopAll();
});
}
if (refreshButton) {
refreshButton.addEventListener("click", () => {
void refreshSnapshot();
});
}
if (startRoleButton) {
startRoleButton.addEventListener("click", () => {
void startRole();
});
}
if (stopRoleButton) {
stopRoleButton.addEventListener("click", () => {
void stopRole();
});
}
if (clearLogButton && logTextarea) {
clearLogButton.addEventListener("click", () => {
logTextarea.value = "";
});
}
await listen<string>("kb-demo-ws-manager-log", (event) => {
appendLogLine(event.payload);
});
await listen<DemoWsManagerSnapshotPayload>("kb-demo-ws-manager-snapshot", (event) => {
renderSnapshot(event.payload);
});
await loadRoles();
await refreshSnapshot();
});

View File

@@ -23,6 +23,14 @@ async function openDemoHttpWindow(): Promise<void> {
console.error("open_demo_http_window failed:", error);
}
}
async function openDemoWsManagerWindow(): Promise<void> {
try {
await invoke("open_demo_ws_manager_window");
} catch (error) {
console.error("open_demo_ws_manager_window failed:", error);
}
}
document.addEventListener("DOMContentLoaded", async () => {
void takeoverConsole();
const sidebarToggle = document.querySelector<HTMLButtonElement>('#sidebarToggle');
@@ -64,6 +72,8 @@ document.addEventListener("DOMContentLoaded", async () => {
const openDemoHttpButton = document.querySelector<HTMLButtonElement>("#openDemoHttpButton");
const openDemoHttpButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoHttpButtonSecondary");
const openDemoWsManagerButton = document.querySelector<HTMLButtonElement>("#openDemoWsManagerButton");
const openDemoWsManagerButtonSecondary = document.querySelector<HTMLButtonElement>("#openDemoWsManagerButtonSecondary");
if (openDemoWsButton) {
openDemoWsButton.addEventListener("click", () => {
@@ -88,6 +98,18 @@ document.addEventListener("DOMContentLoaded", async () => {
void openDemoHttpWindow();
});
}
if (openDemoWsManagerButton) {
openDemoWsManagerButton.addEventListener("click", () => {
void openDemoWsManagerWindow();
});
}
if (openDemoWsManagerButtonSecondary) {
openDemoWsManagerButtonSecondary.addEventListener("click", () => {
void openDemoWsManagerWindow();
});
}
debug("window loaded");