v0.1.4-pre.009

This commit is contained in:
2026-08-16 14:39:36 +02:00
parent 675350f5bf
commit cb10fd354b
20 changed files with 940 additions and 24 deletions

View File

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

View File

@@ -71,7 +71,9 @@
"target_prefix": "ksp-app-config-desk",
"level": "debug"
}
]
]
}
]
}

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/README.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# `ksp-app-config-desk`
@@ -25,6 +25,8 @@ Le gabarit desktop actif fournit maintenant :
- le frontend Vanilla TypeScript + Vite sous `frontend/` ;
- SCSS, Bootstrap et Font Awesome ;
- SimpleBar + `resize-observer-polyfill` pour le scrolling du shell ;
- DataTables 3 + Select 4 pour linventaire interactif des documents Config ;
- le panneau Documents générique alimenté par le registre `ksp-config-lib`, avec diagnostics backend et réparation validée ;
- `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;

View File

@@ -1,13 +1,14 @@
<!-- file: crates/ksp-app-config-desk/TODO.md -->
<!-- version: 7 -->
<!-- version: 8 -->
# 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 et le lifecycle `splash -> main` 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` et le panneau Documents sont maintenant présents.
Les prochaines tranches doivent encore :
- développer les panneaux Documents, Profils, Environnement/`.env`, Secrets et Logging ;
- introduire DataTables au premier tableau qui exige tri, filtrage, sélection ou checkbox ;
- développer les panneaux Profils, Environnement/`.env`, Secrets et Logging ;
- 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 ;
- démontrer le hot reload Logging et le panneau de test de routage ;
- é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: 8 -->
<!-- version: 9 -->
# Utilisation de `ksp-app-config-desk`
## État actuel
Le gabarit Rust/Tauri et le frontend Vite/TypeScript/SCSS sont présents. Le backend initialise désormais Config, le runtime Logging et `AppState`; les panneaux métier Config restent volontairement absents à ce stade.
Le gabarit Rust/Tauri et le frontend Vite/TypeScript/SCSS sont présents. Le backend initialise Config, le runtime Logging et `AppState`. Le panneau Documents est fonctionnel ; les panneaux Profils, Environnement/`.env`, Secrets et Logging restent à 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.
@@ -136,3 +136,20 @@ Seule la vue d'ensemble consomme déjà `get_app_snapshot`; les autres routes re
## Asset font du splash
La police bot3 de référence est `DOS_Amazigh.ttf`. Vérifier l'asset local contre l'empreinte documentée dans `frontend/fonts/README.md` ; `frontend/sass/splash.scss` l'applique au titre du splash.
## Panneau Documents
La vue **Documents** liste les documents de kind `Config` depuis le registre `ksp-config-lib`; les schemas ne sont pas codés en dur dans le frontend. DataTables fournit tri, recherche, pagination et sélection simple.
Pour chaque document, le backend expose :
- `file_id` ;
- filename mappé ;
- schema associé ;
- path résolu par Config ;
- état valide/invalide ;
- étape de diagnostic `read`, `json`, `schema`, `semantic`, `effective` ou `other`.
La sélection charge le source brut via `ConfigManagement::read_source()`, y compris lorsque le document est invalide mais lisible. Le bouton **Valider et sauvegarder** envoie le texte à `ConfigManagement::save_source_candidate()` : aucune écriture frontend directe du fichier nest effectuée. Un candidat invalide est refusé avant persistence et le diagnostic backend reste lautorité de validation.
Les clics, sélections, chargements, remplacements DOM et sauvegardes sont tracés via le bridge frontend KSP sans journaliser le contenu du source.

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
<!-- version: 3 -->
<!-- version: 4 -->
<!DOCTYPE html>
<html lang="fr">
@@ -55,6 +55,67 @@
</dl>
</section>
<section id="documentsPanel" hidden aria-label="Documents Config">
<div class="d-flex flex-wrap gap-2 align-items-center justify-content-between mb-3">
<div>
<h2 class="h5 mb-1">Documents enregistrés</h2>
<p class="text-body-secondary small mb-0">Le registre, la validation et la persistence restent exclusivement sous contrôle de ksp-config-lib.</p>
</div>
<button id="refreshDocuments" 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 class="table-responsive mb-4">
<table id="configDocumentsTable" class="table table-striped table-hover align-middle w-100">
<thead>
<tr>
<th scope="col">file_id</th>
<th scope="col">Fichier</th>
<th scope="col">Schema</th>
<th scope="col">État</th>
<th scope="col">Étape</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<section id="documentDetailPanel" hidden class="border rounded p-3 mb-4 bg-body-tertiary" aria-label="Détail du document Config">
<div class="d-flex flex-wrap gap-2 justify-content-between align-items-start mb-3">
<div>
<h2 class="h5 mb-1">Détail du document</h2>
<span id="documentDetailStatus" class="badge text-bg-secondary"></span>
</div>
<div class="d-flex gap-2">
<button id="reloadDocumentSource" class="btn btn-outline-secondary btn-sm" type="button">Recharger</button>
<button id="saveDocumentSource" class="btn btn-primary btn-sm" type="button">Valider et sauvegarder</button>
</div>
</div>
<dl class="row small mb-3 app-document-metadata">
<dt class="col-lg-3">file_id</dt>
<dd id="documentDetailFileId" class="col-lg-9"></dd>
<dt class="col-lg-3">Fichier mappé</dt>
<dd id="documentDetailFilename" class="col-lg-9"></dd>
<dt class="col-lg-3">Schema</dt>
<dd id="documentDetailSchema" class="col-lg-9"></dd>
<dt class="col-lg-3">Path résolu</dt>
<dd id="documentDetailPath" class="col-lg-9"></dd>
<dt class="col-lg-3">Étape diagnostic</dt>
<dd id="documentDetailStage" class="col-lg-9"></dd>
<dt class="col-lg-3 mb-0">Diagnostic</dt>
<dd id="documentDetailDiagnostic" class="col-lg-9 mb-0"></dd>
</dl>
<label class="form-label fw-semibold" for="documentSourceEditor">Source brut Config</label>
<textarea id="documentSourceEditor" class="form-control app-document-source" rows="18" spellcheck="false" autocomplete="off"></textarea>
<div class="form-text">La sauvegarde est refusée tant que le candidat échoue au parsing JSON, au schema ou aux invariants sémantiques backend.</div>
</section>
<div id="documentsStatus" class="alert alert-primary mb-0" role="status" aria-live="polite">Chargement de l'inventaire Config...</div>
</section>
<section id="placeholderPanel" hidden aria-live="polite">
<div class="alert alert-secondary mb-4" role="status">
Cette route du shell est prête ; son contenu fonctionnel sera ajouté dans la prerelease prévue par le plan 0.1.4.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/sass/_app.scss
// version: 2
// version: 3
$app-header-height: 72px;
$app-footer-height: 42px;
@@ -52,7 +52,7 @@ body {
}
.app-shell-card {
max-width: 980px;
max-width: 1320px;
}
.app-shell-status {
@@ -72,3 +72,22 @@ body {
.app-overview-list dd {
font-family: var(--bs-font-monospace);
}
.app-document-metadata dd,
.app-document-source,
#configDocumentsTable td:first-child,
#configDocumentsTable td:nth-child(2),
#configDocumentsTable td:nth-child(3) {
font-family: var(--bs-font-monospace);
}
.app-document-source {
min-height: 320px;
resize: vertical;
white-space: pre;
tab-size: 4;
}
#configDocumentsTable tbody tr {
cursor: pointer;
}

View File

@@ -0,0 +1,279 @@
// file: crates/ksp-app-config-desk/frontend/ts/documents.ts
// version: 1
//! Documents panel backed exclusively by ksp-config-lib inventory, validation and management APIs.
import DataTable from "datatables.net-bs5";
import "datatables.net-select-bs5";
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
import "datatables.net-select-bs5/css/select.bootstrap5.css";
import type { ConfigDocumentDetailDto } from "./bindings/ksp_app_config_desk/documents/ConfigDocumentDetailDto";
import type { ConfigDocumentErrorDto } from "./bindings/ksp_app_config_desk/documents/ConfigDocumentErrorDto";
import type { ConfigDocumentSaveResultDto } from "./bindings/ksp_app_config_desk/documents/ConfigDocumentSaveResultDto";
import type { ConfigDocumentSummaryDto } from "./bindings/ksp_app_config_desk/documents/ConfigDocumentSummaryDto";
import { frontendDebug, frontendTrace, frontendWarn } from "./frontend_log";
import { invokeKsp } from "./invoke";
interface DocumentsDataTable {
destroy(): unknown;
on(event: string, callback: (event: unknown, api: unknown, type: string, indexes: number[]) => void): unknown;
row(selector: number): { node(): Node | null };
}
let documentsTable: DocumentsDataTable | null = null;
let activeDocument: ConfigDocumentDetailDto | null = null;
const stageLabels: Record<string, string> = {
valid: "Valide",
read: "Lecture",
json: "JSON",
schema: "Schema",
semantic: "Sémantique",
effective: "Effective",
other: "Autre",
};
function stageLabel(stage: string): string {
return stageLabels[stage] ?? stage;
}
function setText(selector: string, value: string): void {
const element = document.querySelector<HTMLElement>(selector);
if (element) {
element.textContent = value;
}
}
function setDocumentsStatus(message: string, tone: "primary" | "success" | "warning" | "danger" = "primary"): void {
const status = document.querySelector<HTMLElement>("#documentsStatus");
if (!status) {
return;
}
status.className = `alert alert-${tone} mb-0`;
status.textContent = message;
frontendTrace("main", "Documents panel status replaced", { tone });
}
function diagnosticMessage(summary: ConfigDocumentSummaryDto): string {
if (!summary.diagnostic) {
return "Aucun diagnostic backend.";
}
return `${summary.diagnostic.domain}.${summary.diagnostic.code}${summary.diagnostic.message}`;
}
function renderDetail(detail: ConfigDocumentDetailDto): void {
activeDocument = detail;
setText("#documentDetailFileId", detail.summary.fileId);
setText("#documentDetailFilename", detail.summary.filename);
setText("#documentDetailSchema", detail.summary.schemaFileId ?? "—");
setText("#documentDetailPath", detail.summary.path);
setText("#documentDetailStage", stageLabel(detail.summary.diagnosticStage));
setText("#documentDetailDiagnostic", diagnosticMessage(detail.summary));
const badge = document.querySelector<HTMLElement>("#documentDetailStatus");
if (badge) {
const valid = detail.summary.validationStatus === "valid";
badge.className = `badge ${valid ? "text-bg-success" : "text-bg-danger"}`;
badge.textContent = valid ? "Valide" : "Invalide";
}
const editor = document.querySelector<HTMLTextAreaElement>("#documentSourceEditor");
if (editor) {
editor.value = detail.source ?? "";
editor.disabled = detail.source === null;
}
const saveButton = document.querySelector<HTMLButtonElement>("#saveDocumentSource");
if (saveButton) {
saveButton.disabled = detail.source === null;
}
const detailPanel = document.querySelector<HTMLElement>("#documentDetailPanel");
if (detailPanel) {
detailPanel.hidden = false;
}
frontendTrace("main", "Config document detail rendered", {
fileId: detail.summary.fileId,
validationStatus: detail.summary.validationStatus,
diagnosticStage: detail.summary.diagnosticStage,
sourceAvailable: detail.source !== null,
});
}
function clearDetail(): void {
activeDocument = null;
const detailPanel = document.querySelector<HTMLElement>("#documentDetailPanel");
if (detailPanel) {
detailPanel.hidden = true;
}
frontendTrace("main", "Config document detail cleared");
}
async function loadDetail(fileId: string, source: "selection" | "reload"): Promise<void> {
frontendDebug("main", "Config document detail requested", { fileId, source });
try {
const detail = await invokeKsp<ConfigDocumentDetailDto>("main", "get_config_document_detail", { fileId });
renderDetail(detail);
setDocumentsStatus(`Document ${fileId} chargé depuis Config.`, "success");
} catch (caughtError) {
const error = asDocumentError(caughtError);
const message = error
? `${stageLabel(error.diagnosticStage)} : ${error.error.domain}.${error.error.code}${error.error.message}`
: "Le détail du document n'a pas pu être chargé.";
setDocumentsStatus(message, "danger");
}
}
function populateTableRows(documents: ConfigDocumentSummaryDto[]): void {
const body = document.querySelector<HTMLTableSectionElement>("#configDocumentsTable tbody");
if (!body) {
return;
}
body.replaceChildren();
for (const summary of documents) {
const row = document.createElement("tr");
row.dataset.fileId = summary.fileId;
appendCell(row, summary.fileId);
appendCell(row, summary.filename);
appendCell(row, summary.schemaFileId ?? "—");
appendCell(row, summary.validationStatus === "valid" ? "Valide" : "Invalide");
appendCell(row, stageLabel(summary.diagnosticStage));
body.append(row);
}
frontendTrace("main", "Config document inventory table DOM replaced", { documentCount: documents.length });
}
function appendCell(row: HTMLTableRowElement, text: string): void {
const cell = document.createElement("td");
cell.textContent = text;
row.append(cell);
}
function initializeDataTable(): void {
if (documentsTable) {
documentsTable.destroy();
documentsTable = null;
}
const table = new DataTable("#configDocumentsTable", {
order: [[0, "asc"]],
pageLength: 10,
select: { style: "single" },
language: {
emptyTable: "Aucun document Config enregistré.",
info: "_START_ à _END_ sur _TOTAL_ document(s)",
infoEmpty: "0 document",
lengthMenu: "Afficher _MENU_",
search: "Filtrer :",
zeroRecords: "Aucun document correspondant.",
},
}) as unknown as DocumentsDataTable;
table.on("select", (event, api, type, indexes) => {
void event;
void api;
if (type !== "row" || indexes.length === 0) {
return;
}
const node = table.row(indexes[0]).node();
if (!(node instanceof HTMLTableRowElement)) {
return;
}
const fileId = node.dataset.fileId;
frontendTrace("main", "Config document table row selected", { fileId: fileId ?? null });
if (fileId) {
void loadDetail(fileId, "selection");
}
});
documentsTable = table;
frontendTrace("main", "DataTables Documents initialized", { selection: "single" });
}
async function refreshInventory(): Promise<void> {
frontendDebug("main", "Config document inventory refresh requested");
clearDetail();
try {
const documents = await invokeKsp<ConfigDocumentSummaryDto[]>("main", "get_config_documents");
populateTableRows(documents);
initializeDataTable();
setDocumentsStatus(`${documents.length} document(s) Config évalué(s) par le backend.`, "success");
} catch {
setDocumentsStatus("L'inventaire Config n'a pas pu être chargé.", "danger");
}
}
async function reloadActiveDocument(): Promise<void> {
if (!activeDocument) {
return;
}
frontendDebug("main", "Config document reload clicked", { fileId: activeDocument.summary.fileId });
await loadDetail(activeDocument.summary.fileId, "reload");
}
async function saveActiveDocument(): Promise<void> {
if (!activeDocument) {
return;
}
const editor = document.querySelector<HTMLTextAreaElement>("#documentSourceEditor");
if (!editor) {
return;
}
const fileId = activeDocument.summary.fileId;
frontendDebug("main", "Config document validated save clicked", { fileId });
setDocumentsStatus(`Validation et sauvegarde de ${fileId} en cours...`, "primary");
try {
const result = await invokeKsp<ConfigDocumentSaveResultDto>("main", "save_config_document_source", {
fileId,
source: editor.value,
});
renderDetail(result.document);
const outcome = result.sourceChanged ? "source modifiée" : "source inchangée";
setDocumentsStatus(`Sauvegarde validée par Config : ${outcome}; reload_required=${result.reloadRequired}.`, "success");
frontendDebug("main", "Config document validated save completed", {
fileId,
sourceChanged: result.sourceChanged,
reloadRequired: result.reloadRequired,
});
} catch (caughtError) {
const error = asDocumentError(caughtError);
if (error) {
setDocumentsStatus(
`${stageLabel(error.diagnosticStage)} : ${error.error.domain}.${error.error.code}${error.error.message}`,
"danger",
);
frontendWarn("main", "Config document candidate rejected by backend", { fileId, diagnosticStage: error.diagnosticStage });
return;
}
setDocumentsStatus("Le candidat Config a été rejeté sans diagnostic exploitable.", "danger");
}
}
function asDocumentError(value: unknown): ConfigDocumentErrorDto | null {
if (typeof value !== "object" || value === null) {
return null;
}
const candidate = value as Record<string, unknown>;
if (typeof candidate.diagnosticStage !== "string" || typeof candidate.error !== "object" || candidate.error === null) {
return null;
}
const error = candidate.error as Record<string, unknown>;
if (typeof error.domain !== "string" || typeof error.code !== "string" || typeof error.message !== "string") {
return null;
}
return value as ConfigDocumentErrorDto;
}
function bindDocumentActions(): void {
document.querySelector<HTMLButtonElement>("#refreshDocuments")?.addEventListener("click", () => {
frontendTrace("main", "Documents refresh button clicked");
void refreshInventory();
});
document.querySelector<HTMLButtonElement>("#reloadDocumentSource")?.addEventListener("click", () => {
frontendTrace("main", "Document reload button clicked");
void reloadActiveDocument();
});
document.querySelector<HTMLButtonElement>("#saveDocumentSource")?.addEventListener("click", () => {
frontendTrace("main", "Document save button clicked");
void saveActiveDocument();
});
frontendTrace("main", "Documents panel handlers installed");
}
export function initializeDocumentsPanel(): void {
bindDocumentActions();
void refreshInventory();
}

View File

@@ -7,6 +7,7 @@ import "simplebar";
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { AppSnapshotDto } from "./bindings/ksp_app_config_desk/dto_common/AppSnapshotDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
import { initializeDocumentsPanel } from "./documents";
import { invokeKsp } from "./invoke";
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
@@ -21,7 +22,7 @@ const viewCopy: Record<ViewId, { title: string; description: string }> = {
},
documents: {
title: "Documents",
description: "Inventaire, diagnostics et réparation des documents Config — surface fonctionnelle prévue en pre.009.",
description: "Inventaire, diagnostics et réparation validée des documents Config enregistrés.",
},
profiles: {
title: "Profils",
@@ -69,9 +70,13 @@ function activateView(viewId: ViewId, source: "startup" | "user"): void {
if (overview) {
overview.hidden = viewId !== "overview";
}
const documents = document.querySelector<HTMLElement>("#documentsPanel");
if (documents) {
documents.hidden = viewId !== "documents";
}
const placeholder = document.querySelector<HTMLElement>("#placeholderPanel");
if (placeholder) {
placeholder.hidden = viewId === "overview";
placeholder.hidden = viewId === "overview" || viewId === "documents";
}
frontendTrace("main", "Main view DOM updated", { viewId, source });
}
@@ -130,6 +135,7 @@ async function initializeMain(): Promise<void> {
const windowLabel = getCurrentWindow().label;
frontendInfo("main", "Config Desk main frontend loaded", { windowLabel });
bindNavigation();
initializeDocumentsPanel();
activateView("overview", "startup");
try {
await loadSnapshot();

View File

@@ -14,6 +14,8 @@
"@fortawesome/fontawesome-free": "^7.3",
"@tauri-apps/api": "^2.11",
"bootstrap": "^5.3",
"datatables.net-bs5": "^3.0",
"datatables.net-select-bs5": "^4.0",
"resize-observer-polyfill": "^1.5",
"simplebar": "^6.3"
},

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/app_state.rs
// version: 3
// version: 4
//! Shared backend state owned by the Tauri application.
@@ -104,6 +104,12 @@ impl AppState {
});
}
/// Returns the Config management facade owned by the application state.
#[must_use]
pub(crate) const fn config_management(&self) -> &ksp_config_lib::ConfigManagement {
return &self.config_management;
}
/// Returns the resolved splash timings captured during application bootstrap.
#[must_use]
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {

View File

@@ -17,3 +17,5 @@ pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "config.bootstrap";
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Structured domain for Config document inventory, diagnostics and repair.
pub(crate) const TRACING_DOMAIN_DOCUMENTS: &str = "config.documents";

View File

@@ -0,0 +1,275 @@
// file: crates/ksp-app-config-desk/src/documents.rs
// version: 1
//! Generic Config document inventory, diagnostics and validated repair services.
use ts_rs::TS; // rust-rules: derive-import
const STATUS_VALID: &str = "valid";
const STATUS_INVALID: &str = "invalid";
const STAGE_VALID: &str = "valid";
const STAGE_READ: &str = "read";
const STAGE_JSON: &str = "json";
const STAGE_SCHEMA: &str = "schema";
const STAGE_SEMANTIC: &str = "semantic";
const STAGE_EFFECTIVE: &str = "effective";
const STAGE_OTHER: &str = "other";
/// Safe inventory row for one Config document registered by `ksp-config-lib`.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/documents/ConfigDocumentSummaryDto.ts")]
pub(crate) struct ConfigDocumentSummaryDto {
/// Stable logical Config identifier.
pub(crate) file_id: String,
/// Current Config-managed relative filename mapping.
pub(crate) filename: String,
/// Registered validation schema identifier.
pub(crate) schema_file_id: std::option::Option<String>,
/// Resolved filesystem path controlled by Config.
pub(crate) path: String,
/// `valid` or `invalid` according to the backend Config authority.
pub(crate) validation_status: String,
/// Backend diagnostic stage: valid/read/json/schema/semantic/effective/other.
pub(crate) diagnostic_stage: String,
/// Safe bounded diagnostic when the document is invalid.
pub(crate) diagnostic: std::option::Option<crate::CommandErrorDto>,
}
/// Detailed management view for one registered Config document.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/documents/ConfigDocumentDetailDto.ts")]
pub(crate) struct ConfigDocumentDetailDto {
/// Current validation/inventory summary.
pub(crate) summary: ConfigDocumentSummaryDto,
/// Raw Config-managed source when the file can be read, even if its JSON/schema/semantics are invalid.
pub(crate) source: std::option::Option<String>,
}
/// Result of one validated raw-source repair attempt that reached persistence successfully.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/documents/ConfigDocumentSaveResultDto.ts")]
pub(crate) struct ConfigDocumentSaveResultDto {
/// Whether persisted source bytes changed.
pub(crate) source_changed: bool,
/// Whether Config consumers need a reload to observe the changed document.
pub(crate) reload_required: bool,
/// Fresh detail reloaded after persistence.
pub(crate) document: ConfigDocumentDetailDto,
}
/// Safe document-specific command error with backend-owned diagnostic classification.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/documents/ConfigDocumentErrorDto.ts")]
pub(crate) struct ConfigDocumentErrorDto {
/// Backend diagnostic stage associated with the failure.
pub(crate) diagnostic_stage: String,
/// Bounded KSP error projection without arbitrary context/source values.
pub(crate) error: crate::CommandErrorDto,
}
impl ConfigDocumentErrorDto {
fn from_error(error: &ksp_core_lib::Error) -> Self {
return Self { diagnostic_stage: classify_error(error).to_owned(), error: crate::CommandErrorDto::from_error(error) };
}
}
/// Lists all Config-kind documents from the Config registry and evaluates their current backend validation state.
pub(crate) fn inventory(state: &crate::AppState) -> std::vec::Vec<ConfigDocumentSummaryDto> {
let management = state.config_management();
let engine = management.engine();
let mut documents = std::vec::Vec::new();
for descriptor in engine.registry().descriptors() {
if descriptor.kind() != ksp_config_lib::ConfigFileKind::Config {
continue;
}
documents.push(build_summary(management, descriptor));
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_DOCUMENTS,
document_count = documents.len(),
"Config document inventory evaluated"
);
return documents;
}
/// Loads one Config document detail, including raw source when Config can read it.
pub(crate) fn detail(state: &crate::AppState, file_id_text: &str) -> std::result::Result<ConfigDocumentDetailDto, ConfigDocumentErrorDto> {
let file_id = parse_config_file_id(file_id_text);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)),
};
let management = state.config_management();
let descriptor = management.engine().registry().descriptor(&file_id);
let descriptor = match descriptor {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)),
};
if descriptor.kind() != ksp_config_lib::ConfigFileKind::Config {
let error = ksp_core_lib::Error::new(crate::ERROR_CODE_DOCUMENT_KIND_INVALID, "requested document is not a Config-kind document");
return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error));
}
let summary = build_summary(management, descriptor);
let source = management.read_source(&file_id);
let source = match source {
std::result::Result::Ok(value) => std::option::Option::Some(value.content().to_owned()),
std::result::Result::Err(_) => std::option::Option::None,
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_DOCUMENTS,
file_id = file_id.as_str(),
validation_status = summary.validation_status.as_str(),
diagnostic_stage = summary.diagnostic_stage.as_str(),
source_available = source.is_some(),
"Config document detail loaded"
);
return std::result::Result::Ok(ConfigDocumentDetailDto { summary, source });
}
/// Validates and atomically persists one raw Config source candidate, then reloads the document detail.
pub(crate) fn save_source(
state: &crate::AppState,
file_id_text: &str,
source: &str,
) -> std::result::Result<ConfigDocumentSaveResultDto, ConfigDocumentErrorDto> {
let file_id = parse_config_file_id(file_id_text);
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)),
};
let report = state.config_management().save_source_candidate(&file_id, source);
let report = match report {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(ConfigDocumentErrorDto::from_error(&error)),
};
let document = detail(state, file_id.as_str());
let document = match document {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
ksp_logging_lib::info!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_DOCUMENTS,
file_id = file_id.as_str(),
source_changed = report.source_changed(),
reload_required = report.reload_required(),
"validated Config document source candidate persisted"
);
return std::result::Result::Ok(ConfigDocumentSaveResultDto {
source_changed: report.source_changed(),
reload_required: report.reload_required(),
document,
});
}
fn parse_config_file_id(value: &str) -> ksp_core_lib::Result<ksp_config_lib::ConfigFileId> {
let file_id = ksp_config_lib::ConfigFileId::new(value.to_owned());
let file_id = match file_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(file_id);
}
fn build_summary(management: &ksp_config_lib::ConfigManagement, descriptor: &ksp_config_lib::ConfigFileDescriptor) -> ConfigDocumentSummaryDto {
let engine = management.engine();
let path = engine.registry().resolve_path(engine.bootstrap(), descriptor.file_id());
let path = match path {
std::result::Result::Ok(value) => value.to_string_lossy().into_owned(),
std::result::Result::Err(_) => descriptor.filename().to_string_lossy().into_owned(),
};
let validation = validate_effective_document(management, descriptor.file_id());
let (validation_status, diagnostic_stage, diagnostic) = match validation {
std::result::Result::Ok(()) => (STATUS_VALID.to_owned(), STAGE_VALID.to_owned(), std::option::Option::None),
std::result::Result::Err(error) => {
(STATUS_INVALID.to_owned(), classify_error(&error).to_owned(), std::option::Option::Some(crate::CommandErrorDto::from_error(&error)))
},
};
return ConfigDocumentSummaryDto {
file_id: descriptor.file_id().as_str().to_owned(),
filename: descriptor.filename().to_string_lossy().into_owned(),
schema_file_id: descriptor.schema_file_id().map(|value| {
return value.as_str().to_owned();
}),
path,
validation_status,
diagnostic_stage,
diagnostic,
};
}
fn validate_effective_document(management: &ksp_config_lib::ConfigManagement, file_id: &ksp_config_lib::ConfigFileId) -> ksp_core_lib::Result<()> {
let validated = management.engine().load_validated_document(file_id);
match validated {
std::result::Result::Ok(_) => {},
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
if file_id.as_str() != ksp_config_lib::FILE_ID_STD_LOGGING {
return std::result::Result::Ok(());
}
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 = management.engine().load_resolved_logging_config(std::option::Option::None, &environment);
return match resolved {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
fn classify_error(error: &ksp_core_lib::Error) -> &'static str {
let code = error.code();
if code == ksp_config_lib::ERROR_CODE_JSON_FILE_READ_FAILED {
if error_context_file_id_is_schema(error) {
return STAGE_SCHEMA;
}
return STAGE_READ;
}
if code == ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID {
if error_context_file_id_is_schema(error) {
return STAGE_SCHEMA;
}
return STAGE_JSON;
}
if code == ksp_config_lib::ERROR_CODE_SCHEMA_INVALID || code == ksp_config_lib::ERROR_CODE_SCHEMA_VALIDATION_FAILED {
return STAGE_SCHEMA;
}
if code == ksp_config_lib::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID
|| code == ksp_config_lib::ERROR_CODE_PROFILE_NOT_FOUND
|| code == ksp_config_lib::ERROR_CODE_COMPOSITE_REFERENCE_INVALID
{
return STAGE_SEMANTIC;
}
if code == ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID
|| code == ksp_config_lib::ERROR_CODE_ENVIRONMENT_PLACEHOLDER_INVALID
|| code == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VALUE_INVALID
|| code == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_INVALID
|| code == ksp_config_lib::ERROR_CODE_ENVIRONMENT_VARIABLE_MISSING
|| code == ksp_config_lib::ERROR_CODE_DOTENV_FILE_READ_FAILED
|| code == ksp_config_lib::ERROR_CODE_DOTENV_SYNTAX_INVALID
{
return STAGE_EFFECTIVE;
}
return STAGE_OTHER;
}
fn error_context_file_id_is_schema(error: &ksp_core_lib::Error) -> bool {
for field in error.context() {
if field.key() == "file_id" {
return field.value().starts_with("schema.");
}
}
return false;
}
#[cfg(test)]
#[path = "../unit_tests/documents.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/errors.rs
// version: 4
// version: 5
//! Application-local error codes for the configuration desktop shell.
@@ -24,3 +24,6 @@ pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) const ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config_desk", "tauri_window_operation_failed");
/// A Documents command requested a registered file that is not a Config-kind document.
pub(crate) const ERROR_CODE_DOCUMENT_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config_desk", "document_kind_invalid");

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/lib.rs
// version: 5
// version: 6
//! Tauri desktop application for managing and validating KSP configuration.
@@ -10,6 +10,7 @@
mod app_state;
mod bootstrap;
mod constants;
mod documents;
mod dto_common;
mod errors;
mod frontend_logging;
@@ -25,16 +26,22 @@ pub(crate) use self::app_state::AppState;
pub(crate) use self::bootstrap::config_management;
pub(crate) use self::bootstrap::initialize_logging;
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS;
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
pub(crate) use self::constants::TRACING_TARGET;
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
pub(crate) use self::documents::ConfigDocumentDetailDto;
pub(crate) use self::documents::ConfigDocumentErrorDto;
pub(crate) use self::documents::ConfigDocumentSaveResultDto;
pub(crate) use self::documents::ConfigDocumentSummaryDto;
pub(crate) use self::dto_common::AppSnapshotDto;
pub(crate) use self::dto_common::CommandErrorDto;
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
pub(crate) use self::errors::ERROR_CODE_DOCUMENT_KIND_INVALID;
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/src/tauri.rs
// version: 6
// version: 7
//! Tauri runtime assembly for the KSP configuration desktop application.
@@ -37,7 +37,14 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
#[allow(clippy::question_mark_used)] // Tauri generates the question-mark operator internally for async command dispatch.
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![get_app_snapshot, emit_frontend_log, splash_frontend_ready]);
return builder.invoke_handler(tauri::generate_handler![
get_app_snapshot,
get_config_documents,
get_config_document_detail,
save_config_document_source,
emit_frontend_log,
splash_frontend_ready
]);
}
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
@@ -63,6 +70,28 @@ fn get_app_snapshot(state: tauri::State<'_, crate::AppState>) -> std::result::Re
};
}
#[tauri::command]
fn get_config_documents(state: tauri::State<'_, crate::AppState>) -> std::vec::Vec<crate::ConfigDocumentSummaryDto> {
return crate::documents::inventory(&state);
}
#[tauri::command]
fn get_config_document_detail(
file_id: String,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigDocumentDetailDto, crate::ConfigDocumentErrorDto> {
return crate::documents::detail(&state, file_id.as_str());
}
#[tauri::command]
fn save_config_document_source(
file_id: String,
source: String,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::ConfigDocumentSaveResultDto, crate::ConfigDocumentErrorDto> {
return crate::documents::save_source(&state, file_id.as_str(), source.as_str());
}
#[tauri::command]
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload);

View File

@@ -0,0 +1,32 @@
// file: crates/ksp-app-config-desk/unit_tests/documents.rs
// version: 1
use super::*;
#[test]
fn diagnostic_classifier_distinguishes_json_schema_semantic_and_effective_errors() {
let json = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "json");
assert_eq!(classify_error(&json), STAGE_JSON);
let schema = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_SCHEMA_VALIDATION_FAILED, "schema");
assert_eq!(classify_error(&schema), STAGE_SCHEMA);
let semantic = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "semantic");
assert_eq!(classify_error(&semantic), STAGE_SEMANTIC);
let effective = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective");
assert_eq!(classify_error(&effective), STAGE_EFFECTIVE);
}
#[test]
fn schema_source_read_errors_are_classified_as_schema_failures() {
let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_FILE_READ_FAILED, "missing schema").with_context("file_id", "schema.std.logging");
assert_eq!(classify_error(&error), STAGE_SCHEMA);
}
#[test]
fn document_error_projection_does_not_expose_arbitrary_error_context() {
let error = ksp_core_lib::Error::new(ksp_config_lib::ERROR_CODE_JSON_SYNTAX_INVALID, "invalid source").with_context("detail", "sensitive-canary");
let projection = ConfigDocumentErrorDto::from_error(&error);
assert_eq!(projection.diagnostic_stage, STAGE_JSON);
assert_eq!(projection.error.domain, "config");
assert_eq!(projection.error.code, "json_syntax_invalid");
assert_eq!(projection.error.message, "invalid source");
}

168
deltas/0.1.4/pre.009.md Normal file
View File

@@ -0,0 +1,168 @@
<!-- file: deltas/0.1.4/pre.009.md -->
<!-- version: 1 -->
# `0.1.4-pre.009` — Documents + diagnostics
## 1. Base validée
Cette tranche part de `0.1.4-pre.8.fix.3`, validée localement avec :
- `cargo fmt --all` ;
- `cargo check --workspace` ;
- `cargo clippy --workspace --all-targets` ;
- tests `ksp-app-config-desk` ;
- tests `ksp-config-lib` ;
- `cargo tree -p ksp-app-config-desk` ;
- lancement réel `cargo tauri dev -c crates/ksp-app-config-desk/tauri.conf.json`.
Le runtime Logging utilise désormais le profil `local_dev`, résout `KSP_DESK_SPLASH_MINIMUM_MS=12000` et `KSP_DESK_SPLASH_FADE_MS=3000` depuis `.env`, puis mesure environ `15003 ms` de lifecycle backend avant destruction du splash. Les activations de vues sont visibles sous `ksp-app-config-desk.frontend.main`.
## 2. Version technique
```text
workspace.package.version = "0.1.4-pre.9"
```
## 3. Surface Documents backend
La nouvelle unité `src/documents.rs` reste une couche applicative au-dessus de `ksp-config-lib` et n'effectue aucun accès filesystem direct.
Elle fournit :
- inventaire de tous les descripteurs de kind `Config` issus de `ConfigFileRegistry::descriptors()` ;
- path résolu via le registre/bootstrap Config ;
- validation backend via `ConfigDocumentEngine::load_validated_document()` ;
- contrôle effective supplémentaire pour `cfg.std.logging` via `ConfigEnvironment` + `load_resolved_logging_config()` ;
- source brut via `ConfigManagement::read_source()` ;
- persistence/réparation via `ConfigManagement::save_source_candidate()` ;
- reload du détail après persistence.
Le frontend ne devient autorité ni du parsing JSON, ni du schema, ni des invariants sémantiques/effectifs.
## 4. Classification des diagnostics
Les diagnostics sont classifiés côté Rust avant projection vers le frontend :
| Étape | Signification |
|-------------|------------------------------------------------------|
| `read` | document Config principal non lisible |
| `json` | syntaxe JSON du document principal invalide |
| `schema` | schema manquant/invalide ou instance non conforme |
| `semantic` | invariant KSP sémantique invalide |
| `effective` | environnement/placeholders/config effective invalide |
| `other` | erreur non classée dans les catégories précédentes |
| `valid` | validation complète réussie |
Les DTO d'erreur n'exposent jamais les contextes arbitraires ni les sources d'erreur KSP.
## 5. DTO/commandes Tauri
DTO TS-RS introduits :
```text
ConfigDocumentSummaryDto
ConfigDocumentDetailDto
ConfigDocumentSaveResultDto
ConfigDocumentErrorDto
```
Commandes Tauri centralisées dans `tauri.rs` :
```text
get_config_documents
get_config_document_detail
save_config_document_source
```
Le contenu source n'est jamais inclus dans les logs frontend/backend de ces opérations.
## 6. DataTables 3 + Select 4
Le tableau Documents exige tri, filtre/recherche, pagination et sélection simple. Conformément au gabarit KSP, ces fonctions sont fournies par DataTables plutôt que réimplémentées.
Dépendances frontend ajoutées sous `dependencies` :
```text
datatables.net-bs5 = ^3.0
datatables.net-select-bs5 = ^4.0
```
DataTables 3 est la génération stable publiée en juillet 2026 ; Select 4 est la génération correspondante.
Le module `frontend/ts/documents.ts` gère :
- inventaire ;
- DataTable Bootstrap 5 ;
- sélection simple ;
- chargement du détail ;
- édition du source brut ;
- validation/sauvegarde ;
- diagnostics et état de persistence ;
- instrumentation `debug`/`trace` des clics, sélections, chargements et remplacements DOM.
## 7. Règles Logging ajoutées
Deux exigences durables sont formalisées :
1. les fichiers de log applicatifs persistants doivent être uniques par lancement et intégrer l'identité de l'application ainsi qu'un timestamp de démarrage ;
2. le niveau applicatif temporairement élevé à `debug`/`trace` pendant développement/correction doit revenir à `info` ou `warn` avant release.
L'implémentation du nom de fichier unique par lancement est planifiée dans la tranche runtime Logging `pre.016`; le retour du target `ksp-app-config-desk` à `info`/`warn` est une condition de clôture `pre.019`.
## 8. Installation frontend
Après application du delta, depuis la racine :
```bash
cd crates/ksp-app-config-desk
npm install
cd ../../
```
Cette commande est uniquement une opération de gestion des dépendances. Le développement/build normal reste piloté par Tauri depuis la racine du workspace.
## 9. Validation 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
```
Test manuel minimum :
1. ouvrir **Documents** ;
2. vérifier recherche/tri/sélection DataTables ;
3. sélectionner `cfg.std.logging` ;
4. vérifier `file_id`, filename, schema, path, état et source ;
5. introduire volontairement une erreur JSON puis cliquer **Valider et sauvegarder** : le candidat doit être refusé et le fichier existant rester intact ;
6. corriger le source puis sauvegarder : le backend doit accepter et recharger le détail ;
7. vérifier que les logs décrivent les opérations sans contenir le source édité.
## 10. Fichiers modifiés/ajoutés
| Fichier | Action |
|-------------------------------------------------------|--------:|
| `Cargo.toml` | modifié |
| `docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md` | modifié |
| `docs/rules/RULES_KSP.md` | modifié |
| `crates/ksp-app-config-desk/package.json` | modifié |
| `crates/ksp-app-config-desk/README.md` | modifié |
| `crates/ksp-app-config-desk/TODO.md` | modifié |
| `crates/ksp-app-config-desk/USAGE.md` | modifié |
| `crates/ksp-app-config-desk/src/app_state.rs` | modifié |
| `crates/ksp-app-config-desk/src/constants.rs` | modifié |
| `crates/ksp-app-config-desk/src/documents.rs` | ajouté |
| `crates/ksp-app-config-desk/src/errors.rs` | modifié |
| `crates/ksp-app-config-desk/src/lib.rs` | modifié |
| `crates/ksp-app-config-desk/src/tauri.rs` | modifié |
| `crates/ksp-app-config-desk/unit_tests/documents.rs` | ajouté |
| `crates/ksp-app-config-desk/frontend/main.html` | modifié |
| `crates/ksp-app-config-desk/frontend/sass/_app.scss` | modifié |
| `crates/ksp-app-config-desk/frontend/ts/main.ts` | modifié |
| `crates/ksp-app-config-desk/frontend/ts/documents.ts` | ajouté |
| `deltas/0.1.4/pre.009.md` | ajouté |

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md -->
<!-- version: 9 -->
<!-- version: 10 -->
# Plan `0.1.4` — `ksp-app-config-desk`
@@ -910,6 +910,8 @@ La release ne peut pas être clôturée sans preuve des cas suivants :
| L12 | appliquer config invalide | erreur visible, runtime précédent reste fonctionnel et le bouton Log le prouve |
| L13 | redémarrer l'app | profil/default persisté retrouvé selon le document sauvegardé |
| L14 | modifier default profile | nouveau `default_profile` validé/persisté par Config |
| L15 | redémarrer plusieurs fois | chaque lancement écrit dans un fichier applicatif distinct horodaté |
| L16 | clôturer la release | target applicatif ramené à `info` ou `warn` dans le profil de référence |
La démonstration de clôture conservera deux profils de test reproductibles, sans dépendre de données secrètes.
@@ -1196,12 +1198,12 @@ pre.008 shell main + splash de référence
- helpers show/focus/destroy
- instrumentation debug/trace des interactions, tabs et timings splash
pre.009 Documents + diagnostics
pre.009 Documents + diagnostics [en cours]
- inventaire générique par registre
- catégories JSON/schema/sémantique/effective
- source brut invalide
- réparation via Config
- introduire DataTables ici si le tableau Documents requiert déjà tri/filtre/sélection
- DataTables 3 + Select 4 pour tri/filtre/pagination/sélection
pre.010 Profils + provenance
- default_profile
@@ -1242,6 +1244,7 @@ pre.015 Logging editor — mutations/persistence
- save_logging_document
pre.016 Logging runtime
- fichiers applicatifs uniques par lancement avec timestamp de démarrage
- sélection profil à appliquer
- ConfigEnvironment frais
- load_resolved_logging_config

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/RULES_KSP.md -->
<!-- version: 26 -->
<!-- version: 27 -->
# Règles spécifiques à KSP
@@ -222,6 +222,8 @@
- **KSP-APP-027** — Les applications Tauri KSP instrumentent systématiquement le comportement frontend via le bridge Logging commun : actions utilisateur et transitions détat significatives en `debug`, événements techniques fins, rendu/remplacement de sections et étapes fréquentes en `trace`. Les chargements/rafraîchissements de données sont tracés au début et à la fin sans journaliser les payloads sensibles. Cette instrumentation doit permettre de reconstruire le déroulement frontend sans dépendre uniquement de létat visuel.
- **KSP-APP-028** — Le header dune application desk ne répète pas inutilement lidentité déjà portée par son logo : il affiche le nom ou labréviation fonctionnelle de lapplication, puis un tiret cadratin `—` et le titre de la vue active. Les commandes principales peu nombreuses peuvent utiliser des tabs/pills alignées à droite ; lorsque leur nombre nuit à la lisibilité ou à lespace disponible, un dropdown est préféré.
- **KSP-APP-029** — En développement workspace, une application desk Tauri normalise le current working directory du processus Rust vers la racine du workspace avant le bootstrap Config lorsque `cargo tauri ... -c crates/<app>/tauri.conf.json` lance le binaire depuis la crate. Cette adaptation ne lit ni ne parse directement `config/`, `.env` ou les variables `KSP_*`/`KSPB_*` : `ksp-config-lib` reste seul propriétaire de ces ressources. Le comportement de distribution/release reste défini séparément et ne doit pas dépendre dun checkout source.
- **KSP-APP-030** — Lorsquune application KSP persiste des logs applicatifs, chaque lancement doit disposer dun fichier propre et non partagé avec un lancement précédent. Le nom encode au minimum lidentité applicative et un horodatage de démarrage, par exemple `app-name.YYYYMMDD-HHMMSS.log` / `.json` / `.jsonl` selon le format du sink ; une rotation quotidienne ne doit pas fusionner plusieurs exécutions applicatives dans le même fichier.
- **KSP-APP-031** — Le niveau de logging spécifique à une application/crate peut être élevé temporairement à `debug` ou `trace` pendant une phase de développement ou correction. Avant la clôture/release de cette application/crate, son niveau de référence est ramené à `info` ou `warn` selon le besoin opératoire ; il nest remonté que lorsquun développement/correctif est explicitement rouvert.
## Data plane / control plane