v0.1.4-pre.016

This commit is contained in:
2026-08-16 19:00:49 +02:00
parent d92eb01bc2
commit 5aa538ed5d
29 changed files with 994 additions and 73 deletions

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-app-config-desk/Cargo.toml
# version: 5
# version: 6
[package]
name = "ksp-app-config-desk"
@@ -30,6 +30,7 @@ serde.workspace = true
serde_json.workspace = true
tauri.workspace = true
tauri-plugin-tracing.workspace = true
time.workspace = true
tokio.workspace = true
ts-rs.workspace = true

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/README.md -->
<!-- version: 18 -->
<!-- version: 19 -->
# `ksp-app-config-desk`
@@ -30,6 +30,8 @@ Le gabarit desktop actif fournit maintenant :
- 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 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 ;
- lobservabilité du runtime Logging actif : profil, source de sélection, génération, console, compteurs de drop, identité de lancement et prefixes fichiers effectifs ;
- lapplication explicite dun profil Logging persisté sans modifier `default_profile` ni le document source ;
- `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 ;
- la destination frontend externe commune à Tauri et Vite;
@@ -145,7 +147,11 @@ L'inspection expose le profil par défaut ou une sélection explicite, les vues
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.
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 et appliquer**. Le backend reconstruit les types publics Config et appelle `ConfigManagement::save_logging_document()`, qui valide la totalité du candidat avant remplacement atomique. Après persistence, Config Desk recharge un `ConfigEnvironment` frais, résout le `default_profile`, puis appelle `ksp_logging_lib::reinitialize()` sur le `LoggingGuard` actif. Le hot reload est immédiat et `logging_generation` avance uniquement après succès. Si l'application runtime échoue, l'ancien runtime reste actif et la source précédente est restaurée. **Recharger le document** ne modifie que le brouillon/source persistée.
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. Le frontend maintient un brouillon typé : create/clone/rename/delete de profils, 0/1/N file sinks et target filters restent locaux jusqu'à **Sauvegarder et appliquer**. Le backend reconstruit les types publics Config et appelle `ConfigManagement::save_logging_document()`, qui valide la totalité du candidat avant remplacement atomique. Après persistence, Config Desk recharge un `ConfigEnvironment` frais, résout le `default_profile`, puis appelle `ksp_logging_lib::reinitialize()` sur le `LoggingGuard` actif. Le hot reload est immédiat et `logging_generation` avance uniquement après succès. Si l'application runtime échoue, l'ancien runtime reste actif et la source précédente est restaurée. **Recharger le document** ne modifie que le brouillon/source persistée.
`pre.016` distingue en plus le **profil default persistant** du **profil runtime actif**. La section **Runtime actif** expose le profil actuellement appliqué, `selection_source` (`default_profile`, `explicit` ou `fallback`), la génération, l'état console, les compteurs de lignes abandonnées et les file sinks réellement actifs. Un profil déjà persisté peut être appliqué explicitement sans modifier `default_profile` ni écrire le document ; cette action est désactivée tant que le brouillon contient des changements non sauvegardés.
Chaque lancement de Config Desk crée aussi une `LoggingRuntimeIdentity` stable : `application_id` + timestamp UTC de démarrage + PID. `ksp-logging-lib` utilise cette identité pour préfixer les noms des fichiers actifs et la conserve pendant tous les hot reloads du même processus. Deux lancements distincts ne partagent donc plus le même fichier persistant, même avec une rotation `daily`. Exemple d'un prefix effectif : `ksp-app-config-desk.20260816-182519.123Z-p4242.ksp-debug.log`. Le path Config reste inchangé ; l'identité appartient au runtime, pas au document source.
## Traçabilité frontend

View File

@@ -1,13 +1,12 @@
<!-- file: crates/ksp-app-config-desk/TODO.md -->
<!-- version: 15 -->
<!-- version: 16 -->
# TODO `ksp-app-config-desk`
Le gabarit frontend, le bootstrap Config/Logging, `AppState`, le `LoggingGuard` durable, les premiers DTO TS-RS, le bridge frontend logging KSP, le lifecycle `splash -> main`, les panneaux Documents, Profils/provenance et Environnement avec management `.env`/reveal Secret privilégié, ainsi que l'édition typée/persistence atomique du document Logging et son hot reload transactionnel sont maintenant présents.
Le gabarit frontend, le bootstrap Config/Logging, `AppState`, le `LoggingGuard` durable, les premiers DTO TS-RS, le bridge frontend logging KSP, le lifecycle `splash -> main`, les panneaux Documents, Profils/provenance et Environnement avec management `.env`/reveal Secret privilégié, ainsi que l'édition typée/persistence atomique du document Logging, son hot reload transactionnel, la sélection explicite du profil runtime et les fichiers isolés par identité de lancement sont maintenant présents.
Les prochaines tranches doivent encore :
- compléter le runtime Logging par les fichiers uniques par lancement (`app-name.<start-timestamp>.<ext>`) et la sélection explicite du profil à appliquer ;
- ramener le target applicatif de `debug` à `info` ou `warn` avant clôture de la release ;
- démontrer le hot reload Logging avec le panneau de test de routage ;
- démontrer le routing `trace/debug/info/warn/error`, targets et domains avant/après hot reload avec le panneau Test Logging ;
- étudier puis intégrer le retour Rust -> console WebKit via `tauri_plugin_tracing::WebviewLayer` dans le subscriber possédé par `ksp-logging-lib`, afin de pouvoir utiliser `attachConsole()` sans second subscriber, double émission ni boucle avec le bridge KSP.

View File

@@ -1,11 +1,11 @@
<!-- file: crates/ksp-app-config-desk/USAGE.md -->
<!-- version: 18 -->
<!-- version: 19 -->
# Utilisation de `ksp-app-config-desk`
## État actuel
Le gabarit Rust/Tauri et le frontend Vite/TypeScript/SCSS sont présents. Le backend initialise Config, le runtime Logging et `AppState`. Les panneaux Documents, Profils et Environnement/`.env` sont fonctionnels, y compris les mutations `.env` create/update/remove et les reveals Secrets privilégiés. Logging expose maintenant un brouillon typé éditable, une persistence atomique et un hot reload runtime sans redémarrage.
Le gabarit Rust/Tauri et le frontend Vite/TypeScript/SCSS sont présents. Le backend initialise Config, le runtime Logging et `AppState`. Les panneaux Documents, Profils et Environnement/`.env` sont fonctionnels, y compris les mutations `.env` create/update/remove et les reveals Secrets privilégiés. Logging expose maintenant un brouillon typé éditable, une persistence atomique, un hot reload sans redémarrage, un profil runtime sélectionnable indépendamment du `default_profile` et une identité de lancement observable pour isoler les fichiers persistants.
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.
@@ -212,3 +212,51 @@ La vue **Logging** appelle `get_logging_document`. Le backend charge le document
Le sélecteur **Profil édité** 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 et appliquer** envoie un candidat typé à `save_logging_document`, qui reconstruit les contrats Config, persiste atomiquement après validation, recharge un `ConfigEnvironment` frais, résout le `default_profile` et hot-reload le `LoggingGuard`. Si le runtime ne peut pas être préparé/rechargé, l'ancien runtime reste actif et la source précédente est restaurée. **Recharger le document** resynchronise uniquement le brouillon depuis la source persistée et demande confirmation si des changements non sauvegardés existent. Les messages Cargo/Tauri/Vite affichés par `cargo tauri dev` sont externes au runtime Logging KSP et ne dépendent pas de `console.enabled`.
## Runtime Logging — profil actif, génération et fichiers par lancement
La section **Runtime actif** du panneau Logging ne décrit pas le brouillon : elle interroge l'état réellement installé dans `ksp-logging-lib`.
Elle expose :
- `active_profile` ;
- `selection_source` : `default_profile`, `explicit` ou `fallback` ;
- `generation` ;
- console active/inactive ;
- compteurs cumulés de lignes abandonnées ;
- `application_id` ;
- `launch_timestamp` ;
- chaque file sink actif avec son directory, son prefix effectif et sa rotation.
Au démarrage de Config Desk, l'identité de lancement est construite une seule fois avec un token de la forme :
```text
20260816-182519.123Z-p4242
```
Pour un fichier source configuré comme `debug/ksp-debug.log`, le runtime peut donc exposer un prefix comme :
```text
ksp-app-config-desk.20260816-182519.123Z-p4242.ksp-debug.log
```
Le même `launch_timestamp` doit rester affiché après chaque hot reload du processus courant. Après fermeture puis nouveau lancement de l'application, il doit changer. La rotation `daily` ou `hourly` peut ajouter sa composante de rotation, mais deux lancements ne doivent plus partager le même prefix applicatif.
### Sélection explicite du profil runtime
`default_profile` reste une propriété persistée de `std.logging.json`. Le sélecteur **Profil runtime à appliquer** travaille uniquement avec les profils déjà persistés et ne modifie pas ce champ.
Scénario attendu avec deux profils persistés `local_dev` et `local_test` :
1. garder `default_profile=local_dev` ;
2. sélectionner `local_test` dans **Profil runtime à appliquer** ;
3. cliquer **Appliquer le profil** ;
4. vérifier `active_profile=local_test`, `selection_source=explicit` et `generation + 1` ;
5. cliquer **Recharger le document** : `default_profile` doit toujours être `local_dev` ;
6. sélectionner/appliquer `local_dev` explicitement si souhaité ;
7. **Sauvegarder et appliquer** un document valide : le runtime revient au `default_profile` du document et `selection_source=default_profile`.
Le bouton d'application explicite est désactivé lorsque le brouillon est sale afin qu'un profil affiché mais non persisté ne soit jamais confondu avec un profil réellement chargeable par Config.
Si la résolution ou la préparation du profil explicite échoue, `ksp_logging_lib::reinitialize()` ne remplace pas les layers actifs et la génération ne doit pas avancer. Une sauvegarde de document qui échoue pendant l'application runtime conserve également l'ancien runtime et restaure la source précédente lorsqu'elle avait été modifiée.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
<!-- version: 13 -->
<!-- version: 14 -->
<!DOCTYPE html>
<html lang="fr">
@@ -310,7 +310,7 @@
</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
Sauvegarder et appliquer
</button>
</div>
</div>
@@ -324,6 +324,42 @@
<dd id="loggingFormatVersion" class="col-lg-9 mb-0 font-monospace"></dd>
</dl>
<section class="border rounded p-3 mb-4 bg-body-tertiary" aria-label="Runtime Logging actif">
<div class="d-flex flex-wrap gap-2 align-items-start justify-content-between mb-3">
<div>
<h3 class="h6 mb-1">Runtime actif</h3>
<p class="text-body-secondary small mb-0">Le profil runtime peut être appliqué explicitement sans modifier <code>default_profile</code>. L'identité de lancement reste stable pendant tous les hot reloads du processus.</p>
</div>
<button id="refreshLoggingRuntime" class="btn btn-outline-secondary btn-sm" type="button">Rafraîchir runtime</button>
</div>
<dl class="row small mb-3 app-profile-metadata">
<dt class="col-xl-3">Profil actif</dt><dd id="loggingRuntimeActiveProfile" class="col-xl-3 font-monospace"></dd>
<dt class="col-xl-3">Source sélection</dt><dd id="loggingRuntimeSelectionSource" class="col-xl-3 font-monospace"></dd>
<dt class="col-xl-3">Génération</dt><dd id="loggingRuntimeGeneration" class="col-xl-3 font-monospace"></dd>
<dt class="col-xl-3">Console</dt><dd id="loggingRuntimeConsole" class="col-xl-3"></dd>
<dt class="col-xl-3">Fallback</dt><dd id="loggingRuntimeFallback" class="col-xl-3"></dd>
<dt class="col-xl-3">Dropped lines</dt><dd id="loggingRuntimeDropped" class="col-xl-3 font-monospace"></dd>
<dt class="col-xl-3">Application</dt><dd id="loggingRuntimeApplicationId" class="col-xl-3 font-monospace"></dd>
<dt class="col-xl-3">Launch timestamp</dt><dd id="loggingRuntimeLaunchTimestamp" class="col-xl-3 font-monospace"></dd>
</dl>
<div class="row g-2 align-items-end mb-3">
<div class="col-xl-8">
<label class="form-label" for="loggingRuntimeProfileSelect">Profil persisté à appliquer explicitement</label>
<select id="loggingRuntimeProfileSelect" class="form-select" disabled></select>
<div class="form-text">Disponible uniquement lorsque le brouillon est synchronisé avec la source persistée.</div>
</div>
<div class="col-xl-4">
<button id="applyLoggingRuntimeProfile" class="btn btn-outline-primary w-100" type="button" disabled>Appliquer ce profil au runtime</button>
</div>
</div>
<div class="table-responsive">
<table id="loggingRuntimeFilesTable" class="table table-sm align-middle mb-0 w-100">
<thead><tr><th>output_id</th><th>Répertoire</th><th>Préfixe fichier effectif</th><th>Rotation</th></tr></thead>
<tbody></tbody>
</table>
</div>
</section>
<section class="border rounded p-3 mb-4 bg-body-tertiary" aria-label="Globals Logging">
<h3 class="h6 mb-3">Globals</h3>
<div class="row g-3">
@@ -438,7 +474,7 @@
</section>
<div class="alert alert-warning small py-2 mb-2" role="note">
<strong>Runtime actif :</strong> <strong>Sauvegarder et appliquer</strong> persiste le candidat puis hot-reload immédiatement le subscriber KSP avec un environnement frais. En cas d'échec d'application, l'ancien runtime reste actif et la source précédente est restaurée. <strong>Recharger le document</strong> resynchronise uniquement le brouillon.
<strong>Runtime actif :</strong> <strong>Sauvegarder et appliquer</strong> persiste le candidat puis applique son <code>default_profile</code>. <strong>Appliquer ce profil au runtime</strong> sélectionne un profil persisté sans modifier le document. En cas d'échec, l'ancien runtime et sa génération restent actifs. <strong>Recharger le document</strong> resynchronise uniquement le brouillon.
</div>
<div id="loggingSaveReport" class="small text-body-secondary mb-2">Aucune sauvegarde/application 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>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/logging.ts
// version: 4
// version: 5
//! Typed Logging editor backed by ConfigManagement persistence and KSP-owned runtime hot reload.
@@ -10,6 +10,7 @@ import type { LoggingDocumentSaveResultDto } from "./bindings/ksp_app_config_des
import type { LoggingFileDto } from "./bindings/ksp_app_config_desk/logging/LoggingFileDto";
import type { LoggingProfileDto } from "./bindings/ksp_app_config_desk/logging/LoggingProfileDto";
import type { LoggingTargetFilterDto } from "./bindings/ksp_app_config_desk/logging/LoggingTargetFilterDto";
import type { LoggingRuntimeStatusDto } from "./bindings/ksp_app_config_desk/logging_runtime/LoggingRuntimeStatusDto";
import { frontendDebug, frontendTrace } from "./frontend_log";
import { invokeKsp } from "./invoke";
@@ -18,6 +19,7 @@ const ROTATIONS = ["never", "hourly", "daily"] as const;
const FORMATS = ["human", "compact", "pretty", "json"] as const;
let draft: LoggingDocumentCandidateDto | null = null;
let persistedDocument: LoggingDocumentDto | null = null;
let selectedProfileId = "";
let dirty = false;
@@ -104,6 +106,7 @@ function markDirty(reason: string): void {
saveButton.disabled = false;
}
setLoggingStatus("Brouillon modifié. Sauvegarde nécessaire pour persister std.logging.json.", "warning");
updateRuntimeActionState();
frontendTrace("main", "Logging draft changed", { reason, selectedProfileId });
}
@@ -295,7 +298,130 @@ function renderSelectedProfile(source: "load" | "selection" | "mutation"): void
});
}
function updateRuntimeActionState(): void {
const select = document.querySelector<HTMLSelectElement>("#loggingRuntimeProfileSelect");
const apply = document.querySelector<HTMLButtonElement>("#applyLoggingRuntimeProfile");
if (select) {
select.disabled = persistedDocument === null || dirty;
}
if (apply) {
apply.disabled = persistedDocument === null || dirty || !select || select.value.length === 0;
}
}
function renderRuntimeProfileSelect(): void {
const select = document.querySelector<HTMLSelectElement>("#loggingRuntimeProfileSelect");
if (!select || !persistedDocument) {
updateRuntimeActionState();
return;
}
const previous = select.value;
select.replaceChildren();
for (const profile of persistedDocument.profiles) {
const option = document.createElement("option");
option.value = profile.profileId;
option.textContent = profile.profileId === persistedDocument.defaultProfile ? `${profile.profileId} (default)` : profile.profileId;
select.append(option);
}
if (persistedDocument.profiles.some(profile => profile.profileId === previous)) {
select.value = previous;
} else {
select.value = persistedDocument.defaultProfile;
}
updateRuntimeActionState();
}
function renderRuntimeFiles(status: LoggingRuntimeStatusDto): void {
const body = document.querySelector<HTMLTableSectionElement>("#loggingRuntimeFilesTable tbody");
if (!body) {
return;
}
body.replaceChildren();
for (const file of status.files) {
const row = document.createElement("tr");
const values: Array<{ value: string; monospace: boolean }> = [
{ value: file.outputId, monospace: true },
{ value: file.directory, monospace: true },
{ value: file.fileNamePrefix, monospace: true },
{ value: file.rotation, monospace: false },
];
for (const item of values) {
const cell = document.createElement("td");
cell.textContent = item.value;
if (item.monospace) {
cell.classList.add("font-monospace");
}
row.append(cell);
}
body.append(row);
}
}
function renderRuntimeStatus(status: LoggingRuntimeStatusDto): void {
setText("#loggingRuntimeActiveProfile", status.activeProfile ?? "fallback transitoire");
setText("#loggingRuntimeSelectionSource", status.selectionSource);
setText("#loggingRuntimeGeneration", status.generation.toString());
setText("#loggingRuntimeFallback", status.fallbackActive ? "oui" : "non");
setText("#loggingRuntimeApplicationId", status.applicationId);
setText("#loggingRuntimeLaunchTimestamp", status.launchTimestamp);
setText("#loggingRuntimeConsole", status.consoleEnabled ? "active" : "désactivée");
setText("#loggingRuntimeDropped", `${status.droppedTotalLines} total · console=${status.droppedConsoleLines} · fichiers=${status.droppedFileLines}`);
renderRuntimeFiles(status);
const select = document.querySelector<HTMLSelectElement>("#loggingRuntimeProfileSelect");
if (select && status.activeProfile && persistedDocument?.profiles.some(profile => profile.profileId === status.activeProfile)) {
select.value = status.activeProfile;
}
updateRuntimeActionState();
frontendTrace("main", "Logging runtime metadata rendered", {
generation: status.generation,
selectionSource: status.selectionSource,
fileCount: status.files.length,
consoleEnabled: status.consoleEnabled,
});
}
async function refreshRuntimeStatus(source: "startup" | "save" | "explicit" | "user"): Promise<void> {
if (source === "user") {
frontendDebug("main", "Logging runtime metadata refresh requested");
}
try {
const status = await invokeKsp<LoggingRuntimeStatusDto>("main", "get_logging_runtime_status");
renderRuntimeStatus(status);
frontendDebug("main", "Logging runtime metadata refresh completed", { generation: status.generation, activeProfile: status.activeProfile });
} catch {
setText("#loggingRuntimeActiveProfile", "indisponible");
}
}
async function applyRuntimeProfile(): Promise<void> {
if (dirty) {
setLoggingStatus("Sauvegarde ou abandonne d'abord le brouillon avant d'appliquer un profil runtime explicite.", "warning");
return;
}
const profileId = selectedValue("#loggingRuntimeProfileSelect");
if (profileId.length === 0) {
return;
}
const apply = document.querySelector<HTMLButtonElement>("#applyLoggingRuntimeProfile");
if (apply) {
apply.disabled = true;
}
setLoggingStatus(`Application explicite du profil runtime ${profileId}...`);
frontendDebug("main", "Explicit Logging runtime profile apply requested", { profileId });
try {
const status = await invokeKsp<LoggingRuntimeStatusDto>("main", "apply_logging_profile", { profileId });
renderRuntimeStatus(status);
window.dispatchEvent(new CustomEvent("ksp:logging-runtime-updated"));
setLoggingStatus(`Profil ${profileId} appliqué au runtime sans modifier default_profile. Génération ${status.generation}.`, "success");
frontendDebug("main", "Explicit Logging runtime profile apply completed", { profileId, generation: status.generation });
} catch {
updateRuntimeActionState();
setLoggingStatus("Application explicite refusée. Le runtime précédent et sa génération sont conservés.", "danger");
}
}
function renderDocument(documentDto: LoggingDocumentDto): void {
persistedDocument = structuredClone(documentDto);
draft = {
logsDirectory: documentDto.logsDirectory,
defaultProfile: documentDto.defaultProfile,
@@ -311,6 +437,7 @@ function renderDocument(documentDto: LoggingDocumentDto): void {
setText("#loggingFormatVersion", documentDto.formatVersion.toString());
setInputValue("#loggingLogsDirectoryInput", draft.logsDirectory);
renderProfileSelects();
renderRuntimeProfileSelect();
renderSelectedProfile("load");
setText("#loggingSaveReport", "Aucune sauvegarde effectuée depuis le dernier chargement.");
frontendTrace("main", "Logging document editor rendered", { profileCount: documentDto.profiles.length });
@@ -512,6 +639,8 @@ async function saveLoggingDocument(): Promise<void> {
try {
const result = await invokeKsp<LoggingDocumentSaveResultDto>("main", "save_logging_document", { candidate: draft });
renderDocument(result.document);
await refreshRuntimeStatus("save");
window.dispatchEvent(new CustomEvent("ksp:logging-runtime-updated"));
setText(
"#loggingSaveReport",
`source_changed=${result.sourceChanged} · reload_required=${result.reloadRequired} · runtime_applied=${result.runtimeApplied} · generation=${result.loggingGeneration} · active_profile=${result.activeProfile}`,
@@ -576,9 +705,12 @@ async function refreshLoggingDocument(source: "startup" | "user"): Promise<void>
profileCount: documentDto.profiles.length,
defaultProfile: documentDto.defaultProfile,
});
await refreshRuntimeStatus(source === "startup" ? "startup" : "user");
} catch {
draft = null;
persistedDocument = null;
setEditorEnabled(false);
updateRuntimeActionState();
setLoggingStatus("Le document Logging typé n'a pas pu être chargé. Consulte les diagnostics backend.", "danger");
}
}
@@ -623,6 +755,9 @@ export function initializeLoggingPanel(): void {
bindStaticDraftFields();
document.querySelector<HTMLButtonElement>("#refreshLoggingDocument")?.addEventListener("click", requestReload);
document.querySelector<HTMLButtonElement>("#saveLoggingDocument")?.addEventListener("click", () => void saveLoggingDocument());
document.querySelector<HTMLButtonElement>("#refreshLoggingRuntime")?.addEventListener("click", () => void refreshRuntimeStatus("user"));
document.querySelector<HTMLButtonElement>("#applyLoggingRuntimeProfile")?.addEventListener("click", () => void applyRuntimeProfile());
document.querySelector<HTMLSelectElement>("#loggingRuntimeProfileSelect")?.addEventListener("change", updateRuntimeActionState);
document.querySelector<HTMLSelectElement>("#loggingProfileSelect")?.addEventListener("change", event => {
syncStaticProfileFields();
selectedProfileId = (event.currentTarget as HTMLSelectElement).value;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/ts/main.ts
// version: 10
// version: 11
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
@@ -146,10 +146,18 @@ async function loadSnapshot(): Promise<void> {
frontendTrace("main", "Main shell status replaced", { status: "ready" });
}
function bindRuntimeSnapshotRefresh(): void {
window.addEventListener("ksp:logging-runtime-updated", () => {
frontendDebug("main", "Application snapshot refresh requested after Logging runtime update");
void loadSnapshot();
});
}
async function initializeMain(): Promise<void> {
const windowLabel = getCurrentWindow().label;
frontendInfo("main", "Config Desk main frontend loaded", { windowLabel });
bindNavigation();
bindRuntimeSnapshotRefresh();
initializeDocumentsPanel();
initializeProfilesPanel();
initializeEnvironmentPanel();

View File

@@ -1,11 +1,12 @@
// file: crates/ksp-app-config-desk/src/app_state.rs
// version: 5
// version: 6
//! Shared backend state owned by the Tauri application.
struct LoggingRuntimeState {
guard: ksp_logging_lib::LoggingGuard,
active_profile_id: std::option::Option<String>,
selection_source: String,
generation: u32,
fallback_active: bool,
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
@@ -27,7 +28,12 @@ impl AppState {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logging_startup = crate::initialize_logging(&config_management);
let runtime_identity = crate::logging_runtime::launch_identity();
let runtime_identity = match runtime_identity {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logging_startup = crate::initialize_logging(&config_management, &runtime_identity);
let logging_startup = match logging_startup {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -61,6 +67,7 @@ impl AppState {
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
guard: logging_startup.guard,
active_profile_id: logging_startup.active_profile_id,
selection_source: logging_startup.selection_source,
generation: 1,
fallback_active: logging_startup.fallback_active,
startup_diagnostic: logging_startup.startup_diagnostic,
@@ -111,7 +118,12 @@ impl AppState {
}
/// Replaces the active Logging runtime with freshly resolved settings and advances the runtime generation only after success.
pub(crate) fn reinitialize_logging_runtime(&self, profile_id: &str, settings: &ksp_logging_lib::LoggingSettings) -> ksp_core_lib::Result<u32> {
pub(crate) fn reinitialize_logging_runtime(
&self,
profile_id: &str,
selection_source: &str,
settings: &ksp_logging_lib::LoggingSettings,
) -> ksp_core_lib::Result<u32> {
let runtime = self.logging_runtime.lock();
let mut runtime = match runtime {
std::result::Result::Ok(value) => value,
@@ -127,12 +139,59 @@ impl AppState {
return std::result::Result::Err(error);
}
runtime.active_profile_id = std::option::Option::Some(profile_id.to_owned());
runtime.selection_source = selection_source.to_owned();
runtime.generation = runtime.generation.saturating_add(1);
runtime.fallback_active = false;
runtime.startup_diagnostic = std::option::Option::None;
return std::result::Result::Ok(runtime.generation);
}
/// Returns a safe observable snapshot of the currently active Logging runtime.
pub(crate) fn logging_runtime_status(&self) -> ksp_core_lib::Result<crate::LoggingRuntimeStatusDto> {
let runtime = self.logging_runtime.lock();
let runtime = match runtime {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_LOCK_FAILED,
"Config Desk Logging runtime state lock is poisoned",
));
},
};
let identity = runtime.guard.runtime_identity();
let identity = match identity {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Config Desk Logging runtime is missing its application launch identity",
));
},
};
let console_enabled = match runtime.guard.settings().console() {
std::option::Option::Some(console) => console.enabled(),
std::option::Option::None => false,
};
let dropped = runtime.guard.dropped_lines();
let mut files = std::vec::Vec::<crate::LoggingRuntimeFileDto>::new();
for metadata in runtime.guard.active_file_outputs() {
files.push(crate::logging_runtime::project_file(&metadata));
}
return std::result::Result::Ok(crate::LoggingRuntimeStatusDto {
active_profile: runtime.active_profile_id.clone(),
selection_source: runtime.selection_source.clone(),
generation: runtime.generation,
fallback_active: runtime.fallback_active,
application_id: identity.application_id().to_owned(),
launch_timestamp: identity.launch_timestamp().to_owned(),
console_enabled,
dropped_console_lines: crate::logging_runtime::count_to_u64(dropped.console()),
dropped_file_lines: crate::logging_runtime::count_to_u64(dropped.file()),
dropped_total_lines: crate::logging_runtime::count_to_u64(dropped.total()),
files,
});
}
/// Returns the resolved splash timings captured during application bootstrap.
#[must_use]
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {

View File

@@ -1,11 +1,12 @@
// file: crates/ksp-app-config-desk/src/bootstrap.rs
// version: 1
// version: 2
//! Config and Logging bootstrap for the desktop application.
pub(crate) struct LoggingStartup {
pub(crate) guard: ksp_logging_lib::LoggingGuard,
pub(crate) active_profile_id: std::option::Option<String>,
pub(crate) selection_source: String,
pub(crate) fallback_active: bool,
pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}
@@ -25,20 +26,23 @@ pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_li
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine));
}
pub(crate) fn initialize_logging(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result<LoggingStartup> {
pub(crate) fn initialize_logging(
management: &ksp_config_lib::ConfigManagement,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return initialize_fallback_logging(error),
std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity),
};
let resolved = management.engine().load_resolved_logging_config(std::option::Option::None, &environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return initialize_fallback_logging(error),
std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity),
};
let active_profile_id = resolved.profile_id().to_owned();
let settings = resolved.into_settings();
let guard = ksp_logging_lib::initialize(&settings);
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
return match guard {
std::result::Result::Ok(guard) => {
ksp_logging_lib::info!(
@@ -50,18 +54,22 @@ pub(crate) fn initialize_logging(management: &ksp_config_lib::ConfigManagement)
std::result::Result::Ok(LoggingStartup {
guard,
active_profile_id: std::option::Option::Some(active_profile_id),
selection_source: "default_profile".to_owned(),
fallback_active: false,
startup_diagnostic: std::option::Option::None,
})
},
std::result::Result::Err(error) => initialize_fallback_logging(error),
std::result::Result::Err(error) => initialize_fallback_logging(error, runtime_identity),
};
}
fn initialize_fallback_logging(initial_error: ksp_core_lib::Error) -> ksp_core_lib::Result<LoggingStartup> {
fn initialize_fallback_logging(
initial_error: ksp_core_lib::Error,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<LoggingStartup> {
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
let settings = fallback_logging_settings();
let guard = ksp_logging_lib::initialize(&settings);
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
let guard = match guard {
std::result::Result::Ok(value) => value,
std::result::Result::Err(fallback_error) => {
@@ -83,6 +91,7 @@ fn initialize_fallback_logging(initial_error: ksp_core_lib::Error) -> ksp_core_l
return std::result::Result::Ok(LoggingStartup {
guard,
active_profile_id: std::option::Option::None,
selection_source: "fallback".to_owned(),
fallback_active: true,
startup_diagnostic: std::option::Option::Some(diagnostic),
});

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/constants.rs
// version: 7
// version: 8
//! Application-owned tracing targets and domains.
@@ -27,3 +27,5 @@ pub(crate) const TRACING_DOMAIN_ENVIRONMENT: &str = "config.environment";
pub(crate) const TRACING_DOMAIN_SECRETS: &str = "config.secrets";
/// Structured domain for typed Logging editor inspection.
pub(crate) const TRACING_DOMAIN_LOGGING_EDITOR: &str = "config.logging_editor";
/// Structured domain for active Logging runtime metadata and explicit profile application.
pub(crate) const TRACING_DOMAIN_LOGGING_RUNTIME: &str = "config.logging_runtime";

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/lib.rs
// version: 12
// version: 13
//! Tauri desktop application for managing and validating KSP configuration.
@@ -16,6 +16,7 @@ mod environment;
mod errors;
mod frontend_logging;
mod logging_editor;
mod logging_runtime;
mod profiles;
mod secrets;
mod splash;
@@ -34,6 +35,7 @@ pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS;
pub(crate) use self::constants::TRACING_DOMAIN_ENVIRONMENT;
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_EDITOR;
pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_RUNTIME;
pub(crate) use self::constants::TRACING_DOMAIN_PROFILES;
pub(crate) use self::constants::TRACING_DOMAIN_SECRETS;
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
@@ -70,6 +72,7 @@ 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::LoggingDocumentSaveResultDto;
pub(crate) use self::logging_runtime::{LoggingRuntimeFileDto, LoggingRuntimeStatusDto};
pub(crate) use self::profiles::ConfigProfileDetailDto;
pub(crate) use self::profiles::ConfigProfileDocumentDto;
pub(crate) use self::secrets::SecretRevealRequestDto;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/logging_editor.rs
// version: 3
// version: 4
//! Typed Logging document projection and validated persistence for the Config Desk Logging editor.
@@ -207,7 +207,7 @@ fn apply_persisted_runtime(state: &crate::AppState) -> ksp_core_lib::Result<(Str
};
let profile_id = resolved.profile_id().to_owned();
let settings = resolved.into_settings();
let generation = state.reinitialize_logging_runtime(profile_id.as_str(), &settings);
let generation = state.reinitialize_logging_runtime(profile_id.as_str(), "default_profile", &settings);
return match generation {
std::result::Result::Ok(value) => std::result::Result::Ok((profile_id, value)),
std::result::Result::Err(error) => std::result::Result::Err(error),

View File

@@ -0,0 +1,130 @@
// file: crates/ksp-app-config-desk/src/logging_runtime.rs
// version: 1
//! Runtime Logging metadata, launch identity and explicit profile application for Config Desk.
use ts_rs::TS; // rust-rules: derive-import
/// Metadata for one currently active persistent Logging file output.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging_runtime/LoggingRuntimeFileDto.ts")]
pub(crate) struct LoggingRuntimeFileDto {
/// Stable configured output identifier.
pub(crate) output_id: String,
/// Resolved runtime directory.
pub(crate) directory: String,
/// Effective per-launch filename prefix passed to the rolling appender.
pub(crate) file_name_prefix: String,
/// Effective file rotation cadence.
pub(crate) rotation: String,
}
/// Safe observable state of the currently active KSP Logging runtime.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging_runtime/LoggingRuntimeStatusDto.ts")]
pub(crate) struct LoggingRuntimeStatusDto {
/// Active profile identifier, or `None` while fallback Logging is active.
pub(crate) active_profile: std::option::Option<String>,
/// Source that selected the active runtime profile.
pub(crate) selection_source: String,
/// Monotonic runtime generation.
pub(crate) generation: u32,
/// Whether transient fallback Logging is active.
pub(crate) fallback_active: bool,
/// Stable application identifier embedded in launch-specific file names.
pub(crate) application_id: String,
/// Stable launch timestamp token reused by all hot reloads in this process.
pub(crate) launch_timestamp: String,
/// Whether the currently active console sink is enabled.
pub(crate) console_enabled: bool,
/// Cumulative console lines dropped by non-blocking outputs.
pub(crate) dropped_console_lines: u64,
/// Cumulative file lines dropped by non-blocking outputs.
pub(crate) dropped_file_lines: u64,
/// Cumulative lines dropped across all outputs.
pub(crate) dropped_total_lines: u64,
/// Active persistent file outputs and their launch-specific prefixes.
pub(crate) files: std::vec::Vec<LoggingRuntimeFileDto>,
}
/// Creates the stable runtime identity for this Config Desk process launch.
pub(crate) fn launch_identity() -> ksp_core_lib::Result<ksp_logging_lib::LoggingRuntimeIdentity> {
let now = time::OffsetDateTime::now_utc();
let milliseconds = now.nanosecond() / 1_000_000;
let timestamp = format!(
"{:04}{:02}{:02}-{:02}{:02}{:02}.{:03}Z-p{}",
now.year(),
now.month() as u8,
now.day(),
now.hour(),
now.minute(),
now.second(),
milliseconds,
std::process::id(),
);
return ksp_logging_lib::LoggingRuntimeIdentity::new(crate::TRACING_TARGET, timestamp);
}
/// Returns safe metadata for the currently active Logging runtime.
pub(crate) fn status(state: &crate::AppState) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> {
return state.logging_runtime_status();
}
/// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document.
pub(crate) fn apply_profile(state: &crate::AppState, profile_id: &str) -> ksp_core_lib::Result<LoggingRuntimeStatusDto> {
let environment = ksp_config_lib::ConfigEnvironment::load();
let environment = match environment {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let resolved = state.config_management().engine().load_resolved_logging_config(std::option::Option::Some(profile_id), &environment);
let resolved = match resolved {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let active_profile = resolved.profile_id().to_owned();
let settings = resolved.into_settings();
let generation = state.reinitialize_logging_runtime(active_profile.as_str(), "explicit", &settings);
if let std::result::Result::Err(error) = generation {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_LOGGING_RUNTIME,
active_profile = active_profile.as_str(),
selection_source = "explicit",
"persisted Logging profile applied explicitly to runtime"
);
return state.logging_runtime_status();
}
pub(crate) fn project_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> LoggingRuntimeFileDto {
return LoggingRuntimeFileDto {
output_id: metadata.output_id().to_owned(),
directory: metadata.directory().display().to_string(),
file_name_prefix: metadata.file_name_prefix().to_owned(),
rotation: rotation_label(metadata.rotation()).to_owned(),
};
}
pub(crate) const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str {
return match rotation {
ksp_logging_lib::FileRotation::Never => "never",
ksp_logging_lib::FileRotation::Hourly => "hourly",
ksp_logging_lib::FileRotation::Daily => "daily",
};
}
pub(crate) fn count_to_u64(value: usize) -> u64 {
let converted = u64::try_from(value);
return match converted {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => u64::MAX,
};
}
#[cfg(test)]
#[path = "../unit_tests/logging_runtime.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/tauri.rs
// version: 13
// version: 14
//! Tauri runtime assembly for the KSP configuration desktop application.
@@ -49,6 +49,8 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
remove_environment_value,
reveal_environment_value,
get_logging_document,
get_logging_runtime_status,
apply_logging_profile,
save_logging_document,
emit_frontend_log,
splash_frontend_ready
@@ -181,6 +183,27 @@ fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result
};
}
#[tauri::command]
fn get_logging_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> {
let result = crate::logging_runtime::status(&state);
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
fn apply_logging_profile(
profile_id: String,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> {
let result = crate::logging_runtime::apply_profile(&state, profile_id.as_str());
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
};
}
#[tauri::command]
fn save_logging_document(
candidate: crate::LoggingDocumentCandidateDto,

View File

@@ -0,0 +1,26 @@
// file: crates/ksp-app-config-desk/unit_tests/logging_runtime.rs
// version: 1
#[test]
fn launch_identity_is_safe_and_contains_application_and_process_identity() {
let identity = super::launch_identity();
assert!(identity.is_ok());
if let std::result::Result::Ok(identity) = identity {
assert_eq!(identity.application_id(), crate::TRACING_TARGET);
assert!(identity.launch_timestamp().contains("Z-p"));
assert!(identity.launch_timestamp().ends_with(std::process::id().to_string().as_str()));
}
}
#[test]
fn runtime_rotation_labels_are_stable() {
assert_eq!(super::rotation_label(ksp_logging_lib::FileRotation::Never), "never");
assert_eq!(super::rotation_label(ksp_logging_lib::FileRotation::Hourly), "hourly");
assert_eq!(super::rotation_label(ksp_logging_lib::FileRotation::Daily), "daily");
}
#[test]
fn runtime_count_projection_is_non_lossy_for_normal_values() {
assert_eq!(super::count_to_u64(0), 0);
assert_eq!(super::count_to_u64(42), 42);
}