v0.1.4-pre.008

This commit is contained in:
2026-08-16 12:32:11 +02:00
parent ac8867d5a1
commit 1825676ff9
24 changed files with 1102 additions and 61 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
<!-- version: 1 -->
<!-- version: 2 -->
<!DOCTYPE html>
<html lang="fr">
@@ -13,12 +13,19 @@
<body>
<header class="app-header">
<nav class="navbar navbar-expand-lg h-100 py-0 bg-light text-dark">
<div class="container-fluid px-4">
<div class="navbar-brand d-flex align-items-center">
<nav class="navbar h-100 py-0 bg-light text-dark">
<div class="container-fluid px-4 flex-nowrap">
<div class="navbar-brand d-flex align-items-center me-4">
<img alt="KSP" src="imgs/logo.png" class="app-logo">
<span class="ps-2 fs-4 fw-bold text-primary">KSP — <span class="badge text-bg-primary">Config Desk</span></span>
</div>
<div class="app-nav nav nav-pills flex-nowrap gap-1" aria-label="Navigation Config Desk">
<button class="nav-link app-nav-button active" type="button" data-view="overview">Vue d'ensemble</button>
<button class="nav-link app-nav-button" type="button" data-view="documents">Documents</button>
<button class="nav-link app-nav-button" type="button" data-view="profiles">Profils</button>
<button class="nav-link app-nav-button" type="button" data-view="environment">Environnement / .env</button>
<button class="nav-link app-nav-button" type="button" data-view="logging">Logging</button>
</div>
</div>
</nav>
</header>
@@ -28,10 +35,30 @@
<div class="container-fluid px-4 py-4">
<div class="card app-shell-card mx-auto shadow-sm border-0">
<div class="card-body p-4">
<h1 class="h3 mb-3">Configuration KSP</h1>
<p class="text-body-secondary mb-4">
Le shell desktop est prêt. Les panneaux Documents, Profils, Environnement et Logging seront ajoutés dans les prereleases suivantes.
</p>
<h1 id="viewTitle" class="h3 mb-2">Vue d'ensemble</h1>
<p id="viewDescription" class="text-body-secondary mb-4">État sûr du bootstrap Config Desk et du runtime Logging actuellement actif.</p>
<section id="overviewPanel" aria-label="Vue d'ensemble">
<dl class="row mb-4 app-overview-list">
<dt class="col-sm-5">Version application</dt>
<dd id="overviewVersion" class="col-sm-7 text-sm-end"></dd>
<dt class="col-sm-5">Descripteurs Config enregistrés</dt>
<dd id="overviewDocuments" class="col-sm-7 text-sm-end"></dd>
<dt class="col-sm-5">Profil Logging actif</dt>
<dd id="overviewLoggingProfile" class="col-sm-7 text-sm-end"></dd>
<dt class="col-sm-5">Génération Logging</dt>
<dd id="overviewLoggingGeneration" class="col-sm-7 text-sm-end"></dd>
<dt class="col-sm-5 mb-0">Fallback Logging actif</dt>
<dd id="overviewLoggingFallback" class="col-sm-7 text-sm-end mb-0"></dd>
</dl>
</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.
</div>
</section>
<div class="alert alert-primary mb-0" role="status">
<i class="fa-solid fa-desktop me-2" aria-hidden="true"></i>
<span id="shellStatus" class="app-shell-status">Initialisation du frontend...</span>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/sass/_app.scss
// version: 1
// version: 2
$app-header-height: 72px;
$app-footer-height: 42px;
@@ -58,3 +58,17 @@ body {
.app-shell-status {
font-family: var(--bs-font-monospace);
}
.app-nav {
min-width: 0;
overflow-x: auto;
scrollbar-width: thin;
}
.app-nav-button {
white-space: nowrap;
}
.app-overview-list dd {
font-family: var(--bs-font-monospace);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-config-desk/frontend/sass/splash.scss
// version: 2
// version: 3
@font-face {
font-family: "Dos Amazigh";
@@ -26,7 +26,7 @@ body {
position: relative;
width: 960px;
height: 637px;
opacity: 1;
opacity: 0;
}
#splash-image {
@@ -62,4 +62,4 @@ body {
background: rgba(0, 0, 0, 0.35);
font-family: monospace;
font-size: 0.82rem;
}
}

View File

@@ -0,0 +1,19 @@
// file: crates/ksp-app-config-desk/frontend/ts/invoke.ts
// version: 1
//! Shared Tauri invoke wrapper with bounded frontend lifecycle tracing.
import { invoke } from "@tauri-apps/api/core";
import { frontendDebug, frontendError, frontendTrace, type FrontendLogTargetId } from "./frontend_log";
export async function invokeKsp<T>(targetId: FrontendLogTargetId, command: string, args?: Record<string, unknown>): Promise<T> {
frontendDebug(targetId, "Frontend IPC command requested", { command });
try {
const result = await invoke<T>(command, args);
frontendTrace(targetId, "Frontend IPC command completed", { command });
return result;
} catch (caughtError) {
frontendError(targetId, "Frontend IPC command failed", { command });
throw caughtError;
}
}

View File

@@ -1,20 +1,140 @@
// file: crates/ksp-app-config-desk/frontend/ts/main.ts
// version: 2
// version: 3
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
import "simplebar";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { frontendInfo, installFrontendConsoleBridge } from "./frontend_log";
import type { AppSnapshotDto } from "./bindings/ksp_app_config_desk/dto_common/AppSnapshotDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
installFrontendConsoleBridge("main");
document.addEventListener("DOMContentLoaded", () => {
const windowLabel = getCurrentWindow().label;
const status = document.querySelector<HTMLElement>("#shellStatus");
if (status) {
status.textContent = `Frontend chargé dans la fenêtre '${windowLabel}'.`;
type ViewId = "overview" | "documents" | "profiles" | "environment" | "logging";
const viewCopy: Record<ViewId, { title: string; description: string }> = {
overview: {
title: "Vue d'ensemble",
description: "État sûr du bootstrap Config Desk et du runtime Logging actuellement actif.",
},
documents: {
title: "Documents",
description: "Inventaire, diagnostics et réparation des documents Config — surface fonctionnelle prévue en pre.009.",
},
profiles: {
title: "Profils",
description: "Sélection et provenance des profils Config — surface fonctionnelle prévue en pre.010.",
},
environment: {
title: "Environnement / .env",
description: "Rapports et management des variables KSP/KSPB — surfaces fonctionnelles prévues en pre.011/pre.012.",
},
logging: {
title: "Logging",
description: "Édition des profils et hot reload du runtime Logging — surfaces fonctionnelles prévues en pre.014 à pre.017.",
},
};
function isViewId(value: string): value is ViewId {
return Object.prototype.hasOwnProperty.call(viewCopy, value);
}
function activateView(viewId: ViewId, source: "startup" | "user"): void {
if (source === "user") {
frontendDebug("main", "Main navigation activated", { viewId });
}
const copy = viewCopy[viewId];
const title = document.querySelector<HTMLElement>("#viewTitle");
const description = document.querySelector<HTMLElement>("#viewDescription");
if (title) {
title.textContent = copy.title;
}
if (description) {
description.textContent = copy.description;
}
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
const active = button.dataset.view === viewId;
button.classList.toggle("active", active);
button.setAttribute("aria-current", active ? "page" : "false");
});
const overview = document.querySelector<HTMLElement>("#overviewPanel");
if (overview) {
overview.hidden = viewId !== "overview";
}
const placeholder = document.querySelector<HTMLElement>("#placeholderPanel");
if (placeholder) {
placeholder.hidden = viewId === "overview";
}
frontendTrace("main", "Main view DOM updated", { viewId, source });
}
function bindNavigation(): void {
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
button.addEventListener("click", () => {
const requestedView = button.dataset.view;
if (requestedView && isViewId(requestedView)) {
activateView(requestedView, "user");
}
});
});
frontendTrace("main", "Main navigation handlers installed");
}
function renderSnapshot(snapshot: AppSnapshotDto): void {
const version = document.querySelector<HTMLElement>("#overviewVersion");
const documents = document.querySelector<HTMLElement>("#overviewDocuments");
const profile = document.querySelector<HTMLElement>("#overviewLoggingProfile");
const generation = document.querySelector<HTMLElement>("#overviewLoggingGeneration");
const fallback = document.querySelector<HTMLElement>("#overviewLoggingFallback");
if (version) {
version.textContent = snapshot.applicationVersion;
}
if (documents) {
documents.textContent = snapshot.configDocumentCount.toString();
}
if (profile) {
profile.textContent = snapshot.activeLoggingProfile ?? "fallback transitoire";
}
if (generation) {
generation.textContent = snapshot.loggingGeneration.toString();
}
if (fallback) {
fallback.textContent = snapshot.fallbackLoggingActive ? "oui" : "non";
}
frontendTrace("main", "Application snapshot rendered", {
loggingGeneration: snapshot.loggingGeneration,
fallbackLoggingActive: snapshot.fallbackLoggingActive,
});
}
async function loadSnapshot(): Promise<void> {
const status = document.querySelector<HTMLElement>("#shellStatus");
const snapshot = await invokeKsp<AppSnapshotDto>("main", "get_app_snapshot");
renderSnapshot(snapshot);
if (status) {
status.textContent = "Shell principal prêt.";
}
frontendTrace("main", "Main shell status replaced", { status: "ready" });
}
async function initializeMain(): Promise<void> {
const windowLabel = getCurrentWindow().label;
frontendInfo("main", "Config Desk main frontend loaded", { windowLabel });
bindNavigation();
activateView("overview", "startup");
try {
await loadSnapshot();
} catch {
const status = document.querySelector<HTMLElement>("#shellStatus");
if (status) {
status.textContent = "Le snapshot initial n'a pas pu être chargé.";
}
frontendTrace("main", "Main shell status replaced", { status: "snapshot_error" });
}
}
document.addEventListener("DOMContentLoaded", () => {
void initializeMain();
});

View File

@@ -1,16 +1,113 @@
// file: crates/ksp-app-config-desk/frontend/ts/splash.ts
// version: 2
// version: 3
import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { frontendInfo, installFrontendConsoleBridge } from "./frontend_log";
import type { SplashOrderDto } from "./bindings/ksp_app_config_desk/splash/SplashOrderDto.ts";
import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
installFrontendConsoleBridge("splash");
document.addEventListener("DOMContentLoaded", () => {
const windowLabel = getCurrentWindow().label;
const defaultFadeDurationMs = 300;
let activeOpacityFrame: number | null = null;
function normalizeDurationMs(value: number | null | undefined): number {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
return defaultFadeDurationMs;
}
return value;
}
function easeInOut(progress: number): number {
if (progress < 0.5) {
return 2 * progress * progress;
}
return 1 - Math.pow(-2 * progress + 2, 2) / 2;
}
async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpacity: number, durationMs: number): Promise<void> {
frontendTrace("splash", "Splash opacity animation started", { fromOpacity, toOpacity, durationMs });
if (activeOpacityFrame !== null) {
cancelAnimationFrame(activeOpacityFrame);
activeOpacityFrame = null;
}
element.style.opacity = fromOpacity.toString();
element.style.willChange = "opacity";
await new Promise<void>(resolve => requestAnimationFrame(() => resolve()));
await new Promise<void>(resolve => {
const startedAt = performance.now();
const opacityDelta = toOpacity - fromOpacity;
const updateOpacity = (currentTime: number): void => {
const elapsedMs = currentTime - startedAt;
const rawProgress = Math.min(elapsedMs / durationMs, 1);
element.style.opacity = (fromOpacity + opacityDelta * easeInOut(rawProgress)).toString();
if (rawProgress >= 1) {
element.style.opacity = toOpacity.toString();
element.style.willChange = "auto";
activeOpacityFrame = null;
resolve();
return;
}
activeOpacityFrame = requestAnimationFrame(updateOpacity);
};
activeOpacityFrame = requestAnimationFrame(updateOpacity);
});
frontendTrace("splash", "Splash opacity animation completed", { toOpacity, durationMs });
}
function replaceStatus(message: string | null): void {
if (!message) {
return;
}
const status = document.querySelector<HTMLElement>("#splash-status");
if (status) {
status.textContent = `Frontend chargé dans la fenêtre '${windowLabel}'. Lifecycle complet prévu en pre.008.`;
status.textContent = message;
frontendTrace("splash", "Splash status replaced", { message });
}
}
async function handleSplashOrder(order: SplashOrderDto): Promise<void> {
frontendTrace("splash", "Splash order received", { action: order.action });
const container = document.querySelector<HTMLElement>("#splash-container");
replaceStatus(order.message);
if (!container) {
return;
}
if (order.action === "fade_in") {
await animateOpacity(container, 0, 1, normalizeDurationMs(order.durationMs));
return;
}
if (order.action === "fade_out") {
await animateOpacity(container, 1, 0, normalizeDurationMs(order.durationMs));
}
}
async function initializeSplash(): Promise<void> {
const windowLabel = getCurrentWindow().label;
frontendInfo("splash", "Config Desk splash frontend loaded", { windowLabel });
const container = document.querySelector<HTMLElement>("#splash-container");
if (container) {
container.style.opacity = "0";
container.style.willChange = "opacity";
frontendTrace("splash", "Splash container prepared for managed fade-in");
}
await listen<SplashOrderDto>("ksp-splash-order", event => {
void handleSplashOrder(event.payload);
});
frontendDebug("splash", "Splash lifecycle listener installed");
try {
await invokeKsp<void>("splash", "splash_frontend_ready");
} catch {
replaceStatus("Le lifecycle du splash n'a pas pu démarrer.");
if (container) {
container.style.opacity = "1";
container.style.willChange = "auto";
}
frontendError("splash", "Splash readiness command failed");
}
}
document.addEventListener("DOMContentLoaded", () => {
void initializeSplash();
});