v0.3.7-pre.002

This commit is contained in:
2026-09-02 09:31:46 +02:00
parent dd85ac5e77
commit bc7ccd97a5
49 changed files with 3390 additions and 6 deletions

View File

@@ -0,0 +1,110 @@
// file: crates/ksp-app-backfill-desk/frontend/ts/frontend_log.ts
// version: 1
import { invoke } from "@tauri-apps/api/core";
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_backfill_desk/frontend_logging/FrontendLogPayloadDto.ts";
export type FrontendLogLevel = "trace" | "debug" | "info" | "warn" | "error";
export type FrontendLogTargetId = "frontend" | "main" | "splash";
type ConsoleMethod = (...items: unknown[]) => void;
const originalConsole = {
trace: console.trace.bind(console),
debug: console.debug.bind(console),
log: console.log.bind(console),
info: console.info.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
};
function stringifyItem(item: unknown): string {
if (item instanceof Error) {
return item.stack ?? item.message;
}
if (typeof item === "string") {
return item;
}
try {
const serialized = JSON.stringify(item);
return serialized ?? String(item);
} catch {
return String(item);
}
}
function formatMessage(items: unknown[]): string {
return items.map(item => stringifyItem(item)).join(" ");
}
async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
const payload: FrontendLogPayloadDto = {
level,
targetId,
message,
};
await invoke("emit_frontend_log", { payload });
}
function writeOriginalConsole(level: FrontendLogLevel, message: string): void {
return level === "trace"
? originalConsole.trace(message)
: level === "debug"
? originalConsole.debug(message)
: level === "info"
? originalConsole.info(message)
: level === "warn"
? originalConsole.warn(message)
: originalConsole.error(message);
}
export async function emitFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise<void> {
writeOriginalConsole(level, message);
await sendFrontendLog(level, targetId, message);
}
export function frontendTrace(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.trace(message);
void sendFrontendLog("trace", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
export function frontendDebug(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.debug(message);
void sendFrontendLog("debug", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
export function frontendInfo(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.info(message);
void sendFrontendLog("info", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
export function frontendWarn(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.warn(message);
void sendFrontendLog("warn", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
export function frontendError(targetId: FrontendLogTargetId, ...items: unknown[]): void {
const message = formatMessage(items);
originalConsole.error(message);
void sendFrontendLog("error", targetId, message).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
}
function buildConsoleBridge(level: FrontendLogLevel, targetId: FrontendLogTargetId, original: ConsoleMethod): ConsoleMethod {
return (...items: unknown[]) => {
original(...items);
void sendFrontendLog(level, targetId, formatMessage(items)).catch(caughtError => originalConsole.error("KSP frontend logging bridge failed", caughtError));
};
}
export function installFrontendConsoleBridge(targetId: FrontendLogTargetId): void {
console.trace = buildConsoleBridge("trace", targetId, originalConsole.trace);
console.debug = buildConsoleBridge("debug", targetId, originalConsole.debug);
console.log = buildConsoleBridge("info", targetId, originalConsole.log);
console.info = buildConsoleBridge("info", targetId, originalConsole.info);
console.warn = buildConsoleBridge("warn", targetId, originalConsole.warn);
console.error = buildConsoleBridge("error", targetId, originalConsole.error);
}

View File

@@ -0,0 +1,17 @@
// file: crates/ksp-app-backfill-desk/frontend/ts/invoke.ts
// version: 1
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

@@ -0,0 +1,144 @@
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
// version: 1
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
import "simplebar";
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { ShellStatusDto } from "./bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
installFrontendConsoleBridge("main");
type ViewId = "backfill" | "diagnostics";
const viewTitles: Record<ViewId, string> = {
backfill: "Backfill",
diagnostics: "Diagnostics",
};
function isViewId(value: string | undefined): value is ViewId {
return value === "backfill" || value === "diagnostics";
}
function activateView(viewId: ViewId, source: "startup" | "user"): void {
document.querySelectorAll<HTMLElement>("[data-view-panel]").forEach(panel => {
panel.hidden = panel.dataset.viewPanel !== viewId;
});
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 header = document.querySelector<HTMLElement>("#headerViewTitle");
if (header) {
header.textContent = viewTitles[viewId];
}
document.title = `Backfill Desk — ${viewTitles[viewId]}`;
frontendTrace("main", "Backfill Desk view DOM updated", { viewId, source });
}
function bindFrontendInteractions(): void {
document.addEventListener(
"click",
event => {
const source = event.target;
if (!(source instanceof Element)) {
return;
}
const control = source.closest<HTMLElement>('button, a, input, select, textarea, [role="button"], [role="tab"], [data-view]');
if (!control) {
return;
}
frontendTrace("main", "Backfill Desk frontend control clicked", {
controlId: control.id || null,
role: control.getAttribute("role"),
tagName: control.tagName.toLowerCase(),
viewId: control.dataset.view ?? null,
});
},
true,
);
frontendTrace("main", "Backfill Desk frontend control interaction logger installed");
}
function bindNavigation(): void {
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
button.addEventListener("click", () => {
const viewId = button.dataset.view;
frontendDebug("main", "Backfill Desk navigation tab clicked", { viewId: viewId ?? "unknown" });
if (isViewId(viewId)) {
activateView(viewId, "user");
}
});
});
frontendTrace("main", "Backfill Desk navigation handlers installed");
}
function renderRuntimeStatus(status: ShellStatusDto): void {
const values: Record<string, string> = {
runtimeVersion: status.applicationVersion,
runtimeShellPhase: status.shellPhase,
runtimeConfigDocuments: status.configDocumentCount.toString(),
runtimeLoggingProfile: status.activeLoggingProfile ?? "fallback transitoire",
runtimeLoggingFallback: status.fallbackLoggingActive ? "oui" : "non",
};
for (const [id, value] of Object.entries(values)) {
const element = document.querySelector<HTMLElement>(`#${id}`);
if (element) {
element.textContent = value;
}
}
const badge = document.querySelector<HTMLElement>("#appVersionBadge");
if (badge) {
badge.textContent = status.applicationVersion;
}
const diagnostic = document.querySelector<HTMLElement>("#runtimeDiagnostic");
if (diagnostic) {
diagnostic.hidden = status.startupDiagnostic === null;
diagnostic.textContent = status.startupDiagnostic === null ? "" : `${status.startupDiagnostic.domain}/${status.startupDiagnostic.code}: ${status.startupDiagnostic.message}`;
}
frontendTrace("main", "Backfill Desk runtime status rendered", {
fallbackLoggingActive: status.fallbackLoggingActive,
configDocumentCount: status.configDocumentCount,
});
}
async function loadRuntimeStatus(source: "startup" | "user"): Promise<void> {
frontendDebug("main", "Backfill Desk runtime status load started", { source });
const status = await invokeKsp<ShellStatusDto>("main", "get_runtime_status");
renderRuntimeStatus(status);
frontendDebug("main", "Backfill Desk runtime status load completed", { source });
}
function bindRuntimeStatusRefresh(): void {
const button = document.querySelector<HTMLButtonElement>("#refreshRuntimeStatus");
if (!button) {
return;
}
button.addEventListener("click", () => {
frontendDebug("main", "Backfill Desk runtime status refresh button clicked");
void loadRuntimeStatus("user").catch(() => frontendWarn("main", "Backfill Desk runtime status refresh failed"));
});
frontendTrace("main", "Backfill Desk runtime status refresh handler installed");
}
async function initializeMain(): Promise<void> {
const windowLabel = getCurrentWindow().label;
frontendInfo("main", "Backfill Desk main frontend loaded", { windowLabel });
bindFrontendInteractions();
bindNavigation();
bindRuntimeStatusRefresh();
activateView("backfill", "startup");
try {
await loadRuntimeStatus("startup");
} catch {
frontendWarn("main", "Backfill Desk startup runtime status load failed");
}
}
document.addEventListener("DOMContentLoaded", () => {
void initializeMain();
});

View File

@@ -0,0 +1,131 @@
// file: crates/ksp-app-backfill-desk/frontend/ts/splash.ts
// version: 1
import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { SplashOrderDto } from "./bindings/ksp_app_backfill_desk/splash/SplashOrderDto.ts";
import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
installFrontendConsoleBridge("splash");
let activeOpacityFrame: number | null = null;
function easeInOut(value: number): number {
return value < 0.5 ? 2 * value * value : 1 - Math.pow(-2 * value + 2, 2) / 2;
}
function normalizeDurationMs(value: number | null): number {
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
}
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 = durationMs === 0 ? 1 : 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 scrollToLatest(element: HTMLElement): void {
element.scrollTop = element.scrollHeight;
}
function addMessage(message: string, status: string | null): void {
const container = document.querySelector<HTMLElement>("#messages-container");
if (!container) {
return;
}
const line = document.createElement("div");
const safeStatus = status === "warning" || status === "error" || status === "success" ? status : "info";
line.className = `splash-message ${safeStatus}`;
line.textContent = message;
container.appendChild(line);
scrollToLatest(container);
}
function addDebugMessage(message: string): void {
const container = document.querySelector<HTMLElement>("#debug-info");
if (!container) {
return;
}
container.hidden = false;
const line = document.createElement("div");
line.textContent = `${new Date().toLocaleTimeString()}: ${message}`;
container.appendChild(line);
scrollToLatest(container);
}
async function handleSplashOrder(order: SplashOrderDto): Promise<void> {
frontendTrace("splash", "Splash order received", { action: order.action });
const container = document.querySelector<HTMLElement>("#splash-container");
if (order.action === "add_message" && order.message) {
addMessage(order.message, order.status);
return;
}
if (order.action === "add_debug" && order.message) {
addDebugMessage(order.message);
return;
}
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", "Backfill 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 {
addMessage("Le lifecycle du splash n'a pas pu démarrer.", "error");
if (container) {
container.style.opacity = "1";
container.style.willChange = "auto";
}
frontendError("splash", "Splash readiness command failed");
}
}
document.addEventListener("DOMContentLoaded", () => {
void initializeSplash();
});