diff --git a/Cargo.toml b/Cargo.toml
index 9f326c4..5116f35 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,12 +1,12 @@
# file: Cargo.toml
-# version: 88
+# version: 89
[workspace]
resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
[workspace.package]
-version = "0.1.4-pre.16.fix.2"
+version = "0.1.4-pre.17"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
diff --git a/crates/ksp-app-config-desk/README.md b/crates/ksp-app-config-desk/README.md
index a43b564..a5a262a 100644
--- a/crates/ksp-app-config-desk/README.md
+++ b/crates/ksp-app-config-desk/README.md
@@ -1,5 +1,5 @@
-
+
# `ksp-app-config-desk`
@@ -113,7 +113,13 @@ main -> ksp-app-config-desk.frontend.main
splash -> ksp-app-config-desk.frontend.splash
```
-Rust valide le niveau et le `targetId`, choisit un callsite statique puis émet exclusivement via les macros de `ksp-logging-lib`. Le package applicatif n'importe pas directement `tracing`. Le pont actuel garantit donc le trajet WebView -> Rust tout en conservant l'affichage local des appels `console.*` dans la console WebKit. Il ne réémet pas encore les événements Rust arbitraires vers la console WebKit : cette direction nécessitera une intégration contrôlée de `tauri_plugin_tracing::WebviewLayer` dans le subscriber possédé par `ksp-logging-lib`, sans second subscriber ni boucle avec le bridge KSP. Le panneau manuel de test Logging, ses targets `test.*` et ses domaines personnalisables restent une surface distincte prévue plus tard.
+Rust valide le niveau et le `targetId`, choisit un callsite statique puis émet exclusivement via les macros de `ksp-logging-lib`. Le package applicatif n'importe pas directement `tracing`. Le pont actuel garantit donc le trajet WebView -> Rust tout en conservant l'affichage local des appels `console.*` dans la console WebKit. Il ne réémet pas encore les événements Rust arbitraires vers la console WebKit : cette direction nécessitera une intégration contrôlée de `tauri_plugin_tracing::WebviewLayer` dans le subscriber possédé par `ksp-logging-lib`, sans second subscriber ni boucle avec le bridge KSP. Le panneau **Test Logging** complète désormais ce bridge : il peut émettre des événements backend via `ksp-logging-lib` avec target KSP statique et domain contrôlé, ou réutiliser le bridge frontend existant avec son target/domain fixes.
+
+## Panneau Test Logging
+
+`pre.017` ajoute une surface de validation volontairement explicite. Le champ **Message** est le contenu qui sera réellement journalisé ; il ne doit donc jamais recevoir de Secret. **Log backend** appelle `emit_logging_test`, qui valide le niveau (`trace/debug/info/warn/error/tous`), choisit un target statique (`ksp-app-config-desk` ou `ksp-app-config-desk.logging-test`) et applique un domain absent, connu ou personnalisé borné avant d'émettre exclusivement avec les macros `ksp-logging-lib`. Le résultat retourne uniquement des métadonnées sûres : nombre d'événements, niveau demandé, target/domain effectifs et génération runtime.
+
+**Log via bridge frontend** réutilise le bridge déjà installé. Son contrat reste volontairement fixe : `target=ksp-app-config-desk.frontend.main`, `domain=frontend`. Cela permet de comparer dans le même panneau le routing backend et le chemin WebView -> Rust -> `ksp-logging-lib`, avant et après modification/hot reload des filtres.
## Bootstrap Config et Logging
diff --git a/crates/ksp-app-config-desk/TODO.md b/crates/ksp-app-config-desk/TODO.md
index 03fe2f1..711c096 100644
--- a/crates/ksp-app-config-desk/TODO.md
+++ b/crates/ksp-app-config-desk/TODO.md
@@ -1,12 +1,11 @@
-
+
# TODO `ksp-app-config-desk`
-Le gabarit frontend, le bootstrap Config/Logging, `AppState`, le `LoggingGuard` durable, les premiers DTO TS-RS, le bridge frontend logging KSP, le lifecycle `splash -> main`, les panneaux Documents, Profils/provenance et Environnement avec management `.env`/reveal Secret privilégié, ainsi que l'édition typée/persistence atomique du document Logging, son hot reload transactionnel, la sélection explicite du profil runtime et les fichiers isolés par identité de lancement sont maintenant présents.
+Le gabarit frontend, le bootstrap Config/Logging, `AppState`, le `LoggingGuard` durable, les premiers DTO TS-RS, le bridge frontend logging KSP, le lifecycle `splash -> main`, les panneaux Documents, Profils/provenance et Environnement avec management `.env`/reveal Secret privilégié, ainsi que l'édition typée/persistence atomique du document Logging, son hot reload transactionnel, la sélection explicite du profil runtime, les fichiers isolés par identité de lancement et le panneau Test Logging backend/frontend sont maintenant présents.
Les prochaines tranches doivent encore :
- ramener le target applicatif de `debug` à `info` ou `warn` avant clôture de la release ;
-- démontrer le routing `trace/debug/info/warn/error`, targets et domains avant/après hot reload avec le panneau Test Logging ;
- étudier puis intégrer le retour Rust -> console WebKit via `tauri_plugin_tracing::WebviewLayer` dans le subscriber possédé par `ksp-logging-lib`, afin de pouvoir utiliser `attachConsole()` sans second subscriber, double émission ni boucle avec le bridge KSP.
diff --git a/crates/ksp-app-config-desk/USAGE.md b/crates/ksp-app-config-desk/USAGE.md
index 32f707d..7da0364 100644
--- a/crates/ksp-app-config-desk/USAGE.md
+++ b/crates/ksp-app-config-desk/USAGE.md
@@ -1,11 +1,11 @@
-
+
# Utilisation de `ksp-app-config-desk`
## État actuel
-Le gabarit Rust/Tauri et le frontend Vite/TypeScript/SCSS sont présents. Le backend initialise Config, le runtime Logging et `AppState`. Les panneaux Documents, Profils et Environnement/`.env` sont fonctionnels, y compris les mutations `.env` create/update/remove et les reveals Secrets privilégiés. Logging expose maintenant un brouillon typé éditable, une persistence atomique, un hot reload sans redémarrage, un profil runtime sélectionnable indépendamment du `default_profile` et une identité de lancement observable pour isoler les fichiers persistants.
+Le gabarit Rust/Tauri et le frontend Vite/TypeScript/SCSS sont présents. Le backend initialise Config, le runtime Logging et `AppState`. Les panneaux Documents, Profils et Environnement/`.env` sont fonctionnels, y compris les mutations `.env` create/update/remove et les reveals Secrets privilégiés. Logging expose maintenant un brouillon typé éditable, une persistence atomique, un hot reload sans redémarrage, un profil runtime sélectionnable indépendamment du `default_profile`, une identité de lancement observable pour isoler les fichiers persistants et un panneau de test du routing backend/frontend.
La fenêtre `splash` est visible au démarrage. Après readiness du frontend et temporisation résolue par Config, l'application effectue le fade-out, affiche/focalise `main` puis détruit `splash`. La fenêtre principale expose immédiatement la navigation monofenêtre de référence.
@@ -214,6 +214,23 @@ Le sélecteur **Profil édité** travaille sur un brouillon local. `logs_directo
+## Test Logging — routing backend et bridge frontend
+
+La section **Test Logging — routing contrôlé** utilise un message explicitement destiné aux logs. Ne jamais y copier une valeur `KSP_SECRET_*` / `KSPB_SECRET_*`.
+
+Pour le backend :
+
+1. choisir `trace`, `debug`, `info`, `warn`, `error` ou `tous` ;
+2. choisir `ksp-app-config-desk` ou `ksp-app-config-desk.logging-test` ;
+3. choisir un domain `absent`, `connu` (`config.logging_test`, `config.logging_runtime`, `frontend`) ou `personnalisé` ;
+4. cliquer **Log backend**.
+
+La commande `emit_logging_test` renvoie le nombre d'événements émis et la génération runtime active, mais ne duplique pas le message dans son DTO de résultat. Le test `tous` émet exactement cinq événements, un par niveau.
+
+**Log via bridge frontend** réutilise le même message et le même niveau, mais garde le contrat du bridge : `target=ksp-app-config-desk.frontend.main`, `domain=frontend`. En mode `tous`, cinq événements sont envoyés. Cette voie permet de vérifier que les changements de console/file sinks/target/domain appliqués par hot reload affectent aussi les événements issus de la WebView.
+
+Scénario conseillé : émettre `tous`, modifier les filtres du profil actif, **Sauvegarder et appliquer**, réémettre exactement le même test et comparer les sinks réellement alimentés sans redémarrer l'application.
+
## Runtime Logging — profil actif, génération et fichiers par lancement
La section **Runtime actif** du panneau Logging ne décrit pas le brouillon : elle interroge l'état réellement installé dans `ksp-logging-lib`.
diff --git a/crates/ksp-app-config-desk/frontend/main.html b/crates/ksp-app-config-desk/frontend/main.html
index d4284f8..b1a0e7c 100644
--- a/crates/ksp-app-config-desk/frontend/main.html
+++ b/crates/ksp-app-config-desk/frontend/main.html
@@ -1,5 +1,5 @@
-
+
@@ -360,6 +360,66 @@
+
+
+
+
Test Logging — routing contrôlé
+
Émet des événements volontairement journalisés pour vérifier niveaux, targets, domains, sinks et hot reload. Ne colle jamais de Secret dans le message de test.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Le test backend respecte le target/domain choisis. Le bridge frontend conserve volontairement son contrat fixe target=ksp-app-config-desk.frontend.main, domain=frontend et réutilise uniquement le message/niveau.
+
Aucun événement de test émis pendant ce lancement.
+
+
Globals
diff --git a/crates/ksp-app-config-desk/frontend/ts/frontend_log.ts b/crates/ksp-app-config-desk/frontend/ts/frontend_log.ts
index e63812c..8ca6624 100644
--- a/crates/ksp-app-config-desk/frontend/ts/frontend_log.ts
+++ b/crates/ksp-app-config-desk/frontend/ts/frontend_log.ts
@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/frontend_log.ts
-// version: 1
+// version: 2
//! Frontend logging helpers routed through the KSP Logging facade.
@@ -45,47 +45,73 @@ async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTar
targetId,
message,
};
- try {
- await invoke("emit_frontend_log", { payload });
- } catch (caughtError) {
- originalConsole.error("KSP frontend logging bridge failed", caughtError);
- }
+ await invoke("emit_frontend_log", { payload });
+}
+
+function writeOriginalConsole(level: FrontendLogLevel, message: string): void {
+ return level === "trace"
+ ? originalConsole.trace(message)
+ : level === "debug"
+ ? originalConsole.debug(message)
+ : level === "info"
+ ? originalConsole.info(message)
+ : level === "warn"
+ ? originalConsole.warn(message)
+ : originalConsole.error(message);
+}
+
+/// Emits one frontend event and resolves only after the Rust bridge accepted it.
+export async function emitFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise {
+ writeOriginalConsole(level, message);
+ await sendFrontendLog(level, targetId, message);
}
export function frontendTrace(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.trace(message);
- void sendFrontendLog("trace", targetId, message);
+ void sendFrontendLog("trace", targetId, message).catch(caughtError =>
+ originalConsole.error("KSP frontend logging bridge failed", caughtError),
+ );
}
export function frontendDebug(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.debug(message);
- void sendFrontendLog("debug", targetId, message);
+ void sendFrontendLog("debug", targetId, message).catch(caughtError =>
+ originalConsole.error("KSP frontend logging bridge failed", caughtError),
+ );
}
export function frontendInfo(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.info(message);
- void sendFrontendLog("info", targetId, message);
+ void sendFrontendLog("info", targetId, message).catch(caughtError =>
+ originalConsole.error("KSP frontend logging bridge failed", caughtError),
+ );
}
export function frontendWarn(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.warn(message);
- void sendFrontendLog("warn", targetId, message);
+ void sendFrontendLog("warn", targetId, message).catch(caughtError =>
+ originalConsole.error("KSP frontend logging bridge failed", caughtError),
+ );
}
export function frontendError(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.error(message);
- void sendFrontendLog("error", targetId, message);
+ void sendFrontendLog("error", targetId, message).catch(caughtError =>
+ originalConsole.error("KSP frontend logging bridge failed", caughtError),
+ );
}
function buildConsoleBridge(level: FrontendLogLevel, targetId: FrontendLogTargetId, original: ConsoleMethod): ConsoleMethod {
return (...items: unknown[]) => {
original(...items);
- void sendFrontendLog(level, targetId, formatMessage(items));
+ void sendFrontendLog(level, targetId, formatMessage(items)).catch(caughtError =>
+ originalConsole.error("KSP frontend logging bridge failed", caughtError),
+ );
};
}
diff --git a/crates/ksp-app-config-desk/frontend/ts/logging.ts b/crates/ksp-app-config-desk/frontend/ts/logging.ts
index 5f209b0..fb06718 100644
--- a/crates/ksp-app-config-desk/frontend/ts/logging.ts
+++ b/crates/ksp-app-config-desk/frontend/ts/logging.ts
@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/logging.ts
-// version: 5
+// version: 6
//! Typed Logging editor backed by ConfigManagement persistence and KSP-owned runtime hot reload.
@@ -13,6 +13,7 @@ import type { LoggingTargetFilterDto } from "./bindings/ksp_app_config_desk/logg
import type { LoggingRuntimeStatusDto } from "./bindings/ksp_app_config_desk/logging_runtime/LoggingRuntimeStatusDto";
import { frontendDebug, frontendTrace } from "./frontend_log";
import { invokeKsp } from "./invoke";
+import { initializeLoggingTestPanel } from "./logging_test";
const LEVELS = ["off", "error", "warn", "info", "debug", "trace"] as const;
const ROTATIONS = ["never", "hourly", "daily"] as const;
@@ -772,6 +773,7 @@ export function initializeLoggingPanel(): void {
document.querySelector("#loggingDiscardDraftConfirm")?.addEventListener("click", confirmReload);
document.querySelector("#addLoggingFile")?.addEventListener("click", addFile);
document.querySelector("#addLoggingTargetFilter")?.addEventListener("click", addTargetFilter);
+ initializeLoggingTestPanel();
frontendTrace("main", "Logging editor mutation handlers installed");
void refreshLoggingDocument("startup");
}
diff --git a/crates/ksp-app-config-desk/frontend/ts/logging_test.ts b/crates/ksp-app-config-desk/frontend/ts/logging_test.ts
new file mode 100644
index 0000000..c92e498
--- /dev/null
+++ b/crates/ksp-app-config-desk/frontend/ts/logging_test.ts
@@ -0,0 +1,122 @@
+// file: crates/ksp-app-config-desk/frontend/ts/logging_test.ts
+// version: 1
+
+//! Controlled Logging test panel for proving backend routing and the frontend bridge.
+
+import type { LoggingTestRequestDto } from "./bindings/ksp_app_config_desk/logging_test/LoggingTestRequestDto";
+import type { LoggingTestResultDto } from "./bindings/ksp_app_config_desk/logging_test/LoggingTestResultDto";
+import { emitFrontendLog, frontendTrace, type FrontendLogLevel } from "./frontend_log";
+import { invokeKsp } from "./invoke";
+
+const FRONTEND_BRIDGE_TARGET = "ksp-app-config-desk.frontend.main";
+const FRONTEND_BRIDGE_DOMAIN = "frontend";
+
+function inputValue(selector: string): string {
+ return document.querySelector(selector)?.value ?? "";
+}
+
+function selectedValue(selector: string): string {
+ return document.querySelector(selector)?.value ?? "";
+}
+
+function setStatus(message: string, tone: "secondary" | "success" | "warning" | "danger" = "secondary"): void {
+ const status = document.querySelector("#loggingTestStatus");
+ if (!status) {
+ return;
+ }
+ status.className = `alert alert-${tone} mb-0`;
+ status.textContent = message;
+}
+
+function updateDomainControls(): void {
+ const mode = selectedValue("#loggingTestDomainMode");
+ const known = document.querySelector("#loggingTestKnownDomain");
+ const custom = document.querySelector("#loggingTestCustomDomain");
+ if (known) {
+ known.disabled = mode !== "known";
+ }
+ if (custom) {
+ custom.disabled = mode !== "custom";
+ }
+}
+
+function effectiveDomain(): string {
+ const mode = selectedValue("#loggingTestDomainMode");
+ if (mode === "known") {
+ return selectedValue("#loggingTestKnownDomain");
+ }
+ if (mode === "custom") {
+ return inputValue("#loggingTestCustomDomain");
+ }
+ return "";
+}
+
+function buildRequest(): LoggingTestRequestDto {
+ return {
+ level: selectedValue("#loggingTestLevel"),
+ targetId: selectedValue("#loggingTestTarget"),
+ domainMode: selectedValue("#loggingTestDomainMode"),
+ domain: effectiveDomain(),
+ message: inputValue("#loggingTestMessage"),
+ };
+}
+
+async function emitBackendTest(): Promise {
+ const request = buildRequest();
+ setStatus("Émission backend en cours...");
+ try {
+ const result = await invokeKsp("main", "emit_logging_test", { request });
+ const domain = result.domain ?? "";
+ const summary = `${result.emittedEventCount} événement(s) backend émis · niveau=${result.requestedLevel} · target=${result.target}`
+ + ` · domain=${domain} · génération=${result.loggingGeneration}.`;
+ setStatus(summary, "success");
+ } catch {
+ setStatus("Émission backend refusée. Vérifie le message, le target et le domain sélectionnés.", "danger");
+ }
+}
+
+async function emitFrontendLevel(level: FrontendLogLevel, message: string): Promise {
+ await emitFrontendLog(level, "main", message);
+}
+
+async function emitFrontendBridgeTest(): Promise {
+ const message = inputValue("#loggingTestMessage").trim();
+ if (message.length === 0 || message.length > 500) {
+ setStatus("Le message de test frontend doit contenir entre 1 et 500 caractères.", "danger");
+ return;
+ }
+ const level = selectedValue("#loggingTestLevel");
+ setStatus("Émission via bridge frontend en cours...");
+ try {
+ if (level === "all") {
+ for (const item of ["trace", "debug", "info", "warn", "error"] as const) {
+ await emitFrontendLevel(item, `[logging-test frontend/${item}] ${message}`);
+ }
+ setStatus(
+ `5 événements acceptés par le bridge frontend · target=${FRONTEND_BRIDGE_TARGET} · domain=${FRONTEND_BRIDGE_DOMAIN}.`,
+ "success",
+ );
+ return;
+ }
+ if (level === "trace" || level === "debug" || level === "info" || level === "warn" || level === "error") {
+ await emitFrontendLevel(level, `[logging-test frontend/${level}] ${message}`);
+ setStatus(
+ `1 événement accepté par le bridge frontend · target=${FRONTEND_BRIDGE_TARGET} · domain=${FRONTEND_BRIDGE_DOMAIN}.`,
+ "success",
+ );
+ return;
+ }
+ setStatus("Le niveau de test frontend sélectionné n'est pas supporté.", "danger");
+ } catch {
+ setStatus("Le bridge frontend n'a pas accepté l'événement de test.", "danger");
+ }
+}
+
+/// Initializes the controlled Logging test surface.
+export function initializeLoggingTestPanel(): void {
+ document.querySelector("#loggingTestDomainMode")?.addEventListener("change", updateDomainControls);
+ document.querySelector("#emitLoggingTestBackend")?.addEventListener("click", () => void emitBackendTest());
+ document.querySelector("#emitLoggingTestFrontend")?.addEventListener("click", () => void emitFrontendBridgeTest());
+ updateDomainControls();
+ frontendTrace("main", "Logging test panel handlers installed");
+}
diff --git a/crates/ksp-app-config-desk/frontend/ts/main.ts b/crates/ksp-app-config-desk/frontend/ts/main.ts
index 5ffe28d..1b138fa 100644
--- a/crates/ksp-app-config-desk/frontend/ts/main.ts
+++ b/crates/ksp-app-config-desk/frontend/ts/main.ts
@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/main.ts
-// version: 11
+// version: 12
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
@@ -38,7 +38,7 @@ const viewCopy: Record = {
},
logging: {
title: "Logging",
- description: "Édition typée et persistence atomique des profils, sinks et filtres Logging ; le hot reload runtime reste dans la tranche suivante.",
+ description: "Édition typée, hot reload runtime, profils actifs et panneau de test du routing Logging KSP.",
},
};
diff --git a/crates/ksp-app-config-desk/src/constants.rs b/crates/ksp-app-config-desk/src/constants.rs
index 111d2b1..60f8cd1 100644
--- a/crates/ksp-app-config-desk/src/constants.rs
+++ b/crates/ksp-app-config-desk/src/constants.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/constants.rs
-// version: 8
+// version: 9
//! Application-owned tracing targets and domains.
@@ -11,6 +11,8 @@ pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-config-desk.frontend";
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-config-desk.frontend.main";
/// Owning target for splash-window frontend events.
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-config-desk.frontend.splash";
+/// Dedicated target used by the controlled Logging test panel.
+pub(crate) const TRACING_TARGET_LOGGING_TEST: &str = "ksp-app-config-desk.logging-test";
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "config.bootstrap";
/// Structured domain used by technical frontend events.
@@ -29,3 +31,5 @@ pub(crate) const TRACING_DOMAIN_SECRETS: &str = "config.secrets";
pub(crate) const TRACING_DOMAIN_LOGGING_EDITOR: &str = "config.logging_editor";
/// Structured domain for active Logging runtime metadata and explicit profile application.
pub(crate) const TRACING_DOMAIN_LOGGING_RUNTIME: &str = "config.logging_runtime";
+/// Structured domain used by controlled Logging test events.
+pub(crate) const TRACING_DOMAIN_LOGGING_TEST: &str = "config.logging_test";
diff --git a/crates/ksp-app-config-desk/src/errors.rs b/crates/ksp-app-config-desk/src/errors.rs
index 0cb342b..4f9f87f 100644
--- a/crates/ksp-app-config-desk/src/errors.rs
+++ b/crates/ksp-app-config-desk/src/errors.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/errors.rs
-// version: 8
+// version: 9
//! Application-local error codes for the configuration desktop shell.
@@ -18,6 +18,8 @@ pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_level_invalid");
/// Frontend logging requested a target outside the application whitelist.
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "frontend_log_target_invalid");
+/// Controlled Logging test request contains an unsupported or invalid selector.
+pub(crate) const ERROR_CODE_LOGGING_TEST_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "logging_test_request_invalid");
/// A validated Config document does not expose the standard profile contract expected by the Profiles panel.
pub(crate) const ERROR_CODE_PROFILE_CONTRACT_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "profile_contract_missing");
diff --git a/crates/ksp-app-config-desk/src/lib.rs b/crates/ksp-app-config-desk/src/lib.rs
index df6aa88..076a5ff 100644
--- a/crates/ksp-app-config-desk/src/lib.rs
+++ b/crates/ksp-app-config-desk/src/lib.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/lib.rs
-// version: 13
+// version: 14
//! Tauri desktop application for managing and validating KSP configuration.
@@ -17,6 +17,7 @@ mod errors;
mod frontend_logging;
mod logging_editor;
mod logging_runtime;
+mod logging_test;
mod profiles;
mod secrets;
mod splash;
@@ -36,6 +37,7 @@ pub(crate) use self::constants::TRACING_DOMAIN_ENVIRONMENT;
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_EDITOR;
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_RUNTIME;
+pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_TEST;
pub(crate) use self::constants::TRACING_DOMAIN_PROFILES;
pub(crate) use self::constants::TRACING_DOMAIN_SECRETS;
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
@@ -43,6 +45,7 @@ pub(crate) use self::constants::TRACING_TARGET;
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
+pub(crate) use self::constants::TRACING_TARGET_LOGGING_TEST;
pub(crate) use self::documents::ConfigDocumentDetailDto;
pub(crate) use self::documents::ConfigDocumentErrorDto;
pub(crate) use self::documents::ConfigDocumentSaveResultDto;
@@ -58,6 +61,7 @@ pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
pub(crate) use self::errors::ERROR_CODE_LOGGING_SOURCE_ROLLBACK_FAILED;
+pub(crate) use self::errors::ERROR_CODE_LOGGING_TEST_REQUEST_INVALID;
pub(crate) use self::errors::ERROR_CODE_PROFILE_CONTRACT_MISSING;
pub(crate) use self::errors::ERROR_CODE_PROFILE_PROJECTION_FAILED;
pub(crate) use self::errors::ERROR_CODE_SECRET_REVEAL_REQUIRES_SECRET;
@@ -73,6 +77,7 @@ pub(crate) use self::logging_editor::LoggingDocumentCandidateDto;
pub(crate) use self::logging_editor::LoggingDocumentDto;
pub(crate) use self::logging_editor::LoggingDocumentSaveResultDto;
pub(crate) use self::logging_runtime::{LoggingRuntimeFileDto, LoggingRuntimeStatusDto};
+pub(crate) use self::logging_test::{LoggingTestRequestDto, LoggingTestResultDto};
pub(crate) use self::profiles::ConfigProfileDetailDto;
pub(crate) use self::profiles::ConfigProfileDocumentDto;
pub(crate) use self::secrets::SecretRevealRequestDto;
diff --git a/crates/ksp-app-config-desk/src/logging_test.rs b/crates/ksp-app-config-desk/src/logging_test.rs
new file mode 100644
index 0000000..4d4b41a
--- /dev/null
+++ b/crates/ksp-app-config-desk/src/logging_test.rs
@@ -0,0 +1,428 @@
+// file: crates/ksp-app-config-desk/src/logging_test.rs
+// version: 1
+
+//! Controlled Logging test events for validating KSP runtime routing from Config Desk.
+
+use ts_rs::TS; // rust-rules: derive-import
+
+/// Controlled request emitted through the backend KSP Logging facade.
+#[derive(Clone, Debug, serde::Deserialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging_test/LoggingTestRequestDto.ts")]
+pub(crate) struct LoggingTestRequestDto {
+ /// Requested level: trace/debug/info/warn/error/all.
+ pub(crate) level: String,
+ /// Controlled backend target identifier.
+ pub(crate) target_id: String,
+ /// Domain selection mode: absent/known/custom.
+ pub(crate) domain_mode: String,
+ /// Selected known domain or custom domain token.
+ pub(crate) domain: String,
+ /// Human test message emitted as the event body.
+ pub(crate) message: String,
+}
+
+/// Safe result metadata for one controlled backend Logging test request.
+#[derive(Clone, Debug, serde::Serialize, TS)]
+#[serde(rename_all = "camelCase")]
+#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging_test/LoggingTestResultDto.ts")]
+pub(crate) struct LoggingTestResultDto {
+ /// Number of Logging events emitted by the request.
+ pub(crate) emitted_event_count: u32,
+ /// Requested level selector.
+ pub(crate) requested_level: String,
+ /// Effective static KSP target used by the emitted events.
+ pub(crate) target: String,
+ /// Effective domain, or `None` when domain routing is intentionally absent.
+ pub(crate) domain: std::option::Option,
+ /// Logging runtime generation active while the events were emitted.
+ pub(crate) logging_generation: u32,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum LoggingTestLevel {
+ Trace,
+ Debug,
+ Info,
+ Warn,
+ Error,
+ All,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+enum LoggingTestTarget {
+ App,
+ Dedicated,
+}
+
+/// Emits one controlled backend test request through `ksp-logging-lib` only.
+pub(crate) fn emit(state: &crate::AppState, request: LoggingTestRequestDto) -> ksp_core_lib::Result {
+ let level = parse_level(request.level.as_str());
+ let level = match level {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let target = parse_target(request.target_id.as_str());
+ let target = match target {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let domain = resolve_domain(request.domain_mode.as_str(), request.domain.as_str());
+ let domain = match domain {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let message = validate_message(request.message.as_str());
+ let message = match message {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let runtime_status = state.logging_runtime_status();
+ let runtime_status = match runtime_status {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let emitted_event_count = emit_levels(level, target, domain.as_deref(), message);
+ return std::result::Result::Ok(LoggingTestResultDto {
+ emitted_event_count,
+ requested_level: request.level.trim().to_ascii_lowercase(),
+ target: target_value(target).to_owned(),
+ domain,
+ logging_generation: runtime_status.generation,
+ });
+}
+
+fn parse_level(level: &str) -> ksp_core_lib::Result {
+ return match level.trim().to_ascii_lowercase().as_str() {
+ "trace" => std::result::Result::Ok(LoggingTestLevel::Trace),
+ "debug" => std::result::Result::Ok(LoggingTestLevel::Debug),
+ "info" => std::result::Result::Ok(LoggingTestLevel::Info),
+ "warn" => std::result::Result::Ok(LoggingTestLevel::Warn),
+ "error" => std::result::Result::Ok(LoggingTestLevel::Error),
+ "all" => std::result::Result::Ok(LoggingTestLevel::All),
+ _ => invalid_request("Logging test level is not supported"),
+ };
+}
+
+fn parse_target(target_id: &str) -> ksp_core_lib::Result {
+ return match target_id.trim().to_ascii_lowercase().as_str() {
+ "app" => std::result::Result::Ok(LoggingTestTarget::App),
+ "logging_test" => std::result::Result::Ok(LoggingTestTarget::Dedicated),
+ _ => invalid_request("Logging test target is not supported"),
+ };
+}
+
+fn resolve_domain(domain_mode: &str, domain: &str) -> ksp_core_lib::Result> {
+ return match domain_mode.trim().to_ascii_lowercase().as_str() {
+ "absent" => std::result::Result::Ok(std::option::Option::None),
+ "known" => resolve_known_domain(domain),
+ "custom" => resolve_custom_domain(domain),
+ _ => invalid_request("Logging test domain mode is not supported"),
+ };
+}
+
+fn resolve_known_domain(domain: &str) -> ksp_core_lib::Result> {
+ return match domain.trim() {
+ crate::TRACING_DOMAIN_LOGGING_TEST | crate::TRACING_DOMAIN_LOGGING_RUNTIME | crate::TRACING_DOMAIN_FRONTEND => {
+ std::result::Result::Ok(std::option::Option::Some(domain.trim().to_owned()))
+ },
+ _ => invalid_request("Logging test known domain is not supported"),
+ };
+}
+
+fn resolve_custom_domain(domain: &str) -> ksp_core_lib::Result> {
+ let domain = domain.trim();
+ if domain.is_empty() || domain.len() > 80 || !domain.chars().all(is_domain_character) {
+ return invalid_request("Logging test custom domain is invalid");
+ }
+ return std::result::Result::Ok(std::option::Option::Some(domain.to_owned()));
+}
+
+fn is_domain_character(character: char) -> bool {
+ return character.is_ascii_alphanumeric() || character == '.' || character == '_' || character == '-';
+}
+
+fn validate_message(message: &str) -> ksp_core_lib::Result<&str> {
+ let message = message.trim();
+ if message.is_empty() || message.len() > 500 {
+ return invalid_request("Logging test message must contain between 1 and 500 bytes");
+ }
+ return std::result::Result::Ok(message);
+}
+
+fn invalid_request(message: &str) -> ksp_core_lib::Result {
+ return std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_LOGGING_TEST_REQUEST_INVALID, message));
+}
+
+const fn target_value(target: LoggingTestTarget) -> &'static str {
+ return match target {
+ LoggingTestTarget::App => crate::TRACING_TARGET,
+ LoggingTestTarget::Dedicated => crate::TRACING_TARGET_LOGGING_TEST,
+ };
+}
+
+fn emit_levels(level: LoggingTestLevel, target: LoggingTestTarget, domain: std::option::Option<&str>, message: &str) -> u32 {
+ return match level {
+ LoggingTestLevel::Trace => {
+ emit_one(LoggingTestLevel::Trace, target, domain, message);
+ 1
+ },
+ LoggingTestLevel::Debug => {
+ emit_one(LoggingTestLevel::Debug, target, domain, message);
+ 1
+ },
+ LoggingTestLevel::Info => {
+ emit_one(LoggingTestLevel::Info, target, domain, message);
+ 1
+ },
+ LoggingTestLevel::Warn => {
+ emit_one(LoggingTestLevel::Warn, target, domain, message);
+ 1
+ },
+ LoggingTestLevel::Error => {
+ emit_one(LoggingTestLevel::Error, target, domain, message);
+ 1
+ },
+ LoggingTestLevel::All => {
+ emit_one(LoggingTestLevel::Trace, target, domain, message);
+ emit_one(LoggingTestLevel::Debug, target, domain, message);
+ emit_one(LoggingTestLevel::Info, target, domain, message);
+ emit_one(LoggingTestLevel::Warn, target, domain, message);
+ emit_one(LoggingTestLevel::Error, target, domain, message);
+ 5
+ },
+ };
+}
+
+fn emit_one(level: LoggingTestLevel, target: LoggingTestTarget, domain: std::option::Option<&str>, message: &str) {
+ return match target {
+ LoggingTestTarget::App => emit_app_target(level, domain, message),
+ LoggingTestTarget::Dedicated => emit_dedicated_target(level, domain, message),
+ };
+}
+
+fn emit_app_target(level: LoggingTestLevel, domain: std::option::Option<&str>, message: &str) {
+ return match level {
+ LoggingTestLevel::Trace => emit_app_trace(domain, message),
+ LoggingTestLevel::Debug => emit_app_debug(domain, message),
+ LoggingTestLevel::Info => emit_app_info(domain, message),
+ LoggingTestLevel::Warn => emit_app_warn(domain, message),
+ LoggingTestLevel::Error => emit_app_error(domain, message),
+ LoggingTestLevel::All => (),
+ };
+}
+
+fn emit_dedicated_target(level: LoggingTestLevel, domain: std::option::Option<&str>, message: &str) {
+ return match level {
+ LoggingTestLevel::Trace => emit_dedicated_trace(domain, message),
+ LoggingTestLevel::Debug => emit_dedicated_debug(domain, message),
+ LoggingTestLevel::Info => emit_dedicated_info(domain, message),
+ LoggingTestLevel::Warn => emit_dedicated_warn(domain, message),
+ LoggingTestLevel::Error => emit_dedicated_error(domain, message),
+ LoggingTestLevel::All => (),
+ };
+}
+
+fn emit_app_trace(domain: std::option::Option<&str>, message: &str) {
+ return match domain {
+ std::option::Option::Some(domain) => ksp_logging_lib::trace!(
+ target: crate::TRACING_TARGET,
+ domain = domain,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "trace",
+ "{message}"
+ ),
+ std::option::Option::None => ksp_logging_lib::trace!(
+ target: crate::TRACING_TARGET,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "trace",
+ "{message}"
+ ),
+ };
+}
+
+fn emit_app_debug(domain: std::option::Option<&str>, message: &str) {
+ return match domain {
+ std::option::Option::Some(domain) => ksp_logging_lib::debug!(
+ target: crate::TRACING_TARGET,
+ domain = domain,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "debug",
+ "{message}"
+ ),
+ std::option::Option::None => ksp_logging_lib::debug!(
+ target: crate::TRACING_TARGET,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "debug",
+ "{message}"
+ ),
+ };
+}
+
+fn emit_app_info(domain: std::option::Option<&str>, message: &str) {
+ return match domain {
+ std::option::Option::Some(domain) => ksp_logging_lib::info!(
+ target: crate::TRACING_TARGET,
+ domain = domain,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "info",
+ "{message}"
+ ),
+ std::option::Option::None => ksp_logging_lib::info!(
+ target: crate::TRACING_TARGET,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "info",
+ "{message}"
+ ),
+ };
+}
+
+fn emit_app_warn(domain: std::option::Option<&str>, message: &str) {
+ return match domain {
+ std::option::Option::Some(domain) => ksp_logging_lib::warn!(
+ target: crate::TRACING_TARGET,
+ domain = domain,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "warn",
+ "{message}"
+ ),
+ std::option::Option::None => ksp_logging_lib::warn!(
+ target: crate::TRACING_TARGET,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "warn",
+ "{message}"
+ ),
+ };
+}
+
+fn emit_app_error(domain: std::option::Option<&str>, message: &str) {
+ return match domain {
+ std::option::Option::Some(domain) => ksp_logging_lib::error!(
+ target: crate::TRACING_TARGET,
+ domain = domain,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "error",
+ "{message}"
+ ),
+ std::option::Option::None => ksp_logging_lib::error!(
+ target: crate::TRACING_TARGET,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "error",
+ "{message}"
+ ),
+ };
+}
+
+fn emit_dedicated_trace(domain: std::option::Option<&str>, message: &str) {
+ return match domain {
+ std::option::Option::Some(domain) => ksp_logging_lib::trace!(
+ target: crate::TRACING_TARGET_LOGGING_TEST,
+ domain = domain,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "trace",
+ "{message}"
+ ),
+ std::option::Option::None => ksp_logging_lib::trace!(
+ target: crate::TRACING_TARGET_LOGGING_TEST,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "trace",
+ "{message}"
+ ),
+ };
+}
+
+fn emit_dedicated_debug(domain: std::option::Option<&str>, message: &str) {
+ return match domain {
+ std::option::Option::Some(domain) => ksp_logging_lib::debug!(
+ target: crate::TRACING_TARGET_LOGGING_TEST,
+ domain = domain,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "debug",
+ "{message}"
+ ),
+ std::option::Option::None => ksp_logging_lib::debug!(
+ target: crate::TRACING_TARGET_LOGGING_TEST,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "debug",
+ "{message}"
+ ),
+ };
+}
+
+fn emit_dedicated_info(domain: std::option::Option<&str>, message: &str) {
+ return match domain {
+ std::option::Option::Some(domain) => ksp_logging_lib::info!(
+ target: crate::TRACING_TARGET_LOGGING_TEST,
+ domain = domain,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "info",
+ "{message}"
+ ),
+ std::option::Option::None => ksp_logging_lib::info!(
+ target: crate::TRACING_TARGET_LOGGING_TEST,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "info",
+ "{message}"
+ ),
+ };
+}
+
+fn emit_dedicated_warn(domain: std::option::Option<&str>, message: &str) {
+ return match domain {
+ std::option::Option::Some(domain) => ksp_logging_lib::warn!(
+ target: crate::TRACING_TARGET_LOGGING_TEST,
+ domain = domain,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "warn",
+ "{message}"
+ ),
+ std::option::Option::None => ksp_logging_lib::warn!(
+ target: crate::TRACING_TARGET_LOGGING_TEST,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "warn",
+ "{message}"
+ ),
+ };
+}
+
+fn emit_dedicated_error(domain: std::option::Option<&str>, message: &str) {
+ return match domain {
+ std::option::Option::Some(domain) => ksp_logging_lib::error!(
+ target: crate::TRACING_TARGET_LOGGING_TEST,
+ domain = domain,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "error",
+ "{message}"
+ ),
+ std::option::Option::None => ksp_logging_lib::error!(
+ target: crate::TRACING_TARGET_LOGGING_TEST,
+ action = "logging_test",
+ test_source = "backend",
+ test_level = "error",
+ "{message}"
+ ),
+ };
+}
+
+#[cfg(test)]
+#[path = "../unit_tests/logging_test.rs"]
+mod tests;
diff --git a/crates/ksp-app-config-desk/src/tauri.rs b/crates/ksp-app-config-desk/src/tauri.rs
index 35fef0e..ff99d5e 100644
--- a/crates/ksp-app-config-desk/src/tauri.rs
+++ b/crates/ksp-app-config-desk/src/tauri.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/tauri.rs
-// version: 14
+// version: 15
//! Tauri runtime assembly for the KSP configuration desktop application.
@@ -52,6 +52,7 @@ fn configure_commands(builder: tauri::Builder) -> tauri::Builder,
+) -> std::result::Result {
+ let result = crate::logging_test::emit(&state, request);
+ return match result {
+ std::result::Result::Ok(value) => std::result::Result::Ok(value),
+ std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
+ };
+}
+
#[tauri::command]
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload);
diff --git a/crates/ksp-app-config-desk/unit_tests/logging_test.rs b/crates/ksp-app-config-desk/unit_tests/logging_test.rs
new file mode 100644
index 0000000..2647a1a
--- /dev/null
+++ b/crates/ksp-app-config-desk/unit_tests/logging_test.rs
@@ -0,0 +1,51 @@
+// file: crates/ksp-app-config-desk/unit_tests/logging_test.rs
+// version: 1
+
+use super::{LoggingTestLevel, LoggingTestTarget};
+
+#[test]
+fn controlled_logging_test_levels_include_all_five_levels_and_batch_mode() {
+ assert!(matches!(super::parse_level("trace"), std::result::Result::Ok(LoggingTestLevel::Trace)));
+ assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(LoggingTestLevel::Debug)));
+ assert!(matches!(super::parse_level("info"), std::result::Result::Ok(LoggingTestLevel::Info)));
+ assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(LoggingTestLevel::Warn)));
+ assert!(matches!(super::parse_level("error"), std::result::Result::Ok(LoggingTestLevel::Error)));
+ assert!(matches!(super::parse_level("all"), std::result::Result::Ok(LoggingTestLevel::All)));
+ assert!(super::parse_level("fatal").is_err());
+}
+
+#[test]
+fn controlled_logging_test_targets_are_static_ksp_targets() {
+ assert!(matches!(super::parse_target("app"), std::result::Result::Ok(LoggingTestTarget::App)));
+ assert!(matches!(super::parse_target("logging_test"), std::result::Result::Ok(LoggingTestTarget::Dedicated)));
+ assert_eq!(super::target_value(LoggingTestTarget::App), crate::TRACING_TARGET);
+ assert_eq!(super::target_value(LoggingTestTarget::Dedicated), crate::TRACING_TARGET_LOGGING_TEST);
+ assert!(super::parse_target("external").is_err());
+}
+
+#[test]
+fn logging_test_domain_modes_cover_absent_known_and_bounded_custom_values() {
+ let absent = super::resolve_domain("absent", "ignored");
+ assert!(matches!(absent, std::result::Result::Ok(std::option::Option::None)));
+ let known = super::resolve_domain("known", crate::TRACING_DOMAIN_LOGGING_TEST);
+ assert!(matches!(
+ known.as_ref(),
+ std::result::Result::Ok(std::option::Option::Some(value)) if value == crate::TRACING_DOMAIN_LOGGING_TEST
+ ));
+ let custom = super::resolve_domain("custom", "custom.route_1-test");
+ assert!(matches!(
+ custom.as_ref(),
+ std::result::Result::Ok(std::option::Option::Some(value)) if value == "custom.route_1-test"
+ ));
+ assert!(super::resolve_domain("custom", "custom route").is_err());
+ assert!(super::resolve_domain("custom", "").is_err());
+ assert!(super::resolve_domain("known", "unknown.domain").is_err());
+}
+
+#[test]
+fn logging_test_message_is_required_and_bounded() {
+ let message = super::validate_message(" test ");
+ assert!(matches!(message, std::result::Result::Ok("test")));
+ assert!(super::validate_message(" ").is_err());
+ assert!(super::validate_message("x".repeat(501).as_str()).is_err());
+}
diff --git a/deltas/0.1.4/pre.017.md b/deltas/0.1.4/pre.017.md
new file mode 100644
index 0000000..2939135
--- /dev/null
+++ b/deltas/0.1.4/pre.017.md
@@ -0,0 +1,91 @@
+
+
+
+# Delta `0.1.4-pre.017` — panneau Test Logging
+
+## Base
+
+`0.1.4-pre.016-fix.002` est validée : `cargo fmt --all`, `cargo check --workspace` et `cargo clippy --workspace --all-targets` sont propres ; 56 tests `ksp-app-config-desk`, 88 tests `ksp-config-lib`, 37 tests unitaires `ksp-logging-lib`, les audits ownership, APIs publiques et tests runtime/intégration passent.
+
+## Objectif
+
+Ajouter une surface manuelle et contrôlée pour démontrer le routing réel du runtime Logging avant/après hot reload, sans contourner `ksp-logging-lib` et sans introduire de target dynamique arbitraire.
+
+## Changements
+
+### Backend contrôlé
+
+Config Desk ajoute `emit_logging_test` et deux DTO TS-RS :
+
+- `LoggingTestRequestDto` ;
+- `LoggingTestResultDto`.
+
+Le service accepte :
+
+- niveau `trace`, `debug`, `info`, `warn`, `error` ou `all` ;
+- target logique `app` -> `ksp-app-config-desk` ;
+- target logique `logging_test` -> `ksp-app-config-desk.logging-test` ;
+- domain `absent`, `known` ou `custom`.
+
+Les domains connus sont limités à `config.logging_test`, `config.logging_runtime` et `frontend`. Un domain personnalisé est borné à 80 caractères ASCII alphanumériques plus `.`, `_` et `-`. Le message doit contenir de 1 à 500 octets.
+
+Les targets restent des callsites statiques. L'application n'importe pas `tracing` et émet exclusivement via les macros `ksp-logging-lib`.
+
+Le mode `all` émet exactement cinq événements, un par niveau. Le résultat ne réexpose pas le message ; il contient uniquement le nombre d'événements, le niveau demandé, target/domain effectifs et la génération runtime active.
+
+### Frontend
+
+La vue Logging ajoute **Test Logging — routing contrôlé** avec :
+
+- message ;
+- niveau ;
+- target backend contrôlé ;
+- domain absent/connu/personnalisé ;
+- **Log backend** ;
+- **Log via bridge frontend**.
+
+La voie frontend réutilise le bridge existant et conserve son contrat fixe `target=ksp-app-config-desk.frontend.main`, `domain=frontend`. Elle permet de comparer le routing d'un événement backend et celui d'un événement issu de la WebView dans les mêmes sinks.
+
+Le message du panneau est volontairement journalisé : l'UI rappelle explicitement de ne jamais y placer de Secret.
+
+### Tests
+
+Les tests ajoutés couvrent :
+
+- les cinq niveaux et le mode batch `all` ;
+- les deux targets statiques ;
+- les trois modes de domain ;
+- le rejet des domains personnalisés non sûrs ;
+- la borne du message ;
+- les exports TS-RS des DTO.
+
+## Version technique
+
+```text
+workspace.package.version = 0.1.4-pre.17
+```
+
+## Validation attendue
+
+```bash
+cargo fmt --all
+cargo check --workspace
+cargo clippy --workspace --all-targets
+cargo test -p ksp-app-config-desk
+cargo test -p ksp-config-lib
+cargo test -p ksp-logging-lib
+cargo tauri dev -c crates/ksp-app-config-desk/tauri.conf.json
+```
+
+Scénario fonctionnel :
+
+1. avec le profil actif actuel, lancer `tous` sur `ksp-app-config-desk.logging-test` + `config.logging_test` ;
+2. noter quels niveaux apparaissent en console et dans les file sinks ;
+3. modifier niveaux/targets/domains, **Sauvegarder et appliquer** ;
+4. relancer exactement le même test sans redémarrage ;
+5. vérifier que le routing suit immédiatement le nouveau runtime ;
+6. répéter avec **Log via bridge frontend** et vérifier le target `ksp-app-config-desk.frontend.main` / domain `frontend`.
+
+## Suite
+
+`0.1.4-pre.018` couvrira robustesse/extensibilité/tests desktop : source invalide au démarrage, audits, tests frontend/Tauri/build et ajout futur d'un `file_id`/editor sans refonte du shell.
diff --git a/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md b/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md
index 0ecb4bd..1b83d0e 100644
--- a/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md
+++ b/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md
@@ -1,5 +1,5 @@
-
+
# Plan `0.1.4` — `ksp-app-config-desk`
@@ -1247,13 +1247,13 @@ pre.015 Logging editor — mutations/persistence [réalisé]
- Recharger le document = resynchronisation du brouillon uniquement
- rollback source + ancien runtime conservé si l’application échoue
-pre.016 Logging runtime — compléments [en cours]
+pre.016 Logging runtime — compléments [réalisé]
- fichiers applicatifs uniques par lancement avec timestamp de démarrage
- sélection explicite du profil à appliquer indépendamment du default_profile
- observabilité metadata/génération du runtime
- consolidation des scénarios de rollback/runtime
-pre.017 panneau Test Logging
+pre.017 panneau Test Logging [en cours]
- champ message
- niveau trace/debug/info/warn/error/tous
- select target KSP contrôlé
diff --git a/logs_v0.1.4-pre.017.zip b/logs_v0.1.4-pre.017.zip
new file mode 100644
index 0000000..d520acc
Binary files /dev/null and b/logs_v0.1.4-pre.017.zip differ