v0.1.4-pre.008
This commit is contained in:
19
crates/ksp-app-config-desk/frontend/ts/invoke.ts
Normal file
19
crates/ksp-app-config-desk/frontend/ts/invoke.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user