diff --git a/Cargo.toml b/Cargo.toml
index 5116f35..75788f8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,12 +1,12 @@
# file: Cargo.toml
-# version: 89
+# version: 90
[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.17"
+version = "0.1.4-pre.18"
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 a5a262a..fa4ed97 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`
@@ -53,13 +53,16 @@ frontend/
│ ├── main.scss
│ └── splash.scss
├── ts/
+│ ├── documents.ts
│ ├── environment.ts
│ ├── frontend_log.ts
│ ├── invoke.ts
│ ├── logging.ts
+│ ├── logging_test.ts
│ ├── main.ts
│ ├── profiles.ts
│ ├── secret_reveal.ts
+│ ├── shell_registry.ts
│ └── splash.ts
├── main.html
└── splash.html
@@ -73,6 +76,14 @@ frontend/ts/bindings/ksp_app_config_desk/...
Les DTO Rust applicatifs restent la source de vérité et les bindings générés ne sont pas versionnés.
+## Robustesse desktop et extensibilité
+
+`pre.018` rend le fallback de bootstrap Logging testable avant installation du subscriber : la résolution produit d'abord un plan `managed` ou `fallback`, puis seulement l'initialisation runtime est tentée. Une source Logging invalide peut ainsi être couverte par test sans installer un subscriber global dans le processus de tests. Le fallback reste transitoire, console-only, niveau `Info`, sans file sink.
+
+Le frontend possède désormais `shell_registry.ts`. Le registre décrit les vues du shell et les adapters d'éditeurs spécialisés par `file_id`. Le panneau Documents reste générique ; lorsqu'un document possède un adapter enregistré, **Ouvrir l'éditeur spécialisé** déclenche la navigation par événement de registre. `cfg.std.logging -> Logging` est le premier adapter. Ajouter un futur éditeur ne demande donc pas de réécrire le moteur Documents ni la logique générale d'activation des panneaux.
+
+Deux audits d'intégration applicatifs complètent les audits Config/Logging existants : contrat Tauri/Vite/package scripts, centralisation des commandes Tauri, absence de `tauri-plugin-log`, interdiction des dialogues navigateur natifs et absence de stockage persistant pour le reveal Secret. Le script frontend `npm run check` exécute `tsc --noEmit` puis le build Vite.
+
## Artefacts frontend
Les artefacts frontend construits ne sont pas versionnés. `tauri.conf.json` fixe :
diff --git a/crates/ksp-app-config-desk/TODO.md b/crates/ksp-app-config-desk/TODO.md
index 711c096..38020aa 100644
--- a/crates/ksp-app-config-desk/TODO.md
+++ b/crates/ksp-app-config-desk/TODO.md
@@ -1,11 +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, les fichiers isolés par identité de lancement et le panneau Test Logging backend/frontend 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, le panneau Test Logging backend/frontend, le registre d'éditeurs spécialisés et les audits desktop de robustesse sont maintenant présents.
-Les prochaines tranches doivent encore :
+La clôture `pre.019` doit encore :
- ramener le target applicatif de `debug` à `info` ou `warn` avant clôture de la release ;
- é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 7da0364..12a5628 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`, une identité de lancement observable pour isoler les fichiers persistants et un panneau de test du routing backend/frontend.
+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. Le shell possède aussi un registre d'éditeurs spécialisés par `file_id`, utilisé aujourd'hui pour `cfg.std.logging`.
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.
@@ -155,6 +155,11 @@ La sélection charge le source brut via `ConfigManagement::read_source()`, y com
Les clics, sélections, chargements, remplacements DOM et sauvegardes sont tracés via le bridge frontend KSP sans journaliser le contenu du source.
+### Ouvrir un éditeur spécialisé depuis Documents
+
+Le panneau Documents demeure générique. Lorsqu'un `file_id` possède un adapter déclaré dans `shell_registry.ts`, le détail affiche **Ouvrir l'éditeur spécialisé**. Pour `cfg.std.logging`, ce bouton active la vue Logging sans dupliquer la lecture, la validation ou la persistence Config. Un document sans adapter spécialisé reste entièrement inspectable/réparable dans le panneau Documents et n'affiche pas ce bouton.
+
+
## Panneau Profils
La vue **Profils** charge l'inventaire des documents Config validés qui possèdent `default_profile` et `profiles`. Pour le document sélectionné :
@@ -277,3 +282,22 @@ Scénario attendu avec deux profils persistés `local_dev` et `local_test` :
Le bouton d'application explicite est désactivé lorsque le brouillon est sale afin qu'un profil affiché mais non persisté ne soit jamais confondu avec un profil réellement chargeable par Config.
Si la résolution ou la préparation du profil explicite échoue, `ksp_logging_lib::reinitialize()` ne remplace pas les layers actifs et la génération ne doit pas avancer. Une sauvegarde de document qui échoue pendant l'application runtime conserve également l'ancien runtime et restaure la source précédente lorsqu'elle avait été modifiée.
+
+## Validation desktop de robustesse
+
+Depuis `crates/ksp-app-config-desk`, le contrôle frontend explicite est :
+
+```bash
+npm run check
+```
+
+Il exécute `tsc --noEmit` puis `vite build`. La validation intégrée de la tranche inclut aussi :
+
+```bash
+cargo test -p ksp-app-config-desk
+cargo test -p ksp-config-lib
+cargo test -p ksp-logging-lib
+cargo tauri build -c crates/ksp-app-config-desk/tauri.conf.json
+```
+
+Un source Logging invalide au démarrage doit sélectionner le fallback transitoire sans persistence automatique. Après réparation du document, un nouveau lancement doit reprendre la configuration managée.
diff --git a/crates/ksp-app-config-desk/frontend/main.html b/crates/ksp-app-config-desk/frontend/main.html
index b1a0e7c..7f75d10 100644
--- a/crates/ksp-app-config-desk/frontend/main.html
+++ b/crates/ksp-app-config-desk/frontend/main.html
@@ -1,5 +1,5 @@
-
+
@@ -90,6 +90,7 @@
—
+
diff --git a/crates/ksp-app-config-desk/frontend/ts/documents.ts b/crates/ksp-app-config-desk/frontend/ts/documents.ts
index 79c9d89..1b829ff 100644
--- a/crates/ksp-app-config-desk/frontend/ts/documents.ts
+++ b/crates/ksp-app-config-desk/frontend/ts/documents.ts
@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/documents.ts
-// version: 1
+// version: 2
//! Documents panel backed exclusively by ksp-config-lib inventory, validation and management APIs.
@@ -13,6 +13,7 @@ import type { ConfigDocumentSaveResultDto } from "./bindings/ksp_app_config_desk
import type { ConfigDocumentSummaryDto } from "./bindings/ksp_app_config_desk/documents/ConfigDocumentSummaryDto";
import { frontendDebug, frontendTrace, frontendWarn } from "./frontend_log";
import { invokeKsp } from "./invoke";
+import { requestViewActivation, specializedEditorForFileId } from "./shell_registry";
interface DocumentsDataTable {
destroy(): unknown;
@@ -84,6 +85,12 @@ function renderDetail(detail: ConfigDocumentDetailDto): void {
if (saveButton) {
saveButton.disabled = detail.source === null;
}
+ const specializedEditor = specializedEditorForFileId(detail.summary.fileId);
+ const specializedButton = document.querySelector("#openSpecializedEditor");
+ if (specializedButton) {
+ specializedButton.hidden = specializedEditor === null;
+ specializedButton.textContent = specializedEditor?.label ?? "Ouvrir l'éditeur spécialisé";
+ }
const detailPanel = document.querySelector("#documentDetailPanel");
if (detailPanel) {
detailPanel.hidden = false;
@@ -270,6 +277,15 @@ function bindDocumentActions(): void {
frontendTrace("main", "Document save button clicked");
void saveActiveDocument();
});
+ document.querySelector("#openSpecializedEditor")?.addEventListener("click", () => {
+ const fileId = activeDocument?.summary.fileId ?? "";
+ const descriptor = specializedEditorForFileId(fileId);
+ if (!descriptor) {
+ return;
+ }
+ frontendDebug("main", "Specialized Config editor requested", { fileId, viewId: descriptor.viewId });
+ requestViewActivation(descriptor.viewId);
+ });
frontendTrace("main", "Documents 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 1b138fa..f2a30ff 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: 12
+// version: 13
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
@@ -12,60 +12,32 @@ import { initializeEnvironmentPanel } from "./environment";
import { initializeLoggingPanel } from "./logging";
import { initializeProfilesPanel } from "./profiles";
import { invokeKsp } from "./invoke";
+import { isViewId, registeredViewIds, viewRegistry, type ViewId } from "./shell_registry";
import { clearTransientSecretReveal } from "./secret_reveal";
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
installFrontendConsoleBridge("main");
-type ViewId = "overview" | "documents" | "profiles" | "environment" | "logging";
-
-const viewCopy: Record = {
- overview: {
- title: "Vue d'ensemble",
- description: "État sûr du bootstrap Config Desk et du runtime Logging actuellement actif.",
- },
- documents: {
- title: "Documents",
- description: "Inventaire, diagnostics et réparation validée des documents Config enregistrés.",
- },
- profiles: {
- title: "Profils",
- description: "Sélection default/explicite, vues global/profil/effective sûre et provenance Config.",
- },
- environment: {
- title: "Environnement / .env",
- description: "Rapport sûr desired/effective/source/shadow et management atomique des variables .env KSP/KSPB.",
- },
- logging: {
- title: "Logging",
- description: "Édition typée, hot reload runtime, profils actifs et panneau de test du routing Logging KSP.",
- },
-};
-
-function isViewId(value: string): value is ViewId {
- return Object.prototype.hasOwnProperty.call(viewCopy, value);
-}
-
-function activateView(viewId: ViewId, source: "startup" | "user"): void {
+function activateView(viewId: ViewId, source: "startup" | "user" | "registry"): void {
if (viewId !== "environment") {
clearTransientSecretReveal();
}
- if (source === "user") {
- frontendDebug("main", "Main navigation activated", { viewId });
+ if (source !== "startup") {
+ frontendDebug("main", "Main navigation activated", { viewId, source });
}
- const copy = viewCopy[viewId];
+ const descriptor = viewRegistry[viewId];
const title = document.querySelector("#viewTitle");
const headerTitle = document.querySelector("#headerViewTitle");
const description = document.querySelector("#viewDescription");
if (title) {
- title.textContent = copy.title;
+ title.textContent = descriptor.title;
}
if (headerTitle) {
- headerTitle.textContent = copy.title;
+ headerTitle.textContent = descriptor.title;
}
- document.title = `Config Desk — ${copy.title}`;
+ document.title = `Config Desk — ${descriptor.title}`;
if (description) {
- description.textContent = copy.description;
+ description.textContent = descriptor.description;
}
document.querySelectorAll("[data-view]").forEach(button => {
const active = button.dataset.view === viewId;
@@ -73,25 +45,11 @@ function activateView(viewId: ViewId, source: "startup" | "user"): void {
button.setAttribute("aria-current", active ? "page" : "false");
});
frontendTrace("main", "Main navigation tab state changed", { viewId, source });
- const overview = document.querySelector("#overviewPanel");
- if (overview) {
- overview.hidden = viewId !== "overview";
- }
- const documents = document.querySelector("#documentsPanel");
- if (documents) {
- documents.hidden = viewId !== "documents";
- }
- const profiles = document.querySelector("#profilesPanel");
- if (profiles) {
- profiles.hidden = viewId !== "profiles";
- }
- const environment = document.querySelector("#environmentPanel");
- if (environment) {
- environment.hidden = viewId !== "environment";
- }
- const logging = document.querySelector("#loggingPanel");
- if (logging) {
- logging.hidden = viewId !== "logging";
+ for (const registeredViewId of registeredViewIds()) {
+ const panel = document.querySelector(`#${viewRegistry[registeredViewId].panelId}`);
+ if (panel) {
+ panel.hidden = registeredViewId !== viewId;
+ }
}
frontendTrace("main", "Main view DOM updated", { viewId, source });
}
@@ -106,6 +64,13 @@ function bindNavigation(): void {
}
});
});
+ window.addEventListener("ksp:activate-view", event => {
+ const detail = event instanceof CustomEvent ? event.detail : null;
+ const requestedView = typeof detail === "object" && detail !== null && "viewId" in detail ? String(detail.viewId) : "";
+ if (isViewId(requestedView)) {
+ activateView(requestedView, "registry");
+ }
+ });
frontendTrace("main", "Main navigation handlers installed");
}
diff --git a/crates/ksp-app-config-desk/frontend/ts/shell_registry.ts b/crates/ksp-app-config-desk/frontend/ts/shell_registry.ts
new file mode 100644
index 0000000..1bfdc5c
--- /dev/null
+++ b/crates/ksp-app-config-desk/frontend/ts/shell_registry.ts
@@ -0,0 +1,70 @@
+// file: crates/ksp-app-config-desk/frontend/ts/shell_registry.ts
+// version: 1
+
+//! Declarative shell and specialized-editor registry for Config Desk frontend extensibility.
+
+export type ViewId = "overview" | "documents" | "profiles" | "environment" | "logging";
+
+export interface ViewDescriptor {
+ title: string;
+ description: string;
+ panelId: string;
+}
+
+export interface SpecializedEditorDescriptor {
+ fileId: string;
+ viewId: ViewId;
+ label: string;
+}
+
+export const viewRegistry: Record = {
+ overview: {
+ title: "Vue d'ensemble",
+ description: "État sûr du bootstrap Config Desk et du runtime Logging actuellement actif.",
+ panelId: "overviewPanel",
+ },
+ documents: {
+ title: "Documents",
+ description: "Inventaire, diagnostics et réparation validée des documents Config enregistrés.",
+ panelId: "documentsPanel",
+ },
+ profiles: {
+ title: "Profils",
+ description: "Sélection default/explicite, vues global/profil/effective sûre et provenance Config.",
+ panelId: "profilesPanel",
+ },
+ environment: {
+ title: "Environnement / .env",
+ description: "Rapport sûr desired/effective/source/shadow et management atomique des variables .env KSP/KSPB.",
+ panelId: "environmentPanel",
+ },
+ logging: {
+ title: "Logging",
+ description: "Édition typée, hot reload runtime, profils actifs et panneau de test du routing Logging KSP.",
+ panelId: "loggingPanel",
+ },
+};
+
+const specializedEditorRegistry: SpecializedEditorDescriptor[] = [
+ {
+ fileId: "cfg.std.logging",
+ viewId: "logging",
+ label: "Ouvrir l'éditeur Logging",
+ },
+];
+
+export function registeredViewIds(): ViewId[] {
+ return Object.keys(viewRegistry) as ViewId[];
+}
+
+export function isViewId(value: string): value is ViewId {
+ return Object.prototype.hasOwnProperty.call(viewRegistry, value);
+}
+
+export function specializedEditorForFileId(fileId: string): SpecializedEditorDescriptor | null {
+ return specializedEditorRegistry.find(descriptor => descriptor.fileId === fileId) ?? null;
+}
+
+export function requestViewActivation(viewId: ViewId): void {
+ window.dispatchEvent(new CustomEvent("ksp:activate-view", { detail: { viewId } }));
+}
diff --git a/crates/ksp-app-config-desk/package.json b/crates/ksp-app-config-desk/package.json
index 7ea041b..b96a0ec 100644
--- a/crates/ksp-app-config-desk/package.json
+++ b/crates/ksp-app-config-desk/package.json
@@ -7,7 +7,8 @@
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
- "tauri": "tauri"
+ "tauri": "tauri",
+ "check": "tsc --noEmit && vite build"
},
"dependencies": {
"@fltsci/tauri-plugin-tracing": "^0.3",
diff --git a/crates/ksp-app-config-desk/src/bootstrap.rs b/crates/ksp-app-config-desk/src/bootstrap.rs
index 21d64c7..fb620c9 100644
--- a/crates/ksp-app-config-desk/src/bootstrap.rs
+++ b/crates/ksp-app-config-desk/src/bootstrap.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/bootstrap.rs
-// version: 2
+// version: 3
//! Config and Logging bootstrap for the desktop application.
@@ -11,6 +11,18 @@ pub(crate) struct LoggingStartup {
pub(crate) startup_diagnostic: std::option::Option,
}
+enum LoggingStartupPlan {
+ Managed {
+ active_profile_id: String,
+ settings: ksp_logging_lib::LoggingSettings,
+ },
+ Fallback {
+ initial_error: ksp_core_lib::Error,
+ diagnostic: crate::CommandErrorDto,
+ settings: ksp_logging_lib::LoggingSettings,
+ },
+}
+
pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result {
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
let bootstrap = match bootstrap {
@@ -30,37 +42,52 @@ pub(crate) fn initialize_logging(
management: &ksp_config_lib::ConfigManagement,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result {
+ let plan = resolve_logging_startup(management);
+ return match plan {
+ LoggingStartupPlan::Managed { active_profile_id, settings } => {
+ let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
+ match guard {
+ std::result::Result::Ok(guard) => {
+ ksp_logging_lib::info!(
+ target: crate::TRACING_TARGET,
+ domain = crate::TRACING_DOMAIN_BOOTSTRAP,
+ active_profile = active_profile_id.as_str(),
+ "initialized Config Desk logging from managed configuration"
+ );
+ std::result::Result::Ok(LoggingStartup {
+ guard,
+ active_profile_id: std::option::Option::Some(active_profile_id),
+ selection_source: "default_profile".to_owned(),
+ fallback_active: false,
+ startup_diagnostic: std::option::Option::None,
+ })
+ },
+ std::result::Result::Err(error) => initialize_fallback_logging(error, runtime_identity),
+ }
+ },
+ LoggingStartupPlan::Fallback { initial_error, diagnostic, settings } => {
+ initialize_planned_fallback_logging(initial_error, diagnostic, settings, runtime_identity)
+ },
+ };
+}
+
+fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
- std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity),
+ std::result::Result::Err(error) => return fallback_startup_plan(error),
};
let resolved = management.engine().load_resolved_logging_config(std::option::Option::None, &environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
- std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity),
- };
- let active_profile_id = resolved.profile_id().to_owned();
- let settings = resolved.into_settings();
- let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
- return match guard {
- std::result::Result::Ok(guard) => {
- ksp_logging_lib::info!(
- target: crate::TRACING_TARGET,
- domain = crate::TRACING_DOMAIN_BOOTSTRAP,
- active_profile = active_profile_id.as_str(),
- "initialized Config Desk logging from managed configuration"
- );
- std::result::Result::Ok(LoggingStartup {
- guard,
- active_profile_id: std::option::Option::Some(active_profile_id),
- selection_source: "default_profile".to_owned(),
- fallback_active: false,
- startup_diagnostic: std::option::Option::None,
- })
- },
- std::result::Result::Err(error) => initialize_fallback_logging(error, runtime_identity),
+ std::result::Result::Err(error) => return fallback_startup_plan(error),
};
+ return LoggingStartupPlan::Managed { active_profile_id: resolved.profile_id().to_owned(), settings: resolved.into_settings() };
+}
+
+fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPlan {
+ let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
+ return LoggingStartupPlan::Fallback { initial_error, diagnostic, settings: fallback_logging_settings() };
}
fn initialize_fallback_logging(
@@ -68,7 +95,15 @@ fn initialize_fallback_logging(
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result {
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
- let settings = fallback_logging_settings();
+ return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity);
+}
+
+fn initialize_planned_fallback_logging(
+ initial_error: ksp_core_lib::Error,
+ diagnostic: crate::CommandErrorDto,
+ settings: ksp_logging_lib::LoggingSettings,
+ runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
+) -> ksp_core_lib::Result {
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
let guard = match guard {
std::result::Result::Ok(value) => value,
diff --git a/crates/ksp-app-config-desk/tests/desktop_contract.rs b/crates/ksp-app-config-desk/tests/desktop_contract.rs
new file mode 100644
index 0000000..43fdafe
--- /dev/null
+++ b/crates/ksp-app-config-desk/tests/desktop_contract.rs
@@ -0,0 +1,72 @@
+// file: crates/ksp-app-config-desk/tests/desktop_contract.rs
+// version: 1
+
+//! Desktop build/shell contract audits for Config Desk.
+
+fn app_root() -> std::path::PathBuf {
+ return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+}
+
+fn read_text(path: &std::path::Path) -> String {
+ let source = std::fs::read_to_string(path);
+ assert!(source.is_ok(), "unable to read {}", path.display());
+ return match source {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => String::new(),
+ };
+}
+
+fn read_json(path: &std::path::Path) -> serde_json::Value {
+ let source = read_text(path);
+ let parsed = serde_json::from_str::(source.as_str());
+ assert!(parsed.is_ok(), "unable to parse {}", path.display());
+ return match parsed {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => serde_json::Value::Null,
+ };
+}
+
+#[test]
+fn tauri_and_frontend_build_contracts_remain_explicit() {
+ let root = app_root();
+ let tauri = read_json(root.join("tauri.conf.json").as_path());
+ assert_eq!(tauri.pointer("/build/devUrl").and_then(serde_json::Value::as_str), std::option::Option::Some("http://localhost:1430"));
+ assert_eq!(tauri.pointer("/build/beforeDevCommand").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run dev"));
+ assert_eq!(tauri.pointer("/build/beforeBuildCommand").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run build"));
+ let windows = tauri.pointer("/app/windows").and_then(serde_json::Value::as_array);
+ assert!(windows.is_some());
+ let windows = match windows {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => return,
+ };
+ assert_eq!(windows.len(), 2);
+ let labels = windows.iter().filter_map(|window| window.get("label").and_then(serde_json::Value::as_str)).collect::>();
+ assert_eq!(labels, ["splash", "main"]);
+ let main = windows.iter().find(|window| window.get("label").and_then(serde_json::Value::as_str) == std::option::Option::Some("main"));
+ assert!(main.is_some());
+ if let std::option::Option::Some(main) = main {
+ assert_eq!(main.get("visible").and_then(serde_json::Value::as_bool), std::option::Option::Some(false));
+ }
+ let package = read_json(root.join("package.json").as_path());
+ let build = package.pointer("/scripts/build").and_then(serde_json::Value::as_str);
+ let check = package.pointer("/scripts/check").and_then(serde_json::Value::as_str);
+ assert!(build.is_some_and(|value| value.contains("tsc") && value.contains("vite build")));
+ assert!(check.is_some_and(|value| value.contains("tsc --noEmit") && value.contains("vite build")));
+}
+
+#[test]
+fn shell_registry_owns_view_activation_and_specialized_editor_dispatch() {
+ let root = app_root();
+ let registry = read_text(root.join("frontend/ts/shell_registry.ts").as_path());
+ let main = read_text(root.join("frontend/ts/main.ts").as_path());
+ let documents = read_text(root.join("frontend/ts/documents.ts").as_path());
+ let html = read_text(root.join("frontend/main.html").as_path());
+ assert!(registry.contains("export const viewRegistry"));
+ assert!(registry.contains("fileId: \"cfg.std.logging\""));
+ assert!(registry.contains("viewId: \"logging\""));
+ assert!(main.contains("registeredViewIds()"));
+ assert!(main.contains("ksp:activate-view"));
+ assert!(documents.contains("specializedEditorForFileId"));
+ assert!(documents.contains("requestViewActivation"));
+ assert!(html.contains("id=\"openSpecializedEditor\""));
+}
diff --git a/crates/ksp-app-config-desk/tests/desktop_security.rs b/crates/ksp-app-config-desk/tests/desktop_security.rs
new file mode 100644
index 0000000..91663f5
--- /dev/null
+++ b/crates/ksp-app-config-desk/tests/desktop_security.rs
@@ -0,0 +1,80 @@
+// file: crates/ksp-app-config-desk/tests/desktop_security.rs
+// version: 1
+
+//! Desktop security/ownership regression audits local to Config Desk.
+
+fn app_root() -> std::path::PathBuf {
+ return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+}
+
+fn collect_files(directory: &std::path::Path, extension: &str, files: &mut std::vec::Vec) {
+ let entries = std::fs::read_dir(directory);
+ let entries = match entries {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => return,
+ };
+ for entry in entries {
+ let entry = match entry {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => continue,
+ };
+ let path = entry.path();
+ if path.is_dir() {
+ collect_files(path.as_path(), extension, files);
+ continue;
+ }
+ if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some(extension) {
+ files.push(path);
+ }
+ }
+}
+
+fn read_text(path: &std::path::Path) -> String {
+ let source = std::fs::read_to_string(path);
+ assert!(source.is_ok(), "unable to read {}", path.display());
+ return match source {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => String::new(),
+ };
+}
+
+#[test]
+fn frontend_avoids_native_dialogs_and_persistent_secret_storage() {
+ let root = app_root();
+ let mut typescript = std::vec::Vec::new();
+ collect_files(root.join("frontend/ts").as_path(), "ts", &mut typescript);
+ assert!(!typescript.is_empty());
+ for path in typescript {
+ let source = read_text(path.as_path());
+ for forbidden in ["window.alert(", "window.confirm(", "window.prompt("] {
+ assert!(!source.contains(forbidden), "{} uses forbidden browser-native dialog {forbidden}", path.display());
+ }
+ }
+ let secret_reveal = read_text(root.join("frontend/ts/secret_reveal.ts").as_path());
+ assert!(!secret_reveal.contains("localStorage"));
+ assert!(!secret_reveal.contains("sessionStorage"));
+ assert!(secret_reveal.contains("clearTransientSecretReveal"));
+}
+
+#[test]
+fn tauri_commands_and_logging_adapters_remain_on_owned_boundaries() {
+ let root = app_root();
+ let manifest = read_text(root.join("Cargo.toml").as_path());
+ assert!(manifest.contains("ksp-logging-lib"));
+ assert!(manifest.contains("tauri-plugin-tracing"));
+ assert!(!manifest.contains("tauri-plugin-log"));
+ let source_root = root.join("src");
+ let mut rust_files = std::vec::Vec::new();
+ collect_files(source_root.as_path(), "rs", &mut rust_files);
+ let mut command_attribute_count = 0usize;
+ for path in rust_files {
+ let source = read_text(path.as_path());
+ let count = source.matches("#[tauri::command]").count();
+ if path.file_name().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("tauri.rs") {
+ command_attribute_count += count;
+ } else {
+ assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display());
+ }
+ }
+ assert!(command_attribute_count > 0);
+}
diff --git a/crates/ksp-app-config-desk/unit_tests/bootstrap.rs b/crates/ksp-app-config-desk/unit_tests/bootstrap.rs
index 6448663..1ae41ea 100644
--- a/crates/ksp-app-config-desk/unit_tests/bootstrap.rs
+++ b/crates/ksp-app-config-desk/unit_tests/bootstrap.rs
@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/bootstrap.rs
-// version: 1
+// version: 2
#[test]
fn fallback_logging_is_console_only_and_bounded_to_info() {
@@ -15,3 +15,103 @@ fn fallback_logging_is_console_only_and_bounded_to_info() {
assert_eq!(console.format(), ksp_logging_lib::LogFormat::Human);
}
}
+
+#[test]
+fn invalid_logging_source_is_planned_as_transient_fallback_before_subscriber_installation() {
+ let fixture = BootstrapFixture::new("invalid-source");
+ assert!(fixture.is_ok());
+ let fixture = match fixture {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => return,
+ };
+ let malformed = std::fs::write(fixture.config_root.join(ksp_config_lib::DEFAULT_STD_LOGGING_FILENAME), "{ invalid json");
+ assert!(malformed.is_ok());
+ let management = fixture.management();
+ assert!(management.is_ok());
+ let management = match management {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(_) => return,
+ };
+ let plan = super::resolve_logging_startup(&management);
+ let fallback = matches!(&plan, super::LoggingStartupPlan::Fallback { .. });
+ assert!(fallback, "invalid Logging source must produce a fallback startup plan");
+ if let super::LoggingStartupPlan::Fallback { diagnostic, settings, .. } = plan {
+ assert_eq!(settings.default_filter(), ksp_logging_lib::LogFilterLevel::Info);
+ assert!(settings.files().is_empty());
+ assert!(!diagnostic.domain.is_empty());
+ assert!(!diagnostic.code.is_empty());
+ assert!(!diagnostic.message.is_empty());
+ }
+}
+
+struct BootstrapFixture {
+ root: std::path::PathBuf,
+ config_root: std::path::PathBuf,
+ schema_root: std::path::PathBuf,
+}
+
+impl BootstrapFixture {
+ fn new(label: &str) -> ksp_core_lib::Result {
+ let root = std::env::temp_dir().join(std::format!("ksp-config-desk-{label}-{}", std::process::id()));
+ let cleanup = std::fs::remove_dir_all(root.as_path());
+ match cleanup {
+ std::result::Result::Ok(()) | std::result::Result::Err(_) => {},
+ }
+ let config_root = root.join("config");
+ let schema_root = root.join("schemas");
+ let create_config = std::fs::create_dir_all(config_root.as_path());
+ if let std::result::Result::Err(error) = create_config {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Cannot create bootstrap test config root").with_source(error),
+ );
+ }
+ let create_schema = std::fs::create_dir_all(schema_root.as_path());
+ if let std::result::Result::Err(error) = create_schema {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Cannot create bootstrap test schema root").with_source(error),
+ );
+ }
+ let workspace = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ let workspace = match workspace.parent().and_then(std::path::Path::parent) {
+ std::option::Option::Some(value) => value,
+ std::option::Option::None => {
+ return std::result::Result::Err(ksp_core_lib::Error::new(
+ crate::ERROR_CODE_APP_STATE_INVALID,
+ "Cannot resolve workspace root for bootstrap test",
+ ));
+ },
+ };
+ let source_schema = workspace.join(ksp_config_lib::DEFAULT_SCHEMA_PATH).join(ksp_config_lib::DEFAULT_STD_LOGGING_SCHEMA_FILENAME);
+ let destination_schema = schema_root.join(ksp_config_lib::DEFAULT_STD_LOGGING_SCHEMA_FILENAME);
+ let copied = std::fs::copy(source_schema.as_path(), destination_schema.as_path());
+ if let std::result::Result::Err(error) = copied {
+ return std::result::Result::Err(
+ ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Cannot copy bootstrap test Logging schema").with_source(error),
+ );
+ }
+ return std::result::Result::Ok(Self { root, config_root, schema_root });
+ }
+
+ fn management(&self) -> ksp_core_lib::Result {
+ let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_paths(self.config_root.as_path(), self.schema_root.as_path());
+ let bootstrap = match bootstrap {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ let registry = ksp_config_lib::ConfigFileRegistry::defaults();
+ let registry = match registry {
+ std::result::Result::Ok(value) => value,
+ std::result::Result::Err(error) => return std::result::Result::Err(error),
+ };
+ return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry)));
+ }
+}
+
+impl std::ops::Drop for BootstrapFixture {
+ fn drop(&mut self) {
+ let cleanup = std::fs::remove_dir_all(self.root.as_path());
+ match cleanup {
+ std::result::Result::Ok(()) | std::result::Result::Err(_) => {},
+ }
+ }
+}
diff --git a/deltas/0.1.4/pre.018.md b/deltas/0.1.4/pre.018.md
new file mode 100644
index 0000000..bd6cb42
--- /dev/null
+++ b/deltas/0.1.4/pre.018.md
@@ -0,0 +1,105 @@
+
+
+
+# Delta `0.1.4-pre.018` — robustesse, extensibilité et tests desktop
+
+## Base
+
+`0.1.4-pre.017` est validée : `cargo fmt --all`, `cargo check --workspace` et `cargo clippy --workspace --all-targets` sont propres ; 62 tests unitaires Config Desk, 88 tests Config, 37 tests unitaires Logging et tous les audits/tests d'intégration associés passent. Le scénario runtime montre également le routing `TRACE` backend puis frontend après hot reload.
+
+## Objectif
+
+Durcir Config Desk avant la tranche de clôture : tester explicitement le comportement de démarrage sur source Logging invalide, matérialiser le registre d'éditeurs spécialisés prévu par le plan, et ajouter des audits desktop automatisés sur Tauri, le frontend, les secrets et les frontières d'ownership.
+
+## Changements
+
+### Bootstrap Logging testable avant installation
+
+Le bootstrap sépare désormais la résolution d'un `LoggingStartupPlan` de l'installation du subscriber :
+
+- `Managed` contient le profil résolu et les `LoggingSettings` ;
+- `Fallback` contient le diagnostic sûr, l'erreur initiale et les settings transitoires.
+
+L'installation runtime intervient seulement après cette décision. Un test peut donc fournir un source Logging invalide et vérifier la sélection du fallback sans installer un subscriber global dans le processus de tests.
+
+Le fallback reste inchangé fonctionnellement : console stderr, niveau `Info`, aucun fichier et spans désactivés.
+
+### Registre shell / éditeurs spécialisés
+
+Nouveau `frontend/ts/shell_registry.ts` :
+
+- registre unique des cinq vues du shell ;
+- activation générique des panels par `panelId` ;
+- registre séparé `file_id -> éditeur spécialisé` ;
+- premier adapter `cfg.std.logging -> logging` ;
+- événement `ksp:activate-view` pour demander une navigation sans coupler Documents à l'implémentation de Logging.
+
+Le panneau Documents affiche **Ouvrir l'éditeur Logging** uniquement lorsqu'un adapter spécialisé existe pour le `file_id` sélectionné. Les documents inconnus du registre spécialisé restent inspectables et réparables via le shell générique.
+
+### Audits desktop
+
+Deux nouveaux tests d'intégration Config Desk couvrent :
+
+- contrat Tauri `splash/main`, main initialement caché et commandes Vite explicites ;
+- scripts frontend `build` et `check` ;
+- présence du registre shell et dispatch d'éditeur spécialisé ;
+- absence de `window.alert`, `window.confirm`, `window.prompt` ;
+- absence de `localStorage` / `sessionStorage` dans le reveal Secret ;
+- centralisation de tous les `#[tauri::command]` dans `tauri.rs` ;
+- présence de `ksp-logging-lib` + `tauri-plugin-tracing` et absence de `tauri-plugin-log`.
+
+Les audits Config ownership et Logging ownership existants restent les autorités workspace pour les frontières Config/tracing.
+
+### Frontend build
+
+`package.json` ajoute :
+
+```text
+npm run check
+```
+
+qui exécute `tsc --noEmit && vite build`.
+
+## Version technique
+
+```text
+workspace.package.version = 0.1.4-pre.18
+```
+
+## Validation attendue
+
+Depuis la racine du workspace :
+
+```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
+```
+
+Depuis `crates/ksp-app-config-desk` :
+
+```bash
+npm run check
+```
+
+Puis depuis la racine :
+
+```bash
+cargo tauri build -c crates/ksp-app-config-desk/tauri.conf.json
+cargo tauri dev -c crates/ksp-app-config-desk/tauri.conf.json
+```
+
+Parcours fonctionnel minimal :
+
+1. Documents -> sélectionner `cfg.std.logging` ;
+2. vérifier **Ouvrir l'éditeur Logging** ;
+3. cliquer et vérifier l'activation de la vue Logging ;
+4. revenir à Documents et confirmer que diagnostics/source restent inchangés ;
+5. vérifier le comportement Logging déjà validé (profil runtime, hot reload, Test Logging) après ce refactor de shell.
+
+## Suite
+
+`0.1.4-pre.019` est la tranche de clôture : validation workspace/build complète, matrice fonctionnelle finale, retour du target applicatif de `debug` vers `info`/`warn`, README/USAGE/TODO/changelog, nettoyage/archivage et préparation de `rel.001` ainsi que du prompt de session suivant.
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 1b83d0e..ddfd501 100644
--- a/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md
+++ b/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md
@@ -1023,6 +1023,12 @@ DTO UI
Aucun nouvel éditeur ne reçoit une API de filesystem directe ni ne transporte son propre validateur autoritatif.
+### 15.4 Implémentation `pre.018`
+
+Le shell applicatif matérialise désormais cette architecture dans `frontend/ts/shell_registry.ts` : les vues sont décrites dans un registre unique et les adapters spécialisés sont déclarés séparément par `file_id`. Le panneau Documents interroge ce registre pour afficher ou non **Ouvrir l'éditeur spécialisé** ; il ne contient aucune branche métier Logging. Le premier mapping est `cfg.std.logging -> logging`.
+
+Le bootstrap Logging sépare également la **résolution du plan de démarrage** de l'**installation du subscriber**. Une source invalide peut ainsi être testée comme plan fallback sans initialiser le subscriber global du binaire de tests.
+
## 16. Stratégie de tests
### 16.1 `ksp-config-lib`
@@ -1253,7 +1259,7 @@ pre.016 Logging runtime — compléments [réalisé]
- observabilité metadata/génération du runtime
- consolidation des scénarios de rollback/runtime
-pre.017 panneau Test Logging [en cours]
+pre.017 panneau Test Logging [réalisé]
- champ message
- niveau trace/debug/info/warn/error/tous
- select target KSP contrôlé
@@ -1262,7 +1268,7 @@ pre.017 panneau Test Logging [en cours]
- preuve routing avant/après hot reload
- preuve bridge frontend
-pre.018 robustesse/extensibilité/tests desktop
+pre.018 robustesse/extensibilité/tests desktop [en cours]
- invalid source au démarrage
- audits secrets/Config ownership/tracing
- tests frontend/Tauri/build