v0.1.4-pre.015

This commit is contained in:
2026-08-16 17:38:23 +02:00
parent 789ffb16ce
commit d7185905d8
17 changed files with 1124 additions and 305 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 83 # version: 84
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"] members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"]
[workspace.package] [workspace.package]
version = "0.1.4-pre.14" version = "0.1.4-pre.15"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,79 +1,77 @@
{ {
"format_version": 1, "default_profile": "local_dev",
"logs_directory": "${KSP_LOGS_DIRECTORY:-logs}", "format_version": 1,
"default_profile": "local_dev", "logs_directory": "${KSP_LOGS_DIRECTORY:-logs}",
"profiles": [ "profiles": [
{
"console": {
"ansi": true,
"enabled": true,
"filter": {
"domains": [
"*"
],
"level": "debug",
"targets": [
"*"
]
},
"format": "compact",
"output": "stderr"
},
"default_filter": "info",
"files": [
{ {
"profile_id": "local_dev", "ansi": false,
"default_filter": "warn", "enabled": true,
"span_events": "new_and_close", "filter": {
"console": { "domains": [
"enabled": true, "*"
"output": "stderr",
"ansi": true,
"format": "compact",
"filter": {
"level": "debug",
"targets": [
"*"
],
"domains": [
"*"
]
}
},
"files": [
{
"output_id": "file.all.debug",
"enabled": true,
"path": "debug/ksp-debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"filter": {
"level": "debug",
"targets": [
"*"
],
"domains": [
"*"
]
}
},
{
"output_id": "file.config.error",
"enabled": true,
"path": "config/ksp-config-errors.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"filter": {
"level": "error",
"targets": [
"ksp-config-lib"
],
"domains": [
"config"
]
}
}
], ],
"target_filters": [ "level": "debug",
{ "targets": [
"target_prefix": "ksp-config-lib", "*"
"level": "trace" ]
}, },
{ "format": "human",
"target_prefix": "ksp-logging-lib", "output_id": "file.all.debug",
"level": "debug" "path": "debug/ksp-debug.log",
}, "rotation": "daily"
{ },
"target_prefix": "ksp-app-config-desk", {
"level": "debug" "ansi": false,
} "enabled": true,
] "filter": {
"domains": [
"config"
],
"level": "error",
"targets": [
"ksp-config-lib"
]
},
"format": "json",
"output_id": "file.config.error",
"path": "config/ksp-config-errors.jsonl",
"rotation": "daily"
} }
] ],
"profile_id": "local_dev",
"span_events": "off",
"target_filters": [
{
"level": "trace",
"target_prefix": "ksp-config-lib"
},
{
"level": "debug",
"target_prefix": "ksp-logging-lib"
},
{
"level": "debug",
"target_prefix": "ksp-app-config-desk"
}
]
}
]
} }

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/README.md --> <!-- file: crates/ksp-app-config-desk/README.md -->
<!-- version: 15 --> <!-- version: 16 -->
# `ksp-app-config-desk` # `ksp-app-config-desk`
@@ -29,7 +29,7 @@ Le gabarit desktop actif fournit maintenant :
- le panneau Documents générique alimenté par le registre `ksp-config-lib`, avec diagnostics backend et réparation validée ; - le panneau Documents générique alimenté par le registre `ksp-config-lib`, avec diagnostics backend et réparation validée ;
- le panneau Profils générique pour `default_profile`, sélection explicite, vues global/profil/effective sûre et provenance ; - le panneau Profils générique pour `default_profile`, sélection explicite, vues global/profil/effective sûre et provenance ;
- le panneau Environnement alimenté uniquement par `ConfigManagement`, avec rapport desired/effective sûr, source/shadowing, management atomique `.env` et reveal Secret privilégié/transitoire ; - le panneau Environnement alimenté uniquement par `ConfigManagement`, avec rapport desired/effective sûr, source/shadowing, management atomique `.env` et reveal Secret privilégié/transitoire ;
- le panneau Logging en lecture typée, alimenté par `ConfigManagement::load_logging_document()` et projetant profils, console, fichiers, filtres, targets et domains sans parsing frontend ; - le panneau Logging typé, alimenté par `ConfigManagement::load_logging_document()` / `save_logging_document()`, avec brouillon éditable, profils, console, fichiers, filtres, targets et domains sans parsing JSON frontend ;
- `tauri-plugin-tracing` côté Rust et `@fltsci/tauri-plugin-tracing` côté frontend ; - `tauri-plugin-tracing` côté Rust et `@fltsci/tauri-plugin-tracing` côté frontend ;
- les ports dédiés `1430` pour Vite HTTP et `1431` pour le WebSocket de développement ; - les ports dédiés `1430` pour Vite HTTP et `1431` pour le WebSocket de développement ;
- la destination frontend externe commune à Tauri et Vite; - la destination frontend externe commune à Tauri et Vite;
@@ -132,7 +132,7 @@ KSP_DESK_SPLASH_FADE_MS=300
Après la readiness frontend, Rust émet `fade_in`, attend la durée minimale, émet `fade_out`, attend la durée de fade puis `tw_main.rs` affiche/focalise `main` avant destruction du splash. Le backend journalise en `debug` les valeurs résolues, leur provenance (`process`, `dotenv` ou `fallback`), chaque attente réellement observée et la durée totale. Avec `12000/3000`, la durée backend attendue entre readiness et activation de `main` est donc d'environ `15000 ms`. Une valeur splash invalide ne bloque pas le manager : des timings de fallback sûrs restent en mémoire afin que la future surface `.env` puisse permettre la réparation. Après la readiness frontend, Rust émet `fade_in`, attend la durée minimale, émet `fade_out`, attend la durée de fade puis `tw_main.rs` affiche/focalise `main` avant destruction du splash. Le backend journalise en `debug` les valeurs résolues, leur provenance (`process`, `dotenv` ou `fallback`), chaque attente réellement observée et la durée totale. Avec `12000/3000`, la durée backend attendue entre readiness et activation de `main` est donc d'environ `15000 ms`. Une valeur splash invalide ne bloque pas le manager : des timings de fallback sûrs restent en mémoire afin que la future surface `.env` puisse permettre la réparation.
Le shell principal expose les cinq routes de référence `Vue d'ensemble`, `Documents`, `Profils`, `Environnement / .env` et `Logging`. Le logo porte déjà l'identité KSP ; le texte du header suit donc la forme `Config Desk — <vue active>` au lieu de répéter `KSP`. Les quelques commandes principales restent des pills/tabs à droite ; un dropdown sera préféré lorsqu'une application possède trop de commandes pour conserver ce format lisible. La route Logging expose maintenant la lecture typée du document et de ses profils ; les mutations/persistence et le hot reload restent réservés aux prereleases suivantes. Le shell principal expose les cinq routes de référence `Vue d'ensemble`, `Documents`, `Profils`, `Environnement / .env` et `Logging`. Le logo porte déjà l'identité KSP ; le texte du header suit donc la forme `Config Desk — <vue active>` au lieu de répéter `KSP`. Les quelques commandes principales restent des pills/tabs à droite ; un dropdown sera préféré lorsqu'une application possède trop de commandes pour conserver ce format lisible. La route Logging expose maintenant un brouillon typé éditable et sa persistence atomique ; le hot reload runtime reste réservé à `pre.016`.
## Panneau Profils ## Panneau Profils
@@ -141,11 +141,11 @@ La vue **Profils** inspecte les documents validés qui exposent le contrat stand
L'inspection expose le profil par défaut ou une sélection explicite, les vues source `globals` et `profile`, ainsi que l'effective après résolution environnementale. Cette dernière est toujours sérialisée depuis `ResolvedConfigJson::safe_value()` afin qu'une substitution Secret soit redacted. La provenance top-level distingue `global` / `profile`; la provenance environnement n'expose que JSON Pointer, nom de variable, source `process`/`.env`/fallback et sensibilité, jamais la valeur résolue. L'inspection expose le profil par défaut ou une sélection explicite, les vues source `globals` et `profile`, ainsi que l'effective après résolution environnementale. Cette dernière est toujours sérialisée depuis `ResolvedConfigJson::safe_value()` afin qu'une substitution Secret soit redacted. La provenance top-level distingue `global` / `profile`; la provenance environnement n'expose que JSON Pointer, nom de variable, source `process`/`.env`/fallback et sensibilité, jamais la valeur résolue.
## Panneau Logging — lecture typée ## Panneau Logging — édition typée et persistence
La vue **Logging** charge le document standard exclusivement avec `ConfigManagement::load_logging_document()`. Rust mappe ensuite les types publics `LoggingConfigDocument`, `LoggingProfileConfig`, `LoggingConsoleConfig`, `LoggingFileConfig`, `LoggingOutputFilterConfig` et `LoggingTargetFilterConfig` vers des DTO TS-RS dédiés. Le frontend ne parse donc ni le JSON source ni son schema. La vue **Logging** charge le document standard exclusivement avec `ConfigManagement::load_logging_document()`. Rust mappe ensuite les types publics `LoggingConfigDocument`, `LoggingProfileConfig`, `LoggingConsoleConfig`, `LoggingFileConfig`, `LoggingOutputFilterConfig` et `LoggingTargetFilterConfig` vers des DTO TS-RS dédiés. Le frontend ne parse donc ni le JSON source ni son schema.
La tranche de lecture expose `format_version`, `logs_directory`, `default_profile`, tous les profils, la console, les fichiers persistants, les filtres locaux, les target overrides et les listes de targets/domains. La sélection d'un profil ne modifie rien : `pre.014` est strictement read-only. Les mutations typées et la persistence via `save_logging_document()` appartiennent à `pre.015`. Le panneau expose `format_version`, `logs_directory`, `default_profile`, tous les profils, la console, les fichiers persistants, les filtres locaux, les target overrides et les listes de targets/domains. En `pre.015`, le frontend maintient un brouillon typé : create/clone/rename/delete de profils, 0/1/N file sinks et target filters restent locaux jusqu'à **Sauvegarder**. Le backend reconstruit les types publics Config et appelle `ConfigManagement::save_logging_document()`, qui valide la totalité du candidat avant remplacement atomique. Le runtime actif n'est pas reconfiguré avant `pre.016`.
## Traçabilité frontend ## Traçabilité frontend

View File

@@ -1,13 +1,13 @@
<!-- file: crates/ksp-app-config-desk/TODO.md --> <!-- file: crates/ksp-app-config-desk/TODO.md -->
<!-- version: 13 --> <!-- version: 14 -->
# TODO `ksp-app-config-desk` # 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 la lecture typée du document Logging 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 sont maintenant présents.
Les prochaines tranches doivent encore : Les prochaines tranches doivent encore :
- ajouter les mutations/persistence du Logging editor puis son hot reload et son panneau de test ; - ajouter le hot reload du Logging editor puis son panneau de test ;
- faire évoluer le runtime Logging vers des fichiers applicatifs uniques par lancement (`app-name.<start-timestamp>.<ext>`) ; - faire évoluer le runtime Logging vers des fichiers applicatifs uniques par lancement (`app-name.<start-timestamp>.<ext>`) ;
- ramener le target applicatif de `debug` à `info` ou `warn` avant clôture de la release ; - ramener le target applicatif de `debug` à `info` ou `warn` avant clôture de la release ;
- démontrer le hot reload Logging et le panneau de test de routage ; - démontrer le hot reload Logging et le panneau de test de routage ;

View File

@@ -1,11 +1,11 @@
<!-- file: crates/ksp-app-config-desk/USAGE.md --> <!-- file: crates/ksp-app-config-desk/USAGE.md -->
<!-- version: 15 --> <!-- version: 16 -->
# Utilisation de `ksp-app-config-desk` # Utilisation de `ksp-app-config-desk`
## État actuel ## É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 son document/profils/sinks/filtres en lecture typée ; les mutations et le hot reload restent à compléter. 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 et une persistence atomique ; seul le hot reload runtime reste à compléter.
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. 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.
@@ -131,7 +131,7 @@ Environnement / .env
Logging Logging
``` ```
Les cinq vues sont maintenant fonctionnelles en lecture. Logging charge le contrat typé `std.logging.json`; ses mutations et son hot reload restent désactivés jusqu'aux tranches dédiées. Le header affiche `Config Desk — <vue active>` : le logo fournit déjà l'identité KSP. Les commandes principales peu nombreuses utilisent des pills/tabs alignées à droite ; une application plus chargée devra préférer un dropdown. Chaque clic de tab est tracé en `trace`, l'activation utilisateur significative reste tracée en `debug`, et chaque remplacement/rendu de section en `trace`. Les appels Tauri partagés utilisent `frontend/ts/invoke.ts`, qui journalise le début et la fin d'une commande sans journaliser ses arguments. Les cinq vues sont fonctionnelles. Logging charge le contrat typé `std.logging.json`, permet de modifier son brouillon puis de le persister via Config ; son hot reload runtime reste désactivé jusqu'à `pre.016`. Le header affiche `Config Desk — <vue active>` : le logo fournit déjà l'identité KSP. Les commandes principales peu nombreuses utilisent des pills/tabs alignées à droite ; une application plus chargée devra préférer un dropdown. Chaque clic de tab est tracé en `trace`, l'activation utilisateur significative reste tracée en `debug`, et chaque remplacement/rendu de section en `trace`. Les appels Tauri partagés utilisent `frontend/ts/invoke.ts`, qui journalise le début et la fin d'une commande sans journaliser ses arguments.
## Asset font du splash ## Asset font du splash
@@ -200,7 +200,7 @@ Le bouton **Supprimer** ouvre un modal Bootstrap indiquant la variable ciblée.
Pour valider le shadowing, lancer ponctuellement l'application avec `KSP_LOGS_DIRECTORY` fourni par le process puis modifier la même variable dans `.env` depuis le panneau. Le résultat attendu est `source_changed=true`, `effective_changed=false`, `shadowed_by_process_environment=true`, `reload_required=false`. Le process hérité reste prioritaire et n'est jamais modifié par Config Desk. Pour valider le shadowing, lancer ponctuellement l'application avec `KSP_LOGS_DIRECTORY` fourni par le process puis modifier la même variable dans `.env` depuis le panneau. Le résultat attendu est `source_changed=true`, `effective_changed=false`, `shadowed_by_process_environment=true`, `reload_required=false`. Le process hérité reste prioritaire et n'est jamais modifié par Config Desk.
## Panneau Logging — lecture typée ## Panneau Logging — édition typée
La vue **Logging** appelle `get_logging_document`. Le backend charge le document avec `ConfigManagement::load_logging_document()` et expose : La vue **Logging** appelle `get_logging_document`. Le backend charge le document avec `ConfigManagement::load_logging_document()` et expose :
@@ -210,5 +210,5 @@ La vue **Logging** appelle `get_logging_document`. Le backend charge le document
- tous les fichiers (`output_id`, path, rotation, format, ANSI et filtre) ; - tous les fichiers (`output_id`, path, rotation, format, ANSI et filtre) ;
- tous les `target_filters` globaux. - tous les `target_filters` globaux.
Le sélecteur **Profil inspec** ne modifie pas le document. **Rafraîchir** relit la source typée. Aucun bouton de sauvegarde n'est présent en `pre.014`; les opérations create/clone/rename/delete, `default_profile` et persistence sont volontairement reportées à `pre.015`. Le sélecteur **Profil édi** travaille sur un brouillon local. `logs_directory`, `default_profile`, profils, console, file sinks, filtres et target overrides sont modifiables. **Créer**, **Cloner**, **Renommer** et **Supprimer** agissent d'abord sur le brouillon ; la suppression de profil est confirmée par modal Bootstrap. **Sauvegarder** envoie un candidat typé à `save_logging_document`, qui reconstruit les contrats Config et persiste atomiquement après validation. **Recharger** resynchronise depuis la source et demande confirmation si le brouillon contient des changements non sauvegardés. Le runtime Logging actif ne change pas dans `pre.015`.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/frontend/main.html --> <!-- file: crates/ksp-app-config-desk/frontend/main.html -->
<!-- version: 10 --> <!-- version: 11 -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -300,110 +300,144 @@
<section id="loggingPanel" hidden aria-label="Logging Config"> <section id="loggingPanel" hidden aria-label="Logging Config">
<div class="d-flex flex-wrap gap-2 align-items-center justify-content-between mb-3"> <div class="d-flex flex-wrap gap-2 align-items-center justify-content-between mb-3">
<div> <div>
<h2 class="h5 mb-1">Logging editor — lecture typée</h2> <h2 class="h5 mb-1">Logging editor — mutations/persistence</h2>
<p class="text-body-secondary small mb-0">Projection read-only de std.logging.json via les types publics de ksp-config-lib ; aucune mutation runtime dans cette tranche.</p> <p class="text-body-secondary small mb-0">Brouillon typé de <code>std.logging.json</code>, validé et persisté atomiquement par <code>ksp-config-lib</code>. Le runtime Logging n'est pas rechargé dans cette tranche.</p>
</div>
<div class="d-flex gap-2">
<button id="refreshLoggingDocument" class="btn btn-outline-primary btn-sm" type="button">
<i class="fa-solid fa-rotate me-1" aria-hidden="true"></i>
Recharger
</button>
<button id="saveLoggingDocument" class="btn btn-primary btn-sm" type="button" disabled>
<i class="fa-solid fa-floppy-disk me-1" aria-hidden="true"></i>
Sauvegarder
</button>
</div> </div>
<button id="refreshLoggingDocument" class="btn btn-outline-primary btn-sm" type="button">
<i class="fa-solid fa-rotate me-1" aria-hidden="true"></i>
Rafraîchir
</button>
</div> </div>
<dl class="row small mb-4 app-profile-metadata"> <dl class="row small mb-3 app-profile-metadata">
<dt class="col-lg-3">file_id</dt> <dt class="col-lg-3">file_id</dt>
<dd id="loggingFileId" class="col-lg-9 font-monospace"></dd> <dd id="loggingFileId" class="col-lg-9 font-monospace"></dd>
<dt class="col-lg-3">Path résolu</dt> <dt class="col-lg-3">Path résolu</dt>
<dd id="loggingPath" class="col-lg-9 font-monospace text-break"></dd> <dd id="loggingPath" class="col-lg-9 font-monospace text-break"></dd>
<dt class="col-lg-3">format_version</dt> <dt class="col-lg-3 mb-0">format_version</dt>
<dd id="loggingFormatVersion" class="col-lg-9 font-monospace"></dd> <dd id="loggingFormatVersion" class="col-lg-9 mb-0 font-monospace"></dd>
<dt class="col-lg-3">logs_directory</dt>
<dd id="loggingLogsDirectory" class="col-lg-9 font-monospace text-break"></dd>
<dt class="col-lg-3 mb-0">default_profile</dt>
<dd id="loggingDefaultProfile" class="col-lg-9 mb-0 font-monospace"></dd>
</dl> </dl>
<div class="row g-3 align-items-end mb-4"> <section class="border rounded p-3 mb-4 bg-body-tertiary" aria-label="Globals Logging">
<div class="col-xl-5"> <h3 class="h6 mb-3">Globals</h3>
<label class="form-label" for="loggingProfileSelect">Profil inspecté</label> <div class="row g-3">
<select id="loggingProfileSelect" class="form-select" disabled></select> <div class="col-xl-7">
<label class="form-label" for="loggingLogsDirectoryInput">logs_directory</label>
<input id="loggingLogsDirectoryInput" class="form-control font-monospace" type="text" autocomplete="off" disabled>
</div>
<div class="col-xl-5">
<label class="form-label" for="loggingDefaultProfileSelect">default_profile</label>
<select id="loggingDefaultProfileSelect" class="form-select" disabled></select>
</div>
</div> </div>
<div class="col-xl-3"> </section>
<span class="form-label d-block">default_filter</span>
<code id="loggingProfileDefaultFilter"></code> <section class="border rounded p-3 mb-4" aria-label="Gestion des profils Logging">
<div class="row g-3 align-items-end">
<div class="col-xl-4">
<label class="form-label" for="loggingProfileSelect">Profil édité</label>
<select id="loggingProfileSelect" class="form-select" disabled></select>
</div>
<div class="col-xl-4">
<label class="form-label" for="loggingProfileIdInput">ID cible create/clone/rename</label>
<input id="loggingProfileIdInput" class="form-control font-monospace" type="text" autocomplete="off" placeholder="local_test" disabled>
</div>
<div class="col-xl-4">
<div class="d-flex flex-wrap gap-2">
<button id="createLoggingProfile" class="btn btn-outline-primary btn-sm" type="button" disabled>Créer</button>
<button id="cloneLoggingProfile" class="btn btn-outline-primary btn-sm" type="button" disabled>Cloner</button>
<button id="renameLoggingProfile" class="btn btn-outline-secondary btn-sm" type="button" disabled>Renommer</button>
<button id="deleteLoggingProfile" class="btn btn-outline-danger btn-sm" type="button" disabled>Supprimer</button>
</div>
</div>
</div> </div>
<div class="col-xl-4"> </section>
<span class="form-label d-block">span_events</span>
<code id="loggingProfileSpanEvents"></code> <div class="row g-3 mb-4">
<div class="col-xl-6">
<label class="form-label" for="loggingProfileDefaultFilter">default_filter</label>
<select id="loggingProfileDefaultFilter" class="form-select" disabled>
<option value="off">off</option><option value="error">error</option><option value="warn">warn</option><option value="info">info</option><option value="debug">debug</option><option value="trace">trace</option>
</select>
</div>
<div class="col-xl-6">
<label class="form-label" for="loggingProfileSpanEvents">span_events</label>
<select id="loggingProfileSpanEvents" class="form-select" disabled>
<option value="off">off</option><option value="new_and_close">new_and_close</option><option value="full">full</option>
</select>
</div> </div>
</div> </div>
<section class="border rounded p-3 mb-4 bg-body-tertiary" aria-label="Console Logging"> <section class="border rounded p-3 mb-4 bg-body-tertiary" aria-label="Console Logging">
<h3 class="h6 mb-3">Console</h3> <h3 class="h6 mb-3">Console</h3>
<div class="table-responsive"> <div class="row g-3">
<table class="table table-sm align-middle mb-0"> <div class="col-md-2 form-check ms-2">
<thead> <input id="loggingConsoleEnabled" class="form-check-input" type="checkbox" disabled>
<tr> <label class="form-check-label" for="loggingConsoleEnabled">Enabled</label>
<th scope="col">Enabled</th> </div>
<th scope="col">Output</th> <div class="col-md-2 form-check ms-2">
<th scope="col">ANSI</th> <input id="loggingConsoleAnsi" class="form-check-input" type="checkbox" disabled>
<th scope="col">Format</th> <label class="form-check-label" for="loggingConsoleAnsi">ANSI</label>
<th scope="col">Level</th> </div>
<th scope="col">Targets</th> <div class="col-md-3">
<th scope="col">Domains</th> <label class="form-label" for="loggingConsoleOutput">Output</label>
</tr> <select id="loggingConsoleOutput" class="form-select" disabled><option value="stdout">stdout</option><option value="stderr">stderr</option></select>
</thead> </div>
<tbody> <div class="col-md-4">
<tr> <label class="form-label" for="loggingConsoleFormat">Format</label>
<td id="loggingConsoleEnabled"></td> <select id="loggingConsoleFormat" class="form-select" disabled><option value="human">human</option><option value="compact">compact</option><option value="pretty">pretty</option><option value="json">json</option></select>
<td id="loggingConsoleOutput"></td> </div>
<td id="loggingConsoleAnsi"></td> <div class="col-md-3">
<td id="loggingConsoleFormat"></td> <label class="form-label" for="loggingConsoleLevel">Level</label>
<td id="loggingConsoleLevel"></td> <select id="loggingConsoleLevel" class="form-select" disabled><option value="off">off</option><option value="error">error</option><option value="warn">warn</option><option value="info">info</option><option value="debug">debug</option><option value="trace">trace</option></select>
<td id="loggingConsoleTargets" class="font-monospace text-break"></td> </div>
<td id="loggingConsoleDomains" class="font-monospace text-break"></td> <div class="col-md-4">
</tr> <label class="form-label" for="loggingConsoleTargets">Targets</label>
</tbody> <input id="loggingConsoleTargets" class="form-control font-monospace" type="text" autocomplete="off" disabled>
</table> </div>
<div class="col-md-5">
<label class="form-label" for="loggingConsoleDomains">Domains</label>
<input id="loggingConsoleDomains" class="form-control font-monospace" type="text" autocomplete="off" disabled>
</div>
</div> </div>
</section> </section>
<section class="mb-4" aria-label="Fichiers Logging"> <section class="mb-4" aria-label="Fichiers Logging">
<h3 class="h6 mb-3">Fichiers</h3> <div class="d-flex align-items-center justify-content-between gap-2 mb-2">
<div>
<h3 class="h6 mb-0">Fichiers</h3>
<div class="form-text">0, 1 ou plusieurs sinks persistants. Les suppressions restent dans le brouillon jusqu'à Sauvegarder.</div>
</div>
<button id="addLoggingFile" class="btn btn-outline-primary btn-sm" type="button" disabled>Ajouter un fichier</button>
</div>
<div class="table-responsive"> <div class="table-responsive">
<table id="loggingFilesTable" class="table table-striped table-hover table-sm align-middle w-100"> <table id="loggingFilesTable" class="table table-sm align-middle w-100">
<thead> <thead><tr><th>output_id</th><th>On</th><th>Path</th><th>Rotation</th><th>Format</th><th>Level</th><th>Targets</th><th>Domains</th><th></th></tr></thead>
<tr>
<th scope="col">output_id</th>
<th scope="col">Enabled</th>
<th scope="col">Path</th>
<th scope="col">Rotation</th>
<th scope="col">Format</th>
<th scope="col">ANSI</th>
<th scope="col">Level</th>
<th scope="col">Targets</th>
<th scope="col">Domains</th>
</tr>
</thead>
<tbody></tbody> <tbody></tbody>
</table> </table>
</div> </div>
</section> </section>
<section class="mb-4" aria-label="Target filters Logging"> <section class="mb-4" aria-label="Target filters Logging">
<h3 class="h6 mb-3">Target filters globaux</h3> <div class="d-flex align-items-center justify-content-between gap-2 mb-2">
<h3 class="h6 mb-0">Target filters globaux</h3>
<button id="addLoggingTargetFilter" class="btn btn-outline-primary btn-sm" type="button" disabled>Ajouter un target filter</button>
</div>
<div class="table-responsive"> <div class="table-responsive">
<table id="loggingTargetFiltersTable" class="table table-striped table-hover table-sm align-middle w-100"> <table id="loggingTargetFiltersTable" class="table table-sm align-middle w-100">
<thead> <thead><tr><th>target_prefix</th><th>Level</th><th></th></tr></thead>
<tr>
<th scope="col">target_prefix</th>
<th scope="col">Level</th>
</tr>
</thead>
<tbody></tbody> <tbody></tbody>
</table> </table>
</div> </div>
</section> </section>
<div id="loggingSaveReport" class="small text-body-secondary mb-2">Aucune sauvegarde effectuée pendant ce lancement.</div>
<div id="loggingStatus" class="alert alert-primary mb-0" role="status" aria-live="polite">Chargement du document Logging typé...</div> <div id="loggingStatus" class="alert alert-primary mb-0" role="status" aria-live="polite">Chargement du document Logging typé...</div>
</section> </section>
@@ -417,6 +451,43 @@
</div> </div>
</main> </main>
<div id="loggingDiscardDraftModal" class="modal fade" tabindex="-1" aria-labelledby="loggingDiscardDraftTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 id="loggingDiscardDraftTitle" class="modal-title fs-5">Abandonner le brouillon Logging</h2>
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Annuler"></button>
</div>
<div class="modal-body">
<p class="mb-0">Des modifications non sauvegardées existent. Recharger la source les abandonnera.</p>
</div>
<div class="modal-footer">
<button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">Annuler</button>
<button id="loggingDiscardDraftConfirm" class="btn btn-danger" type="button">Abandonner et recharger</button>
</div>
</div>
</div>
</div>
<div id="loggingProfileDeleteModal" class="modal fade" tabindex="-1" aria-labelledby="loggingProfileDeleteTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 id="loggingProfileDeleteTitle" class="modal-title fs-5">Supprimer le profil du brouillon</h2>
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Annuler"></button>
</div>
<div class="modal-body">
<p class="mb-2">Retirer le profil <code id="loggingProfileDeleteName" class="font-monospace"></code> du brouillon Logging ?</p>
<p class="text-body-secondary small mb-0">Le fichier n'est pas modifié tant que Sauvegarder n'est pas déclenché.</p>
</div>
<div class="modal-footer">
<button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">Annuler</button>
<button id="loggingProfileDeleteConfirm" class="btn btn-danger" type="button">Retirer du brouillon</button>
</div>
</div>
</div>
</div>
<div id="environmentRemoveConfirmModal" class="modal fade" tabindex="-1" aria-labelledby="environmentRemoveConfirmTitle" aria-hidden="true"> <div id="environmentRemoveConfirmModal" class="modal fade" tabindex="-1" aria-labelledby="environmentRemoveConfirmTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content"> <div class="modal-content">

View File

@@ -1,17 +1,27 @@
// file: crates/ksp-app-config-desk/frontend/ts/logging.ts // file: crates/ksp-app-config-desk/frontend/ts/logging.ts
// version: 1 // version: 2
//! Read-only typed Logging editor backed exclusively by ConfigManagement. //! Typed Logging editor backed exclusively by ConfigManagement validation and persistence.
import { Modal } from "bootstrap";
import type { LoggingDocumentCandidateDto } from "./bindings/ksp_app_config_desk/logging/LoggingDocumentCandidateDto";
import type { LoggingDocumentDto } from "./bindings/ksp_app_config_desk/logging/LoggingDocumentDto"; import type { LoggingDocumentDto } from "./bindings/ksp_app_config_desk/logging/LoggingDocumentDto";
import type { LoggingOutputFilterDto } from "./bindings/ksp_app_config_desk/logging/LoggingOutputFilterDto"; import type { LoggingDocumentSaveResultDto } from "./bindings/ksp_app_config_desk/logging/LoggingDocumentSaveResultDto";
import type { LoggingFileDto } from "./bindings/ksp_app_config_desk/logging/LoggingFileDto";
import type { LoggingProfileDto } from "./bindings/ksp_app_config_desk/logging/LoggingProfileDto"; import type { LoggingProfileDto } from "./bindings/ksp_app_config_desk/logging/LoggingProfileDto";
import type { LoggingTargetFilterDto } from "./bindings/ksp_app_config_desk/logging/LoggingTargetFilterDto";
import { frontendDebug, frontendTrace } from "./frontend_log"; import { frontendDebug, frontendTrace } from "./frontend_log";
import { invokeKsp } from "./invoke"; import { invokeKsp } from "./invoke";
let loggingDocument: LoggingDocumentDto | null = null; const LEVELS = ["off", "error", "warn", "info", "debug", "trace"] as const;
const ROTATIONS = ["never", "hourly", "daily"] as const;
const FORMATS = ["human", "compact", "pretty", "json"] as const;
function setLoggingStatus(message: string, tone: "primary" | "success" | "danger" = "primary"): void { let draft: LoggingDocumentCandidateDto | null = null;
let selectedProfileId = "";
let dirty = false;
function setLoggingStatus(message: string, tone: "primary" | "success" | "warning" | "danger" = "primary"): void {
const status = document.querySelector<HTMLElement>("#loggingStatus"); const status = document.querySelector<HTMLElement>("#loggingStatus");
if (!status) { if (!status) {
return; return;
@@ -28,58 +38,177 @@ function setText(selector: string, value: string): void {
} }
} }
function boolLabel(value: boolean): string { function inputValue(selector: string): string {
return value ? "oui" : "non"; return document.querySelector<HTMLInputElement>(selector)?.value ?? "";
} }
function selectorsLabel(values: string[]): string { function selectedValue(selector: string): string {
return values.length > 0 ? values.join(", ") : ""; return document.querySelector<HTMLSelectElement>(selector)?.value ?? "";
} }
function filterSummary(filter: LoggingOutputFilterDto): { level: string; targets: string; domains: string } { function checkboxValue(selector: string): boolean {
return { level: filter.level, targets: selectorsLabel(filter.targets), domains: selectorsLabel(filter.domains) }; return document.querySelector<HTMLInputElement>(selector)?.checked ?? false;
} }
function appendCell(row: HTMLTableRowElement, value: string, className?: string): void { function setInputValue(selector: string, value: string): void {
const cell = document.createElement("td"); const input = document.querySelector<HTMLInputElement>(selector);
cell.textContent = value; if (input) {
if (className) { input.value = value;
cell.className = className;
} }
row.append(cell);
} }
function renderProfileSelect(documentDto: LoggingDocumentDto): void { function setSelectValue(selector: string, value: string): void {
const select = document.querySelector<HTMLSelectElement>("#loggingProfileSelect"); const select = document.querySelector<HTMLSelectElement>(selector);
if (!select) { if (select) {
select.value = value;
}
}
function setCheckboxValue(selector: string, value: boolean): void {
const input = document.querySelector<HTMLInputElement>(selector);
if (input) {
input.checked = value;
}
}
function selectorText(values: string[]): string {
return values.join(", ");
}
function parseSelectors(value: string): string[] {
const result: string[] = [];
for (const part of value.split(",")) {
const trimmed = part.trim();
if (trimmed.length > 0 && !result.includes(trimmed)) {
result.push(trimmed);
}
}
return result;
}
function cloneProfile(profile: LoggingProfileDto): LoggingProfileDto {
return structuredClone(profile);
}
function selectedProfile(): LoggingProfileDto | null {
if (!draft) {
return null;
}
return draft.profiles.find(profile => profile.profileId === selectedProfileId) ?? null;
}
function markDirty(reason: string): void {
dirty = true;
const saveButton = document.querySelector<HTMLButtonElement>("#saveLoggingDocument");
if (saveButton) {
saveButton.disabled = false;
}
setLoggingStatus("Brouillon modifié. Sauvegarde nécessaire pour persister std.logging.json.", "warning");
frontendTrace("main", "Logging draft changed", { reason, selectedProfileId });
}
function setEditorEnabled(enabled: boolean): void {
const selectors = [
"#loggingLogsDirectoryInput",
"#loggingDefaultProfileSelect",
"#loggingProfileSelect",
"#loggingProfileIdInput",
"#createLoggingProfile",
"#cloneLoggingProfile",
"#renameLoggingProfile",
"#deleteLoggingProfile",
"#loggingProfileDefaultFilter",
"#loggingProfileSpanEvents",
"#loggingConsoleEnabled",
"#loggingConsoleAnsi",
"#loggingConsoleOutput",
"#loggingConsoleFormat",
"#loggingConsoleLevel",
"#loggingConsoleTargets",
"#loggingConsoleDomains",
"#addLoggingFile",
"#addLoggingTargetFilter",
];
for (const selector of selectors) {
const element = document.querySelector<HTMLInputElement | HTMLSelectElement | HTMLButtonElement>(selector);
if (element) {
element.disabled = !enabled;
}
}
for (const element of document.querySelectorAll<HTMLInputElement | HTMLSelectElement | HTMLButtonElement>(
"#loggingFilesTable input, #loggingFilesTable select, #loggingFilesTable button, #loggingTargetFiltersTable input, #loggingTargetFiltersTable select, #loggingTargetFiltersTable button",
)) {
element.disabled = !enabled;
}
const save = document.querySelector<HTMLButtonElement>("#saveLoggingDocument");
if (save) {
save.disabled = !enabled || !dirty;
}
}
function renderProfileSelects(): void {
const profileSelect = document.querySelector<HTMLSelectElement>("#loggingProfileSelect");
const defaultSelect = document.querySelector<HTMLSelectElement>("#loggingDefaultProfileSelect");
if (!draft || !profileSelect || !defaultSelect) {
return; return;
} }
const previous = select.value; profileSelect.replaceChildren();
select.replaceChildren(); defaultSelect.replaceChildren();
for (const profile of documentDto.profiles) { for (const profile of draft.profiles) {
const option = document.createElement("option"); const profileOption = document.createElement("option");
option.value = profile.profileId; profileOption.value = profile.profileId;
option.textContent = profile.profileId === documentDto.defaultProfile ? `${profile.profileId} (default)` : profile.profileId; profileOption.textContent = profile.profileId === draft.defaultProfile ? `${profile.profileId} (default)` : profile.profileId;
select.append(option); profileSelect.append(profileOption);
const defaultOption = document.createElement("option");
defaultOption.value = profile.profileId;
defaultOption.textContent = profile.profileId;
defaultSelect.append(defaultOption);
} }
const previousExists = documentDto.profiles.some(profile => profile.profileId === previous); if (!draft.profiles.some(profile => profile.profileId === selectedProfileId)) {
select.value = previousExists ? previous : documentDto.defaultProfile; selectedProfileId = draft.profiles[0]?.profileId ?? "";
if (!documentDto.profiles.some(profile => profile.profileId === select.value) && documentDto.profiles.length > 0) {
select.value = documentDto.profiles[0].profileId;
} }
select.disabled = documentDto.profiles.length === 0; profileSelect.value = selectedProfileId;
frontendTrace("main", "Logging editor profile select replaced", { profileCount: documentDto.profiles.length }); defaultSelect.value = draft.defaultProfile;
frontendTrace("main", "Logging profile selectors replaced", { profileCount: draft.profiles.length });
} }
function renderConsole(profile: LoggingProfileDto): void { function createSelect(values: readonly string[], value: string, onChange: (next: string) => void): HTMLSelectElement {
const summary = filterSummary(profile.console.filter); const select = document.createElement("select");
setText("#loggingConsoleEnabled", boolLabel(profile.console.enabled)); select.className = "form-select form-select-sm";
setText("#loggingConsoleOutput", profile.console.output); for (const item of values) {
setText("#loggingConsoleAnsi", boolLabel(profile.console.ansi)); const option = document.createElement("option");
setText("#loggingConsoleFormat", profile.console.format); option.value = item;
setText("#loggingConsoleLevel", summary.level); option.textContent = item;
setText("#loggingConsoleTargets", summary.targets); select.append(option);
setText("#loggingConsoleDomains", summary.domains); }
select.value = value;
select.addEventListener("change", () => onChange(select.value));
return select;
}
function createTextInput(value: string, onInput: (next: string) => void, monospace = false): HTMLInputElement {
const input = document.createElement("input");
input.type = "text";
input.autocomplete = "off";
input.className = `form-control form-control-sm${monospace ? " font-monospace" : ""}`;
input.value = value;
input.addEventListener("input", () => onInput(input.value));
return input;
}
function createCheckbox(value: boolean, onChange: (next: boolean) => void): HTMLInputElement {
const input = document.createElement("input");
input.type = "checkbox";
input.className = "form-check-input";
input.checked = value;
input.addEventListener("change", () => onChange(input.checked));
return input;
}
function appendControlCell(row: HTMLTableRowElement, control: HTMLElement): void {
const cell = document.createElement("td");
cell.append(control);
row.append(cell);
} }
function renderFiles(profile: LoggingProfileDto): void { function renderFiles(profile: LoggingProfileDto): void {
@@ -88,21 +217,30 @@ function renderFiles(profile: LoggingProfileDto): void {
return; return;
} }
body.replaceChildren(); body.replaceChildren();
for (const file of profile.files) { profile.files.forEach((file, index) => {
const summary = filterSummary(file.filter);
const row = document.createElement("tr"); const row = document.createElement("tr");
appendCell(row, file.outputId, "font-monospace"); appendControlCell(row, createTextInput(file.outputId, value => { file.outputId = value; markDirty("file.output_id"); }, true));
appendCell(row, boolLabel(file.enabled)); appendControlCell(row, createCheckbox(file.enabled, value => { file.enabled = value; markDirty("file.enabled"); }));
appendCell(row, file.path, "font-monospace text-break"); appendControlCell(row, createTextInput(file.path, value => { file.path = value; markDirty("file.path"); }, true));
appendCell(row, file.rotation); appendControlCell(row, createSelect(ROTATIONS, file.rotation, value => { file.rotation = value; markDirty("file.rotation"); }));
appendCell(row, file.format); appendControlCell(row, createSelect(FORMATS, file.format, value => { file.format = value; markDirty("file.format"); }));
appendCell(row, boolLabel(file.ansi)); appendControlCell(row, createSelect(LEVELS, file.filter.level, value => { file.filter.level = value; markDirty("file.filter.level"); }));
appendCell(row, summary.level); appendControlCell(row, createTextInput(selectorText(file.filter.targets), value => { file.filter.targets = parseSelectors(value); markDirty("file.filter.targets"); }, true));
appendCell(row, summary.targets, "font-monospace text-break"); appendControlCell(row, createTextInput(selectorText(file.filter.domains), value => { file.filter.domains = parseSelectors(value); markDirty("file.filter.domains"); }, true));
appendCell(row, summary.domains, "font-monospace text-break"); const remove = document.createElement("button");
remove.type = "button";
remove.className = "btn btn-outline-danger btn-sm";
remove.textContent = "Retirer";
remove.addEventListener("click", () => {
profile.files.splice(index, 1);
markDirty("file.remove");
renderFiles(profile);
frontendDebug("main", "Logging file sink removed from draft", { profileId: profile.profileId, fileIndex: index });
});
appendControlCell(row, remove);
body.append(row); body.append(row);
} });
frontendTrace("main", "Logging file sink table DOM replaced", { fileCount: profile.files.length }); frontendTrace("main", "Logging file sink editor DOM replaced", { fileCount: profile.files.length });
} }
function renderTargetFilters(profile: LoggingProfileDto): void { function renderTargetFilters(profile: LoggingProfileDto): void {
@@ -111,32 +249,45 @@ function renderTargetFilters(profile: LoggingProfileDto): void {
return; return;
} }
body.replaceChildren(); body.replaceChildren();
for (const filter of profile.targetFilters) { profile.targetFilters.forEach((filter, index) => {
const row = document.createElement("tr"); const row = document.createElement("tr");
appendCell(row, filter.targetPrefix, "font-monospace"); appendControlCell(row, createTextInput(filter.targetPrefix, value => { filter.targetPrefix = value; markDirty("target_filter.prefix"); }, true));
appendCell(row, filter.level); appendControlCell(row, createSelect(LEVELS, filter.level, value => { filter.level = value; markDirty("target_filter.level"); }));
const remove = document.createElement("button");
remove.type = "button";
remove.className = "btn btn-outline-danger btn-sm";
remove.textContent = "Retirer";
remove.addEventListener("click", () => {
profile.targetFilters.splice(index, 1);
markDirty("target_filter.remove");
renderTargetFilters(profile);
frontendDebug("main", "Logging target filter removed from draft", { profileId: profile.profileId, filterIndex: index });
});
appendControlCell(row, remove);
body.append(row); body.append(row);
} });
frontendTrace("main", "Logging target override table DOM replaced", { targetFilterCount: profile.targetFilters.length }); frontendTrace("main", "Logging target filter editor DOM replaced", { targetFilterCount: profile.targetFilters.length });
} }
function renderSelectedProfile(source: "load" | "selection"): void { function renderSelectedProfile(source: "load" | "selection" | "mutation"): void {
const documentDto = loggingDocument; const profile = selectedProfile();
const select = document.querySelector<HTMLSelectElement>("#loggingProfileSelect");
if (!documentDto || !select) {
return;
}
const profile = documentDto.profiles.find(candidate => candidate.profileId === select.value);
if (!profile) { if (!profile) {
setLoggingStatus("Le profil Logging sélectionné n'existe plus dans le document chargé.", "danger"); setEditorEnabled(false);
return; return;
} }
setText("#loggingProfileDefaultFilter", profile.defaultFilter); setSelectValue("#loggingProfileDefaultFilter", profile.defaultFilter);
setText("#loggingProfileSpanEvents", profile.spanEvents); setSelectValue("#loggingProfileSpanEvents", profile.spanEvents);
renderConsole(profile); setCheckboxValue("#loggingConsoleEnabled", profile.console.enabled);
setCheckboxValue("#loggingConsoleAnsi", profile.console.ansi);
setSelectValue("#loggingConsoleOutput", profile.console.output);
setSelectValue("#loggingConsoleFormat", profile.console.format);
setSelectValue("#loggingConsoleLevel", profile.console.filter.level);
setInputValue("#loggingConsoleTargets", selectorText(profile.console.filter.targets));
setInputValue("#loggingConsoleDomains", selectorText(profile.console.filter.domains));
renderFiles(profile); renderFiles(profile);
renderTargetFilters(profile); renderTargetFilters(profile);
frontendTrace("main", "Logging profile read-only view rendered", { setEditorEnabled(true);
frontendTrace("main", "Logging profile editor rendered", {
profileId: profile.profileId, profileId: profile.profileId,
source, source,
fileCount: profile.files.length, fileCount: profile.files.length,
@@ -145,46 +296,332 @@ function renderSelectedProfile(source: "load" | "selection"): void {
} }
function renderDocument(documentDto: LoggingDocumentDto): void { function renderDocument(documentDto: LoggingDocumentDto): void {
loggingDocument = documentDto; draft = {
logsDirectory: documentDto.logsDirectory,
defaultProfile: documentDto.defaultProfile,
profiles: documentDto.profiles.map(profile => cloneProfile(profile)),
};
dirty = false;
selectedProfileId = draft.profiles.some(profile => profile.profileId === selectedProfileId) ? selectedProfileId : draft.defaultProfile;
if (!draft.profiles.some(profile => profile.profileId === selectedProfileId)) {
selectedProfileId = draft.profiles[0]?.profileId ?? "";
}
setText("#loggingFileId", documentDto.fileId); setText("#loggingFileId", documentDto.fileId);
setText("#loggingPath", documentDto.path); setText("#loggingPath", documentDto.path);
setText("#loggingFormatVersion", documentDto.formatVersion.toString()); setText("#loggingFormatVersion", documentDto.formatVersion.toString());
setText("#loggingLogsDirectory", documentDto.logsDirectory); setInputValue("#loggingLogsDirectoryInput", draft.logsDirectory);
setText("#loggingDefaultProfile", documentDto.defaultProfile); renderProfileSelects();
renderProfileSelect(documentDto);
renderSelectedProfile("load"); renderSelectedProfile("load");
frontendTrace("main", "Logging document read-only view rendered", { profileCount: documentDto.profiles.length }); setText("#loggingSaveReport", "Aucune sauvegarde effectuée depuis le dernier chargement.");
frontendTrace("main", "Logging document editor rendered", { profileCount: documentDto.profiles.length });
}
function defaultProfile(profileId: string): LoggingProfileDto {
return {
profileId,
defaultFilter: "warn",
spanEvents: "new_and_close",
console: {
enabled: true,
output: "stderr",
ansi: true,
format: "compact",
filter: { level: "info", targets: ["*"], domains: ["*"] },
},
files: [],
targetFilters: [],
};
}
function targetProfileId(): string {
return inputValue("#loggingProfileIdInput").trim();
}
function profileIdAvailable(profileId: string, exceptProfileId?: string): boolean {
if (!draft || profileId.length === 0) {
return false;
}
return !draft.profiles.some(profile => profile.profileId === profileId && profile.profileId !== exceptProfileId);
}
function createProfile(): void {
if (!draft) {
return;
}
const profileId = targetProfileId();
if (!profileIdAvailable(profileId)) {
setLoggingStatus("L'ID cible est vide ou existe déjà dans le brouillon.", "danger");
return;
}
draft.profiles.push(defaultProfile(profileId));
selectedProfileId = profileId;
setInputValue("#loggingProfileIdInput", "");
markDirty("profile.create");
renderProfileSelects();
renderSelectedProfile("mutation");
frontendDebug("main", "Logging profile created in draft", { profileId });
}
function cloneSelectedProfile(): void {
if (!draft) {
return;
}
const source = selectedProfile();
const profileId = targetProfileId();
if (!source || !profileIdAvailable(profileId)) {
setLoggingStatus("Sélectionne un profil et fournis un ID cible libre pour le clone.", "danger");
return;
}
const clone = cloneProfile(source);
clone.profileId = profileId;
draft.profiles.push(clone);
selectedProfileId = profileId;
setInputValue("#loggingProfileIdInput", "");
markDirty("profile.clone");
renderProfileSelects();
renderSelectedProfile("mutation");
frontendDebug("main", "Logging profile cloned in draft", { sourceProfileId: source.profileId, profileId });
}
function renameSelectedProfile(): void {
if (!draft) {
return;
}
const profile = selectedProfile();
const profileId = targetProfileId();
if (!profile || !profileIdAvailable(profileId, profile.profileId)) {
setLoggingStatus("Fournis un nouvel ID de profil libre avant de renommer.", "danger");
return;
}
const previousId = profile.profileId;
profile.profileId = profileId;
if (draft.defaultProfile === previousId) {
draft.defaultProfile = profileId;
}
selectedProfileId = profileId;
setInputValue("#loggingProfileIdInput", "");
markDirty("profile.rename");
renderProfileSelects();
renderSelectedProfile("mutation");
frontendDebug("main", "Logging profile renamed in draft", { previousId, profileId });
}
function confirmDeleteSelectedProfile(): void {
if (!draft || draft.profiles.length <= 1) {
setLoggingStatus("Le document Logging doit conserver au moins un profil.", "danger");
return;
}
const profile = selectedProfile();
const modalElement = document.querySelector<HTMLElement>("#loggingProfileDeleteModal");
if (!profile || !modalElement) {
return;
}
setText("#loggingProfileDeleteName", profile.profileId);
modalElement.dataset.profileId = profile.profileId;
frontendDebug("main", "Logging profile draft deletion confirmation opened", { profileId: profile.profileId });
Modal.getOrCreateInstance(modalElement).show();
}
function deleteConfirmedProfile(): void {
if (!draft) {
return;
}
const modalElement = document.querySelector<HTMLElement>("#loggingProfileDeleteModal");
const profileId = modalElement?.dataset.profileId ?? "";
const index = draft.profiles.findIndex(profile => profile.profileId === profileId);
if (index < 0 || draft.profiles.length <= 1) {
return;
}
draft.profiles.splice(index, 1);
if (draft.defaultProfile === profileId) {
draft.defaultProfile = draft.profiles[0].profileId;
}
selectedProfileId = draft.profiles[Math.min(index, draft.profiles.length - 1)].profileId;
markDirty("profile.delete");
renderProfileSelects();
renderSelectedProfile("mutation");
if (modalElement) {
Modal.getOrCreateInstance(modalElement).hide();
delete modalElement.dataset.profileId;
}
frontendDebug("main", "Logging profile removed from draft", { profileId });
}
function addFile(): void {
const profile = selectedProfile();
if (!profile) {
return;
}
const file: LoggingFileDto = {
outputId: `file.new.${profile.files.length + 1}`,
enabled: true,
path: `new/output-${profile.files.length + 1}.log`,
rotation: "daily",
format: "human",
ansi: false,
filter: { level: "info", targets: ["*"], domains: ["*"] },
};
profile.files.push(file);
markDirty("file.add");
renderFiles(profile);
frontendDebug("main", "Logging file sink added to draft", { profileId: profile.profileId, fileCount: profile.files.length });
}
function addTargetFilter(): void {
const profile = selectedProfile();
if (!profile) {
return;
}
const filter: LoggingTargetFilterDto = { targetPrefix: "ksp-", level: "info" };
profile.targetFilters.push(filter);
markDirty("target_filter.add");
renderTargetFilters(profile);
frontendDebug("main", "Logging target filter added to draft", { profileId: profile.profileId, targetFilterCount: profile.targetFilters.length });
}
function syncStaticProfileFields(): void {
const profile = selectedProfile();
if (!profile) {
return;
}
profile.defaultFilter = selectedValue("#loggingProfileDefaultFilter");
profile.spanEvents = selectedValue("#loggingProfileSpanEvents");
profile.console.enabled = checkboxValue("#loggingConsoleEnabled");
profile.console.ansi = checkboxValue("#loggingConsoleAnsi");
profile.console.output = selectedValue("#loggingConsoleOutput");
profile.console.format = selectedValue("#loggingConsoleFormat");
profile.console.filter.level = selectedValue("#loggingConsoleLevel");
profile.console.filter.targets = parseSelectors(inputValue("#loggingConsoleTargets"));
profile.console.filter.domains = parseSelectors(inputValue("#loggingConsoleDomains"));
}
async function saveLoggingDocument(): Promise<void> {
if (!draft) {
return;
}
syncStaticProfileFields();
draft.logsDirectory = inputValue("#loggingLogsDirectoryInput");
draft.defaultProfile = selectedValue("#loggingDefaultProfileSelect");
setEditorEnabled(false);
setLoggingStatus("Validation et sauvegarde atomique du document Logging...");
frontendDebug("main", "Typed Logging document persistence requested", {
profileCount: draft.profiles.length,
defaultProfile: draft.defaultProfile,
fileCount: draft.profiles.reduce((count, profile) => count + profile.files.length, 0),
});
try {
const result = await invokeKsp<LoggingDocumentSaveResultDto>("main", "save_logging_document", { candidate: draft });
renderDocument(result.document);
setText("#loggingSaveReport", `source_changed=${result.sourceChanged} · reload_required=${result.reloadRequired} · runtime Logging inchangé`);
setLoggingStatus(result.sourceChanged ? "Document Logging validé et persisté. Le runtime sera traité en pre.016." : "Document Logging valide mais source inchangée.", "success");
frontendDebug("main", "Typed Logging document persistence completed", {
sourceChanged: result.sourceChanged,
reloadRequired: result.reloadRequired,
profileCount: result.document.profiles.length,
});
} catch {
setEditorEnabled(true);
setLoggingStatus("Candidat Logging refusé. std.logging.json reste inchangé ; corrige le brouillon puis réessaie.", "danger");
}
}
function requestReload(): void {
if (!dirty) {
void refreshLoggingDocument("user");
return;
}
const modalElement = document.querySelector<HTMLElement>("#loggingDiscardDraftModal");
if (!modalElement) {
return;
}
frontendDebug("main", "Logging dirty draft discard confirmation opened");
Modal.getOrCreateInstance(modalElement).show();
}
function confirmReload(): void {
const modalElement = document.querySelector<HTMLElement>("#loggingDiscardDraftModal");
if (modalElement) {
Modal.getOrCreateInstance(modalElement).hide();
}
frontendDebug("main", "Logging dirty draft discard confirmed");
void refreshLoggingDocument("user");
} }
async function refreshLoggingDocument(source: "startup" | "user"): Promise<void> { async function refreshLoggingDocument(source: "startup" | "user"): Promise<void> {
if (source === "user") { if (source === "user") {
frontendDebug("main", "Logging document refresh requested"); frontendDebug("main", "Logging document reload requested", { dirty });
} }
setEditorEnabled(false);
setLoggingStatus("Chargement du document Logging typé..."); setLoggingStatus("Chargement du document Logging typé...");
try { try {
const documentDto = await invokeKsp<LoggingDocumentDto>("main", "get_logging_document"); const documentDto = await invokeKsp<LoggingDocumentDto>("main", "get_logging_document");
renderDocument(documentDto); renderDocument(documentDto);
setLoggingStatus(`${documentDto.profiles.length} profil(s) Logging chargé(s) en lecture seule.`, "success"); setLoggingStatus(`${documentDto.profiles.length} profil(s) Logging chargé(s). Le brouillon est synchronisé avec la source.`, "success");
frontendDebug("main", "Logging document refresh completed", { frontendDebug("main", "Logging document reload completed", {
profileCount: documentDto.profiles.length, profileCount: documentDto.profiles.length,
defaultProfile: documentDto.defaultProfile, defaultProfile: documentDto.defaultProfile,
}); });
} catch { } catch {
loggingDocument = null; draft = null;
setEditorEnabled(false);
setLoggingStatus("Le document Logging typé n'a pas pu être chargé. Consulte les diagnostics backend.", "danger"); setLoggingStatus("Le document Logging typé n'a pas pu être chargé. Consulte les diagnostics backend.", "danger");
} }
} }
/// Initializes the read-only Logging editor surface. function bindStaticDraftFields(): void {
export function initializeLoggingPanel(): void { document.querySelector<HTMLInputElement>("#loggingLogsDirectoryInput")?.addEventListener("input", () => {
document.querySelector<HTMLButtonElement>("#refreshLoggingDocument")?.addEventListener("click", () => { if (draft) {
void refreshLoggingDocument("user"); draft.logsDirectory = inputValue("#loggingLogsDirectoryInput");
markDirty("logs_directory");
}
}); });
document.querySelector<HTMLSelectElement>("#loggingDefaultProfileSelect")?.addEventListener("change", () => {
if (draft) {
draft.defaultProfile = selectedValue("#loggingDefaultProfileSelect");
markDirty("default_profile");
renderProfileSelects();
}
});
const profileSelectors = ["#loggingProfileDefaultFilter", "#loggingProfileSpanEvents", "#loggingConsoleOutput", "#loggingConsoleFormat", "#loggingConsoleLevel"];
for (const selector of profileSelectors) {
document.querySelector<HTMLSelectElement>(selector)?.addEventListener("change", () => {
syncStaticProfileFields();
markDirty(selector);
});
}
for (const selector of ["#loggingConsoleEnabled", "#loggingConsoleAnsi"]) {
document.querySelector<HTMLInputElement>(selector)?.addEventListener("change", () => {
syncStaticProfileFields();
markDirty(selector);
});
}
for (const selector of ["#loggingConsoleTargets", "#loggingConsoleDomains"]) {
document.querySelector<HTMLInputElement>(selector)?.addEventListener("input", () => {
syncStaticProfileFields();
markDirty(selector);
});
}
}
/// Initializes the typed Logging editor surface.
export function initializeLoggingPanel(): void {
bindStaticDraftFields();
document.querySelector<HTMLButtonElement>("#refreshLoggingDocument")?.addEventListener("click", requestReload);
document.querySelector<HTMLButtonElement>("#saveLoggingDocument")?.addEventListener("click", () => void saveLoggingDocument());
document.querySelector<HTMLSelectElement>("#loggingProfileSelect")?.addEventListener("change", event => { document.querySelector<HTMLSelectElement>("#loggingProfileSelect")?.addEventListener("change", event => {
const profileId = (event.currentTarget as HTMLSelectElement).value; syncStaticProfileFields();
frontendDebug("main", "Logging read-only profile selection changed", { profileId }); selectedProfileId = (event.currentTarget as HTMLSelectElement).value;
frontendDebug("main", "Logging profile selection changed", { profileId: selectedProfileId });
renderSelectedProfile("selection"); renderSelectedProfile("selection");
}); });
frontendTrace("main", "Logging editor handlers installed"); document.querySelector<HTMLButtonElement>("#createLoggingProfile")?.addEventListener("click", createProfile);
document.querySelector<HTMLButtonElement>("#cloneLoggingProfile")?.addEventListener("click", cloneSelectedProfile);
document.querySelector<HTMLButtonElement>("#renameLoggingProfile")?.addEventListener("click", renameSelectedProfile);
document.querySelector<HTMLButtonElement>("#deleteLoggingProfile")?.addEventListener("click", confirmDeleteSelectedProfile);
document.querySelector<HTMLButtonElement>("#loggingProfileDeleteConfirm")?.addEventListener("click", deleteConfirmedProfile);
document.querySelector<HTMLButtonElement>("#loggingDiscardDraftConfirm")?.addEventListener("click", confirmReload);
document.querySelector<HTMLButtonElement>("#addLoggingFile")?.addEventListener("click", addFile);
document.querySelector<HTMLButtonElement>("#addLoggingTargetFilter")?.addEventListener("click", addTargetFilter);
frontendTrace("main", "Logging editor mutation handlers installed");
void refreshLoggingDocument("startup"); void refreshLoggingDocument("startup");
} }

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/main.ts // file: crates/ksp-app-config-desk/frontend/ts/main.ts
// version: 9 // version: 10
import "bootstrap"; import "bootstrap";
import ResizeObserver from "resize-observer-polyfill"; import ResizeObserver from "resize-observer-polyfill";
@@ -38,7 +38,7 @@ const viewCopy: Record<ViewId, { title: string; description: string }> = {
}, },
logging: { logging: {
title: "Logging", title: "Logging",
description: "Lecture typée des profils, sinks et filtres Logging ; mutations et hot reload restent dans les tranches suivantes.", description: "Édition typée et persistence atomique des profils, sinks et filtres Logging ; le hot reload runtime reste dans la tranche suivante.",
}, },
}; };

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/lib.rs // file: crates/ksp-app-config-desk/src/lib.rs
// version: 10 // version: 11
//! Tauri desktop application for managing and validating KSP configuration. //! Tauri desktop application for managing and validating KSP configuration.
@@ -66,7 +66,9 @@ pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED; pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
pub(crate) use self::frontend_logging::FrontendLogPayloadDto; pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
pub(crate) use self::frontend_logging::emit_frontend_log_event; pub(crate) use self::frontend_logging::emit_frontend_log_event;
pub(crate) use self::logging_editor::LoggingDocumentCandidateDto;
pub(crate) use self::logging_editor::LoggingDocumentDto; pub(crate) use self::logging_editor::LoggingDocumentDto;
pub(crate) use self::logging_editor::LoggingDocumentSaveResultDto;
pub(crate) use self::profiles::ConfigProfileDetailDto; pub(crate) use self::profiles::ConfigProfileDetailDto;
pub(crate) use self::profiles::ConfigProfileDocumentDto; pub(crate) use self::profiles::ConfigProfileDocumentDto;
pub(crate) use self::secrets::SecretRevealRequestDto; pub(crate) use self::secrets::SecretRevealRequestDto;

View File

@@ -1,12 +1,12 @@
// file: crates/ksp-app-config-desk/src/logging_editor.rs // file: crates/ksp-app-config-desk/src/logging_editor.rs
// version: 1 // version: 2
//! Read-only typed Logging document projection for the Config Desk Logging editor. //! Typed Logging document projection and validated persistence for the Config Desk Logging editor.
use ts_rs::TS; // rust-rules: derive-import use ts_rs::TS; // rust-rules: derive-import
/// One Logging sink selector/filter exposed to the read-only editor. /// One Logging sink selector/filter exposed to the editor.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingOutputFilterDto.ts")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingOutputFilterDto.ts")]
pub(crate) struct LoggingOutputFilterDto { pub(crate) struct LoggingOutputFilterDto {
@@ -18,8 +18,8 @@ pub(crate) struct LoggingOutputFilterDto {
pub(crate) domains: std::vec::Vec<String>, pub(crate) domains: std::vec::Vec<String>,
} }
/// Console sink configuration exposed to the read-only editor. /// Console sink configuration exposed to the editor.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingConsoleDto.ts")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingConsoleDto.ts")]
pub(crate) struct LoggingConsoleDto { pub(crate) struct LoggingConsoleDto {
@@ -35,8 +35,8 @@ pub(crate) struct LoggingConsoleDto {
pub(crate) filter: LoggingOutputFilterDto, pub(crate) filter: LoggingOutputFilterDto,
} }
/// One persistent file sink exposed to the read-only editor. /// One persistent file sink exposed to the editor.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingFileDto.ts")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingFileDto.ts")]
pub(crate) struct LoggingFileDto { pub(crate) struct LoggingFileDto {
@@ -56,8 +56,8 @@ pub(crate) struct LoggingFileDto {
pub(crate) filter: LoggingOutputFilterDto, pub(crate) filter: LoggingOutputFilterDto,
} }
/// One global Logging target override exposed to the read-only editor. /// One global Logging target override exposed to the editor.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingTargetFilterDto.ts")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingTargetFilterDto.ts")]
pub(crate) struct LoggingTargetFilterDto { pub(crate) struct LoggingTargetFilterDto {
@@ -67,8 +67,8 @@ pub(crate) struct LoggingTargetFilterDto {
pub(crate) level: String, pub(crate) level: String,
} }
/// One typed Logging profile exposed to the read-only editor. /// One typed Logging profile exposed to the editor.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingProfileDto.ts")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingProfileDto.ts")]
pub(crate) struct LoggingProfileDto { pub(crate) struct LoggingProfileDto {
@@ -86,7 +86,7 @@ pub(crate) struct LoggingProfileDto {
pub(crate) target_filters: std::vec::Vec<LoggingTargetFilterDto>, pub(crate) target_filters: std::vec::Vec<LoggingTargetFilterDto>,
} }
/// Complete typed Logging document exposed to the read-only editor. /// Complete typed Logging document exposed to the editor.
#[derive(Clone, Debug, serde::Serialize, TS)] #[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingDocumentDto.ts")] #[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingDocumentDto.ts")]
@@ -105,11 +105,66 @@ pub(crate) struct LoggingDocumentDto {
pub(crate) profiles: std::vec::Vec<LoggingProfileDto>, pub(crate) profiles: std::vec::Vec<LoggingProfileDto>,
} }
/// Complete editable Logging candidate accepted from the frontend.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingDocumentCandidateDto.ts")]
pub(crate) struct LoggingDocumentCandidateDto {
/// Source Logging root expression/path.
pub(crate) logs_directory: String,
/// Autonomous default profile identifier.
pub(crate) default_profile: String,
/// Typed profiles in source order.
pub(crate) profiles: std::vec::Vec<LoggingProfileDto>,
}
/// Result of one validated typed Logging persistence operation.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging/LoggingDocumentSaveResultDto.ts")]
pub(crate) struct LoggingDocumentSaveResultDto {
/// Whether `std.logging.json` bytes changed.
pub(crate) source_changed: bool,
/// Whether Config consumers must reload the document.
pub(crate) reload_required: bool,
/// Fresh typed document loaded after successful persistence.
pub(crate) document: LoggingDocumentDto,
}
/// Loads the typed Logging source through ConfigManagement and projects it for the frontend. /// Loads the typed Logging source through ConfigManagement and projects it for the frontend.
pub(crate) fn document(state: &crate::AppState) -> ksp_core_lib::Result<LoggingDocumentDto> { pub(crate) fn document(state: &crate::AppState) -> ksp_core_lib::Result<LoggingDocumentDto> {
return document_from_management(state.config_management()); return document_from_management(state.config_management());
} }
/// Validates and atomically persists one typed Logging candidate through ConfigManagement.
pub(crate) fn save(state: &crate::AppState, candidate: LoggingDocumentCandidateDto) -> ksp_core_lib::Result<LoggingDocumentSaveResultDto> {
let source = config_candidate(candidate);
let report = state.config_management().save_logging_document(&source);
let report = match report {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let document = document_from_management(state.config_management());
let document = match document {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_LOGGING_EDITOR,
source_changed = report.source_changed(),
reload_required = report.reload_required(),
profile_count = document.profiles.len(),
default_profile = document.default_profile.as_str(),
"typed Logging document persistence completed; runtime was not reinitialized"
);
return std::result::Result::Ok(LoggingDocumentSaveResultDto {
source_changed: report.source_changed(),
reload_required: report.reload_required(),
document,
});
}
fn document_from_management(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<LoggingDocumentDto> { fn document_from_management(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<LoggingDocumentDto> {
let source = management.load_logging_document(); let source = management.load_logging_document();
let source = match source { let source = match source {
@@ -149,11 +204,51 @@ fn document_from_management(management: &ksp_config_lib::ConfigManagement) -> ks
profile_count = result.profiles.len(), profile_count = result.profiles.len(),
file_count, file_count,
target_filter_count, target_filter_count,
"typed Logging document projected for read-only editor" "typed Logging document projected for editor"
); );
return std::result::Result::Ok(result); return std::result::Result::Ok(result);
} }
fn config_candidate(candidate: LoggingDocumentCandidateDto) -> ksp_config_lib::LoggingConfigDocument {
let mut profiles = std::vec::Vec::<ksp_config_lib::LoggingProfileConfig>::with_capacity(candidate.profiles.len());
for profile in candidate.profiles {
profiles.push(config_profile(profile));
}
return ksp_config_lib::LoggingConfigDocument::new(candidate.logs_directory, candidate.default_profile, profiles);
}
fn config_profile(profile: LoggingProfileDto) -> ksp_config_lib::LoggingProfileConfig {
let mut files = std::vec::Vec::<ksp_config_lib::LoggingFileConfig>::with_capacity(profile.files.len());
for file in profile.files {
let mut config =
ksp_config_lib::LoggingFileConfig::new(file.output_id, file.enabled, file.path, file.rotation, file.format, config_output_filter(file.filter));
config.set_ansi(file.ansi);
files.push(config);
}
let mut target_filters = std::vec::Vec::<ksp_config_lib::LoggingTargetFilterConfig>::with_capacity(profile.target_filters.len());
for target_filter in profile.target_filters {
target_filters.push(ksp_config_lib::LoggingTargetFilterConfig::new(target_filter.target_prefix, target_filter.level));
}
return ksp_config_lib::LoggingProfileConfig::new(
profile.profile_id,
profile.default_filter,
profile.span_events,
ksp_config_lib::LoggingConsoleConfig::new(
profile.console.enabled,
profile.console.output,
profile.console.ansi,
profile.console.format,
config_output_filter(profile.console.filter),
),
files,
target_filters,
);
}
fn config_output_filter(filter: LoggingOutputFilterDto) -> ksp_config_lib::LoggingOutputFilterConfig {
return ksp_config_lib::LoggingOutputFilterConfig::new(filter.level, filter.targets, filter.domains);
}
fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingProfileDto { fn project_profile(profile: &ksp_config_lib::LoggingProfileConfig) -> LoggingProfileDto {
let mut files = std::vec::Vec::<LoggingFileDto>::with_capacity(profile.files().len()); let mut files = std::vec::Vec::<LoggingFileDto>::with_capacity(profile.files().len());
for file in profile.files() { for file in profile.files() {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/tauri.rs // file: crates/ksp-app-config-desk/src/tauri.rs
// version: 12 // version: 13
//! Tauri runtime assembly for the KSP configuration desktop application. //! Tauri runtime assembly for the KSP configuration desktop application.
@@ -49,6 +49,7 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
remove_environment_value, remove_environment_value,
reveal_environment_value, reveal_environment_value,
get_logging_document, get_logging_document,
save_logging_document,
emit_frontend_log, emit_frontend_log,
splash_frontend_ready splash_frontend_ready
]); ]);
@@ -180,6 +181,18 @@ fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result
}; };
} }
#[tauri::command]
fn save_logging_document(
candidate: crate::LoggingDocumentCandidateDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LoggingDocumentSaveResultDto, crate::CommandErrorDto> {
let result = crate::logging_editor::save(&state, candidate);
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] #[tauri::command]
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> { fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload); let result = crate::emit_frontend_log_event(payload);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/unit_tests/logging_editor.rs // file: crates/ksp-app-config-desk/unit_tests/logging_editor.rs
// version: 1 // version: 2
#[test] #[test]
fn committed_logging_document_maps_complete_read_only_editor_contract() { fn committed_logging_document_maps_complete_read_only_editor_contract() {
@@ -62,3 +62,72 @@ fn committed_management() -> ksp_core_lib::Result<ksp_config_lib::ConfigManageme
}; };
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry))); return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry)));
} }
#[test]
fn editor_candidate_round_trips_through_typed_config_contract() {
let management = committed_management();
assert!(management.is_ok(), "committed management should construct: {management:?}");
if let std::result::Result::Ok(management) = management {
let document = super::document_from_management(&management);
assert!(document.is_ok(), "Logging editor document should load: {document:?}");
if let std::result::Result::Ok(document) = document {
let candidate = super::LoggingDocumentCandidateDto {
logs_directory: document.logs_directory.clone(),
default_profile: document.default_profile.clone(),
profiles: document.profiles.clone(),
};
let config = super::config_candidate(candidate);
assert_eq!(config.logs_directory(), document.logs_directory);
assert_eq!(config.default_profile(), document.default_profile);
assert_eq!(config.profiles().len(), document.profiles.len());
assert_eq!(config.profiles()[0].files()[0].ansi(), document.profiles[0].files[0].ansi);
}
}
}
#[test]
fn profile_candidate_preserves_multi_file_and_target_filter_shape() {
let profile = super::LoggingProfileDto {
profile_id: "clone_test".to_owned(),
default_filter: "info".to_owned(),
span_events: "full".to_owned(),
console: super::LoggingConsoleDto {
enabled: true,
output: "stdout".to_owned(),
ansi: false,
format: "human".to_owned(),
filter: super::LoggingOutputFilterDto { level: "info".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] },
},
files: std::vec![
super::LoggingFileDto {
output_id: "file.one".to_owned(),
enabled: true,
path: "one.log".to_owned(),
rotation: "daily".to_owned(),
format: "human".to_owned(),
ansi: false,
filter: super::LoggingOutputFilterDto { level: "debug".to_owned(), targets: std::vec!["*".to_owned()], domains: std::vec!["*".to_owned()] },
},
super::LoggingFileDto {
output_id: "file.two".to_owned(),
enabled: false,
path: "two.jsonl".to_owned(),
rotation: "hourly".to_owned(),
format: "json".to_owned(),
ansi: false,
filter: super::LoggingOutputFilterDto {
level: "error".to_owned(),
targets: std::vec!["ksp-config-lib".to_owned()],
domains: std::vec!["config".to_owned()],
},
},
],
target_filters: std::vec![super::LoggingTargetFilterDto { target_prefix: "ksp-app-config-desk".to_owned(), level: "trace".to_owned() }],
};
let config = super::config_profile(profile);
assert_eq!(config.profile_id(), "clone_test");
assert_eq!(config.files().len(), 2);
assert_eq!(config.files()[1].rotation(), "hourly");
assert_eq!(config.target_filters().len(), 1);
assert_eq!(config.target_filters()[0].level(), "trace");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/management.rs // file: crates/ksp-config-lib/src/management.rs
// version: 3 // version: 4
/// Raw source of one registered Config document read for explicit management/correction. /// Raw source of one registered Config document read for explicit management/correction.
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
@@ -482,6 +482,11 @@ impl LoggingFileConfig {
return self.ansi; return self.ansi;
} }
/// Sets whether ANSI output is requested. Persistence validation currently requires persistent file ANSI to remain disabled.
pub fn set_ansi(&mut self, value: bool) {
self.ansi = value;
}
/// Returns the output filter. /// Returns the output filter.
#[must_use] #[must_use]
pub const fn filter(&self) -> &LoggingOutputFilterConfig { pub const fn filter(&self) -> &LoggingOutputFilterConfig {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/public_api.rs // file: crates/ksp-config-lib/tests/public_api.rs
// version: 13 // version: 14
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity, Logging-adapter and //! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity, Logging-adapter and
//! management contracts. //! management contracts.
@@ -219,6 +219,12 @@ fn management_contracts_are_available_from_crate_root() {
assert_eq!(logging.profiles()[0].profile_id(), "local_dev"); assert_eq!(logging.profiles()[0].profile_id(), "local_dev");
assert_eq!(logging.profiles()[0].files().len(), 2); assert_eq!(logging.profiles()[0].files().len(), 2);
assert!(!logging.profiles()[0].files()[0].ansi()); assert!(!logging.profiles()[0].files()[0].ansi());
if let std::option::Option::Some(profile) = logging.profiles_mut().first_mut() {
if let std::option::Option::Some(file) = profile.files_mut().first_mut() {
file.set_ansi(false);
assert!(!file.ansi());
}
}
} }
} }
let save_source_candidate: fn( let save_source_candidate: fn(

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/unit_tests/management.rs // file: crates/ksp-config-lib/unit_tests/management.rs
// version: 3 // version: 4
static NEXT_FIXTURE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); static NEXT_FIXTURE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
@@ -211,6 +211,15 @@ fn raw_source_candidate_rejects_schema_and_unknown_file_ids_without_filesystem_e
cleanup_fixture(&fixture); cleanup_fixture(&fixture);
} }
#[test]
fn logging_file_ansi_mutator_completes_typed_management_surface() {
let filter = crate::LoggingOutputFilterConfig::new("info", std::vec!["*".to_owned()], std::vec!["*".to_owned()]);
let mut file = crate::LoggingFileConfig::new("file.test", true, "test.log", "daily", "human", filter);
assert!(!file.ansi());
file.set_ansi(true);
assert!(file.ansi());
}
#[test] #[test]
fn typed_logging_document_can_be_mutated_validated_and_persisted_atomically() { fn typed_logging_document_can_be_mutated_validated_and_persisted_atomically() {
let fixture = management_fixture(); let fixture = management_fixture();

114
deltas/0.1.4/pre.015.md Normal file
View File

@@ -0,0 +1,114 @@
<!-- file: deltas/0.1.4/pre.015.md -->
<!-- version: 1 -->
# Delta `0.1.4-pre.015` — Logging editor, mutations/persistence
## Statut
Tranche livrée pour validation locale.
`0.1.4-pre.014` est considérée validée : `fmt`, `check`, `clippy`, les 46 tests Config Desk, les 87 tests Config, les cinq audits ownership et les 12 tests d'API publique passent. La vue Logging affiche correctement le document typé, la console, les deux file sinks et les trois target filters ; le refresh relit la source sans modifier le runtime.
## Objectif
Transformer la projection Logging read-only en **éditeur typé persistant**, sans encore reconfigurer le runtime actif. Le frontend manipule un brouillon structuré ; `ksp-config-lib` reste l'unique autorité de validation et de persistence de `std.logging.json`.
## Modifications
### Backend
- les DTO Logging imbriqués acceptent aussi la désérialisation pour former un candidat typé ;
- ajout de `LoggingDocumentCandidateDto` ;
- ajout de `LoggingDocumentSaveResultDto` avec `source_changed`, `reload_required` et document rechargé ;
- nouvelle commande Tauri centralisée `save_logging_document` ;
- reconstruction explicite du candidat avec les contrats publics :
- `LoggingConfigDocument` ;
- `LoggingProfileConfig` ;
- `LoggingConsoleConfig` ;
- `LoggingFileConfig` ;
- `LoggingOutputFilterConfig` ;
- `LoggingTargetFilterConfig` ;
- persistence exclusive via `ConfigManagement::save_logging_document()` ;
- après succès, relecture immédiate du document typé ;
- logs limités aux compteurs, identifiants non sensibles et booléens de changement ;
- aucun appel à `ksp_logging_lib::reinitialize` dans cette tranche.
### Extension Config minimale
- ajout de `LoggingFileConfig::set_ansi()` pour compléter la surface mutable du contrat typed source ;
- la validation existante continue d'imposer `ansi=false` aux file sinks persistants.
### Frontend
- `logs_directory` éditable ;
- `default_profile` éditable ;
- gestion du brouillon profils :
- créer ;
- cloner ;
- renommer ;
- supprimer avec modal Bootstrap ;
- édition de `default_filter` et `span_events` ;
- édition complète de la console et de son filtre ;
- gestion 0/1/N file sinks : ajout, modification, retrait du brouillon ;
- gestion 0/1/N target filters : ajout, modification, retrait du brouillon ;
- selectors `targets`/`domains` saisis comme listes séparées par virgules puis projetés en tableaux ;
- bouton **Sauvegarder** désactivé tant que le brouillon est propre ;
- **Recharger** refuse de jeter silencieusement un brouillon sale et demande confirmation via modal Bootstrap ;
- une sauvegarde refusée conserve le brouillon et laisse la source inchangée ;
- le résultat affiche `source_changed` et `reload_required`, avec rappel explicite que le runtime reste inchangé.
### Tests
- round-trip DTO éditeur -> contrats typed Config ;
- conservation d'un profil multi-fichiers et de ses target filters ;
- les exports TS-RS couvrent les nouveaux DTO candidate/save result ;
- la validation atomique proprement dite reste couverte par les tests `ksp-config-lib` existants.
### Documentation
- `pre.014` marquée réalisée ;
- `pre.015` marquée en cours ;
- README/USAGE/TODO alignés sur l'éditeur mutable et la séparation persistence/runtime.
## Hors scope maintenu
Cette tranche ne :
- recharge pas le subscriber Logging ;
- ne change pas le `LoggingGuard` actif ;
- ne choisit pas encore un profil runtime à appliquer ;
- ne crée pas encore les noms de fichiers uniques par lancement ;
- n'introduit pas encore le panneau de test Logging.
Ces éléments appartiennent respectivement à `pre.016` et `pre.017`.
## Scénarios de validation
1. modifier `default_filter` d'un profil, sauvegarder, puis vérifier `source_changed=true` et `reload_required=true` ;
2. recharger et constater que la valeur persiste dans `std.logging.json` ;
3. cloner `local_dev` vers `local_test`, définir `default_profile=local_test`, sauvegarder puis recharger ;
4. renommer le clone et vérifier que `default_profile` suit automatiquement si le profil renommé est le default ;
5. ajouter un troisième file sink, sauvegarder, puis vérifier qu'il réapparaît après reload ;
6. retirer un sink pour revenir à un seul, puis à zéro si souhaité : le document doit rester valide ;
7. ajouter/retirer un target filter ;
8. produire volontairement un candidat invalide, par exemple `output_id` invalide ou targets vides : la sauvegarde doit être refusée et le fichier existant doit rester inchangé ;
9. avec un brouillon sale, **Recharger** doit ouvrir un modal Bootstrap ; **Annuler** conserve le brouillon, **Abandonner et recharger** restaure la source ;
10. vérifier qu'aucune de ces sauvegardes ne change encore le runtime Logging actif pendant le même lancement.
## Version technique
```text
workspace.package.version = "0.1.4-pre.15"
```
## Validation locale demandée
```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 tree -p ksp-app-config-desk
cargo tauri dev -c crates/ksp-app-config-desk/tauri.conf.json
```

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md --> <!-- file: docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md -->
<!-- version: 16 --> <!-- version: 17 -->
# Plan `0.1.4` — `ksp-app-config-desk` # Plan `0.1.4` — `ksp-app-config-desk`
@@ -238,8 +238,8 @@ La gestion npm distingue le runtime frontend du tooling. Les bibliothèques cons
Chaque application Tauri desk KSP reçoit un couple de ports Vite/HMR propre. La première application réserve : Chaque application Tauri desk KSP reçoit un couple de ports Vite/HMR propre. La première application réserve :
| Application | Port Vite HTTP | Port HMR | | Application | Port Vite HTTP | Port HMR |
| --------------------- | -------------: | -------: | |-----------------------|---------------:|---------:|
| `ksp-app-config-desk` | `1430` | `1431` | | `ksp-app-config-desk` | `1430` | `1431` |
Les applications suivantes incrémentent le couple de deux ports (`1432/1433`, puis `1434/1435`, etc.). Vite doit utiliser un port strict afin qu'une collision soit signalée au lieu de provoquer un basculement silencieux vers un autre port. Cette allocation permet de faire fonctionner simultanément plusieurs applications desk en mode développement. Les applications suivantes incrémentent le couple de deux ports (`1432/1433`, puis `1434/1435`, etc.). Vite doit utiliser un port strict afin qu'une collision soit signalée au lieu de provoquer un basculement silencieux vers un autre port. Cette allocation permet de faire fonctionner simultanément plusieurs applications desk en mode développement.
@@ -249,20 +249,20 @@ Aucune dépendance n'est ajoutée par `pre.001`. Les versions suivantes ont ét
| Dépendance | Version actuelle auditée | Usage envisagé | | Dépendance | Version actuelle auditée | Usage envisagé |
| ------------------------------- | -----------------------: | -------------------------------------------- | | ------------------------------- | -----------------------: | -------------------------------------------- |
| `tauri` | `2.11.5` | runtime desktop | | `tauri` | `2.11.5` | runtime desktop |
| `tauri-build` | `2.6.3` | build Tauri | | `tauri-build` | `2.6.3` | build Tauri |
| `tauri-plugin-tracing` | `0.3.4` | intégration tracing à la frontière Tauri | | `tauri-plugin-tracing` | `0.3.4` | intégration tracing à la frontière Tauri |
| `ts-rs` | `12.0.1` | bindings DTO applicatifs | | `ts-rs` | `12.0.1` | bindings DTO applicatifs |
| `fs2` | `0.4.3` | verrou single-instance | | `fs2` | `0.4.3` | verrou single-instance |
| `@fltsci/tauri-plugin-tracing` | `0.3.4` | package frontend compagnon du plugin tracing | | `@fltsci/tauri-plugin-tracing` | `0.3.4` | package frontend compagnon du plugin tracing |
| `@tauri-apps/api` | `2.11.1` | frontend Tauri | | `@tauri-apps/api` | `2.11.1` | frontend Tauri |
| `@tauri-apps/cli` | `2.11.4` | dev/build desktop | | `@tauri-apps/cli` | `2.11.4` | dev/build desktop |
| `@types/bootstrap` | `5.2.11` | typings Bootstrap | | `@types/bootstrap` | `5.2.11` | typings Bootstrap |
| `vite` | `8.2.1` | build frontend | | `vite` | `8.2.1` | build frontend |
| `typescript` | `7.0.2` | frontend TypeScript | | `typescript` | `7.0.2` | frontend TypeScript |
| `sass-embedded` | `1.102.0` | compilation SCSS | | `sass-embedded` | `1.102.0` | compilation SCSS |
| `bootstrap` | `5.3.8` | composants/layout | | `bootstrap` | `5.3.8` | composants/layout |
| `@fortawesome/fontawesome-free` | `7.3.1` | iconographie | | `@fortawesome/fontawesome-free` | `7.3.1` | iconographie |
Sources d'audit : Sources d'audit :
@@ -694,23 +694,23 @@ Le shell applique également une instrumentation frontend systématique : clics
Les noms ci-dessous sont les noms fonctionnels cibles ; ils pourront être normalisés avant implémentation, mais leurs responsabilités sont fixées. Les noms ci-dessous sont les noms fonctionnels cibles ; ils pourront être normalisés avant implémentation, mais leurs responsabilités sont fixées.
| Commande Tauri | Service interne | API KSP principale | Secret réel ? | | Commande Tauri | Service interne | API KSP principale | Secret réel ? |
| ------------------------------ | ------------------------------------ | --------------------------------------------------------------------------- | -------------------------------------------: | | ------------------------------ | ------------------------------------ | --------------------------------------------------------------------------- | -------------------------------------------: |
| `get_app_snapshot` | `config_service` + `logging_service` | registry/management + runtime state | non | | `get_app_snapshot` | `config_service` + `logging_service` | registry/management + runtime state | non |
| `list_config_documents` | `config_service` | future vue publique du `ConfigFileRegistry` | non | | `list_config_documents` | `config_service` | future vue publique du `ConfigFileRegistry` | non |
| `inspect_config_document` | `config_service` | `load_validated_document` + `read_source` si erreur | non | | `inspect_config_document` | `config_service` | `load_validated_document` + `read_source` si erreur | non |
| `save_config_source_candidate` | `config_service` | future API Config de réparation validée | non | | `save_config_source_candidate` | `config_service` | future API Config de réparation validée | non |
| `inspect_profile` | `config_service` | `load_resolved_profile` + résolution détaillée | non | | `inspect_profile` | `config_service` | `load_resolved_profile` + résolution détaillée | non |
| `get_environment_report` | `environment_service` | `ConfigManagement::environment_report` | non | | `get_environment_report` | `environment_service` | `ConfigManagement::environment_report` | non |
| `set_dotenv_value` | `environment_service` | `ConfigManagement::set_dotenv_value` + rapport frais | entrée potentiellement Secret, jamais loggée | | `set_dotenv_value` | `environment_service` | `ConfigManagement::set_dotenv_value` + rapport frais | entrée potentiellement Secret, jamais loggée |
| `remove_dotenv_value` | `environment_service` | `ConfigManagement::remove_dotenv_value` + rapport frais | non | | `remove_dotenv_value` | `environment_service` | `ConfigManagement::remove_dotenv_value` + rapport frais | non |
| `reveal_environment_value` | `secret_service` | `reveal_effective_environment_value` ou `reveal_dotenv_value` | **oui** | | `reveal_environment_value` | `secret_service` | `reveal_effective_environment_value` ou `reveal_dotenv_value` | **oui** |
| `get_logging_editor` | `logging_service` | `load_logging_document` | non | | `get_logging_editor` | `logging_service` | `load_logging_document` | non |
| `save_logging_document` | `logging_service` | `save_logging_document` | non par design Logging | | `save_logging_document` | `logging_service` | `save_logging_document` | non par design Logging |
| `apply_logging_profile` | `logging_service` | `ConfigEnvironment::load` + `load_resolved_logging_config` + `reinitialize` | valeurs resolved possibles, jamais DTO/log | | `apply_logging_profile` | `logging_service` | `ConfigEnvironment::load` + `load_resolved_logging_config` + `reinitialize` | valeurs resolved possibles, jamais DTO/log |
| `emit_frontend_log` | `frontend_logging_service` | façade `ksp-logging-lib` | non, message technique frontend seulement | | `emit_frontend_log` | `frontend_logging_service` | façade `ksp-logging-lib` | non, message technique frontend seulement |
| `emit_logging_test` | `logging_service` | façade/macros `ksp-logging-lib` | non, message explicitement saisi pour test | | `emit_logging_test` | `logging_service` | façade/macros `ksp-logging-lib` | non, message explicitement saisi pour test |
| `splash_frontend_ready` | `splash` | lifecycle Tauri + settings splash déjà résolus | non | | `splash_frontend_ready` | `splash` | lifecycle Tauri + settings splash déjà résolus | non |
Tous les wrappers dans `tauri.rs` : Tous les wrappers dans `tauri.rs` :
@@ -1233,12 +1233,12 @@ pre.013 Secrets privilégiés [réalisé]
- édition Secret sans préchargement réel - édition Secret sans préchargement réel
- audits non-divulgation - audits non-divulgation
pre.014 Logging editor — lecture/DTO [en cours] pre.014 Logging editor — lecture/DTO [réalisé]
- document/profils/console/files/filters - document/profils/console/files/filters
- targets/domains - targets/domains
- mapping types Config -> DTO - mapping types Config -> DTO
pre.015 Logging editor — mutations/persistence pre.015 Logging editor — mutations/persistence [en cours]
- create/clone/rename/delete profils - create/clone/rename/delete profils
- default_profile - default_profile
- mono-fichier/multi-fichiers - mono-fichier/multi-fichiers