v0.3.8-pre.002

This commit is contained in:
2026-09-03 10:44:49 +02:00
parent 3df8fc74c1
commit 6a1892792c
50 changed files with 3409 additions and 23 deletions

View File

@@ -0,0 +1,40 @@
# file: crates/ksp-app-store-desk/Cargo.toml
# version: 1
[package]
name = "ksp-app-store-desk"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
publish.workspace = true
[lib]
name = "ksp_app_store_desk_lib"
path = "src/lib.rs"
crate-type = ["staticlib", "cdylib", "rlib"]
[[bin]]
name = "ksp-app-store-desk"
path = "src/main.rs"
[build-dependencies]
tauri-build.workspace = true
[dependencies]
chrono = { workspace = true, features = ["std", "now"] }
fs2.workspace = true
ksp-config-lib = { path = "../ksp-config-lib" }
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
serde = { workspace = true, features = ["derive"] }
tauri.workspace = true
tauri-plugin-tracing.workspace = true
tokio = { workspace = true, features = ["time"] }
ts-rs.workspace = true
[dev-dependencies]
serde_json.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,12 @@
// file: crates/ksp-app-store-desk/build.rs
// version: 1
//! Build script for the KSP Store desktop application.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Minimal Store Desk desktop capability set.",
"windows": [
"splash",
"main"
],
"permissions": [
"core:default",
"tracing:default"
]
}

View File

@@ -0,0 +1,25 @@
<!-- file: crates/ksp-app-backfill-desk/frontend/fonts/README.md -->
<!-- version: 1 -->
# Fonts du splash Backfill Desk
Backfill Desk réutilise le gabarit de splash KSP déjà stabilisé dans Config Desk et Wallet Desk et conserve la police locale :
```text
DOS_Amazigh.ttf
font-family: Dos Amazigh
```
Destination :
```text
crates/ksp-app-backfill-desk/frontend/fonts/DOS_Amazigh.ttf
```
Empreinte SHA-256 de l'asset repris du gabarit :
```text
a01ff4b5a0d699db7cefcf5336bf0e379ff6fbf0d096087571a13c8782f9aa02
```
`splash.scss` déclare `Dos Amazigh` avec `@font-face` et l'applique au titre `#app-name`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

View File

@@ -0,0 +1,137 @@
<!-- file: crates/ksp-app-store-desk/frontend/main.html -->
<!-- version: 1 -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="icon" href="imgs/logo.png">
<link rel="stylesheet" href="sass/main.scss">
<title>Store Desk — Overview</title>
</head>
<body>
<header class="app-header">
<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 flex-nowrap">
<img alt="KSP" src="imgs/logo.png" class="app-logo">
<span class="ps-2 fs-4 fw-semibold text-primary text-nowrap">Store Desk</span>
<span class="mx-2 fs-5 text-body-secondary" aria-hidden="true"></span>
<span id="headerViewTitle" class="fs-5 text-body text-nowrap">Overview</span>
</div>
</div>
</nav>
</header>
<main class="app-main container-fluid p-0">
<div class="row g-0 h-100 flex-nowrap">
<aside class="col-auto app-sidebar border-end bg-body-tertiary">
<div class="app-sidebar-scroll h-100" data-simplebar>
<nav class="nav nav-pills flex-column gap-1 p-3" aria-label="Navigation Store Desk">
<button class="nav-link text-start active" type="button" data-view="overview">
<i class="fa-solid fa-gauge-high me-2" aria-hidden="true"></i>Overview
</button>
<button class="nav-link text-start" type="button" data-view="transactions">
<i class="fa-solid fa-receipt me-2" aria-hidden="true"></i>RAW Transactions
</button>
<button class="nav-link text-start" type="button" data-view="accounts">
<i class="fa-solid fa-box-archive me-2" aria-hidden="true"></i>RAW Accounts
</button>
<button class="nav-link text-start" type="button" data-view="diagnostics">
<i class="fa-solid fa-stethoscope me-2" aria-hidden="true"></i>Diagnostics
</button>
</nav>
</div>
</aside>
<section class="col app-content app-scrollable h-100" data-simplebar>
<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">
<section data-view-panel="overview">
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
<div>
<h1 class="h3 mb-1">Overview</h1>
<p class="text-body-secondary mb-0">Scaffold read-only. Le lifecycle Store et les diagnostics backend-neutral seront branchés dans une tranche ultérieure.</p>
</div>
<span id="appVersionBadge" class="badge text-bg-secondary"></span>
</div>
<div class="row g-3">
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Store target</span><div class="fw-semibold mt-2">Non ouvert</div></div></div></div>
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Health</span><div class="fw-semibold mt-2">À venir</div></div></div></div>
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Transactions</span><div class="fw-semibold mt-2">Table skeleton</div></div></div></div>
<div class="col-md-6 col-xl-3"><div class="card h-100 shadow-sm"><div class="card-body"><span class="text-body-secondary small">Accounts</span><div class="fw-semibold mt-2">Table skeleton</div></div></div></div>
</div>
</section>
<section data-view-panel="transactions" hidden>
<div class="mb-4">
<h1 class="h3 mb-1">RAW Transactions</h1>
<p class="text-body-secondary mb-0">Structure DataTables uniquement. Aucune capability Store n'est encore invoquée.</p>
</div>
<div class="table-responsive app-store-table-container">
<table id="rawTransactionsTable" class="table table-hover align-middle mb-0 app-store-table">
<caption class="visually-hidden">Structure future de la table RAW Transactions.</caption>
<thead><tr><th>Signature</th><th>Slot</th><th>Block time</th><th>Format</th><th>Payload size</th><th>Retention</th></tr></thead>
<tbody></tbody>
</table>
</div>
</section>
<section data-view-panel="accounts" hidden>
<div class="mb-4">
<h1 class="h3 mb-1">RAW Accounts</h1>
<p class="text-body-secondary mb-0">Structure DataTables uniquement. Aucune donnée Account ni bytes ne traversent IPC dans ce scaffold.</p>
</div>
<div class="table-responsive app-store-table-container">
<table id="rawAccountsTable" class="table table-hover align-middle mb-0 app-store-table">
<caption class="visually-hidden">Structure future de la table RAW Accounts.</caption>
<thead><tr><th>Pubkey</th><th>Slot</th><th>State hash</th><th>Owner</th><th>Lamports</th><th>Data length</th></tr></thead>
<tbody></tbody>
</table>
</div>
</section>
<section data-view-panel="diagnostics" hidden>
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
<div>
<h1 class="h3 mb-1">Diagnostics</h1>
<p class="text-body-secondary mb-0">État sûr du scaffold Config / Logging. Aucun diagnostic physique Store n'est disponible à cette tranche.</p>
</div>
<button id="refreshDiagnostics" class="btn btn-outline-primary" type="button">
<i class="fa-solid fa-arrows-rotate me-1" aria-hidden="true"></i>Refresh
</button>
</div>
<div class="card shadow-sm">
<div class="card-header fw-semibold">Runtime</div>
<div class="card-body">
<dl class="row mb-0 app-runtime-list">
<dt class="col-sm-5">Version</dt><dd id="runtimeVersion" class="col-sm-7"></dd>
<dt class="col-sm-5">Phase</dt><dd id="runtimeShellPhase" class="col-sm-7"></dd>
<dt class="col-sm-5">Documents Config</dt><dd id="runtimeConfigDocuments" class="col-sm-7"></dd>
<dt class="col-sm-5">Profil Logging</dt><dd id="runtimeLoggingProfile" class="col-sm-7"></dd>
<dt class="col-sm-5">Fallback Logging</dt><dd id="runtimeLoggingFallback" class="col-sm-7"></dd>
<dt class="col-sm-5">Store runtime</dt><dd class="col-sm-7">Non ouvert en pre.002</dd>
</dl>
<div id="runtimeDiagnostic" class="alert alert-warning mt-3 mb-0" role="status" aria-live="polite" hidden></div>
</div>
</div>
</section>
</div>
</div>
</div>
</section>
</div>
</main>
<footer class="app-footer bg-dark text-light">
<div class="container h-100 d-flex align-items-center justify-content-center">
<small>&copy; 2026 SASEDEV</small>
</div>
</footer>
<script type="module" src="ts/main.ts" defer></script>
</body>
</html>

View File

@@ -0,0 +1,92 @@
// file: crates/ksp-app-store-desk/frontend/sass/_app.scss
// version: 1
$app-header-height: 72px;
$app-footer-height: 42px;
$app-sidebar-width: 250px;
html,
body {
width: 100%;
height: 100%;
}
body {
margin: 0;
overflow: hidden;
background: $gray-100;
}
.app-header {
position: fixed;
inset: 0 0 auto;
height: $app-header-height;
z-index: 1020;
border-bottom: 2px solid rgba($primary, 0.3);
}
.app-footer {
position: fixed;
inset: auto 0 0;
height: $app-footer-height;
z-index: 1020;
border-top: 2px solid rgba($primary, 0.3);
}
.app-main {
position: relative;
height: calc(100vh - $app-header-height - $app-footer-height);
margin-top: $app-header-height;
margin-bottom: $app-footer-height;
overflow: hidden;
}
.app-main > .row {
flex-wrap: nowrap;
min-width: 0;
}
.app-scrollable {
height: 100%;
max-height: 100%;
}
.app-logo {
display: block;
width: auto;
height: 42px;
}
.app-shell-card {
max-width: 1320px;
}
.app-sidebar {
width: $app-sidebar-width;
min-width: $app-sidebar-width;
max-width: $app-sidebar-width;
}
.app-sidebar-scroll,
.app-content {
height: 100%;
max-height: 100%;
}
.app-content {
min-width: 0;
}
.app-sidebar .nav-link {
white-space: nowrap;
}
.app-runtime-list dd,
.app-store-table td:first-child,
.app-store-table td:nth-child(2) {
font-family: var(--bs-font-monospace);
}
.app-store-table-container {
min-height: 240px;
}

View File

@@ -0,0 +1,160 @@
// file: crates/ksp-app-store-desk/frontend/sass/_bootswatch.scss
// version: 1
// Pulse 5.3.8
// Bootswatch
// Variables
// Buttons
.btn {
&:focus,
&:active,
&:active:focus,
&.active:focus {
outline: none;
}
&-secondary {
color: $gray-900;
background-color: $white;
border-color: #ccc;
&:hover {
color: $gray-900;
background-color: $gray-300;
border-color: $gray-500;
}
&.disabled {
color: tint-color($gray-900, 5%);
background-color: $white;
border-color: tint-color(#ccc, 5%);
}
}
&-warning {
color: $white;
}
&-primary:focus {
box-shadow: 0 0 5px tint-color($primary, 10%);
}
&-secondary:focus {
box-shadow: 0 0 5px $gray-400;
}
&-success:focus {
box-shadow: 0 0 5px tint-color($success, 10%);
}
&-info:focus {
box-shadow: 0 0 5px tint-color($info, 10%);
}
&-warning:focus {
box-shadow: 0 0 5px tint-color($warning, 10%);
}
&-danger:focus {
box-shadow: 0 0 5px tint-color($danger, 10%);
}
&.disabled:focus {
box-shadow: none;
}
}
// Tables
.table .thead-dark th {
background-color: $secondary;
border-color: $table-border-color;
}
.table-primary,
.table-secondary,
.table-success,
.table-warning,
.table-danger,
.table-info,
.table-light {
--#{$prefix}table-color: #{$body-color};
}
// Forms
.form-control:focus {
box-shadow: 0 0 5px rgba(100, 65, 164, .4);
}
// Navs
.nav-tabs {
.nav-link,
.nav-link.active {
border-width: 0 0 1px;
}
.nav-link:hover,
.nav-link.active,
.nav-link.active:hover,
.nav-link.active:focus {
border-bottom: 1px solid $primary;
}
.nav-item+.nav-item {
margin-left: 0;
}
}
.breadcrumb {
&-item.active {
color: $gray-700;
}
}
// Indicators
.badge {
&.bg-light {
color: $dark;
}
}
// Progress bars
.progress {
height: 8px;
}
// Containers
.list-group {
&-item {
color: rgba(255, 255, 255, .8);
&.active,
&:hover,
&:focus {
color: $white;
}
&.active {
font-weight: 700;
&:hover {
background-color: $list-group-hover-bg;
}
}
&.disabled:hover {
color: $list-group-disabled-color;
}
}
}

View File

@@ -0,0 +1,19 @@
// file: crates/ksp-app-store-desk/frontend/sass/_fontawesome.scss
// version: 1
//@use '@fortawesome/fontawesome-free/scss/variables' with (
// // customizing $font-path - make sure it points to where your webfonts are stored in your project
// $font-path: '../webfonts',
//);
@use '@fortawesome/fontawesome-free/scss/variables' with (
// use fonts from @fortawesome/fontawesome-free
$font-path: '@fortawesome/fontawesome-free/webfonts',
);
// load Font Awesome core
@use '@fortawesome/fontawesome-free/scss/fontawesome';
// load and make available Font Awesome helpers (mixins, functions, and variables)
@use '@fortawesome/fontawesome-free/scss/fa' as fa;
@use '@fortawesome/fontawesome-free/scss/brands' as fa-brands;
@use '@fortawesome/fontawesome-free/scss/regular' as fa-regular;
@use '@fortawesome/fontawesome-free/scss/solid' as fa-solid;

View File

@@ -0,0 +1,248 @@
// file: crates/ksp-app-store-desk/frontend/sass/_simplebar.scss
// version: 1
/* Rtl support */
[data-simplebar] {
position: relative;
flex-direction: column;
flex-wrap: wrap;
justify-content: flex-start;
align-content: flex-start;
align-items: flex-start;
}
.simplebar-wrapper {
overflow: hidden;
width: inherit;
height: inherit;
max-width: inherit;
max-height: inherit;
}
.simplebar-mask {
direction: inherit;
position: absolute;
overflow: hidden;
padding: 0;
margin: 0;
left: 0;
top: 0;
bottom: 0;
right: 0;
width: auto !important;
height: auto !important;
// z-index: 0;
inset: 0;
}
.simplebar-offset {
direction: inherit !important;
box-sizing: inherit !important;
resize: none !important;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
padding: 0;
margin: 0;
-webkit-overflow-scrolling: touch;
inset: 0;
}
.simplebar-content-wrapper {
direction: inherit;
box-sizing: border-box !important;
position: relative;
display: block;
height: 100%;
width: auto;
max-width: 100%;
max-height: 100%;
overflow: auto;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}
.simplebar-hide-scrollbar {
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
position: fixed;
left: 0;
visibility: hidden;
overflow-y: scroll;
scrollbar-width: none;
-ms-overflow-style: none;
}
.simplebar-content {
&:before {
content: ' ';
display: table;
}
&:after {
content: ' ';
display: table;
}
}
.simplebar-placeholder {
max-height: 100%;
max-width: 100%;
width: 100%;
pointer-events: none;
}
.simplebar-height-auto-observer-wrapper {
box-sizing: inherit !important;
height: 100%;
width: 100%;
max-width: 1px;
position: relative;
float: left;
max-height: 1px;
overflow: hidden;
// z-index: -1;
padding: 0;
margin: 0;
pointer-events: none;
flex-grow: inherit;
flex-shrink: 0;
flex-basis: 0;
}
.simplebar-height-auto-observer {
box-sizing: inherit;
display: block;
opacity: 0;
position: absolute;
top: 0;
left: 0;
height: 1000%;
width: 1000%;
min-height: 1px;
min-width: 1px;
overflow: hidden;
pointer-events: none;
// z-index: -1;
}
.simplebar-track {
// z-index: 1;
position: absolute;
right: 0;
bottom: 0;
pointer-events: none;
overflow: hidden;
}
[data-simplebar].simplebar-dragging {
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.simplebar-content {
pointer-events: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.simplebar-track {
pointer-events: all;
}
}
.simplebar-scrollbar {
position: absolute;
left: 0;
right: 0;
min-height: 10px;
&:before {
position: absolute;
content: '';
background: black;
border-radius: 7px;
left: 2px;
right: 2px;
opacity: 0;
transition: opacity 0.2s 0.5s linear;
top: 2px;
bottom: 2px;
}
}
.simplebar-scrollbar.simplebar-visible {
&:before {
opacity: 0.5;
transition-delay: 0s;
transition-duration: 0s;
}
}
.simplebar-track.simplebar-vertical {
top: 0;
width: 11px;
}
.simplebar-track.simplebar-horizontal {
left: 0;
height: 11px;
.simplebar-scrollbar {
right: auto;
left: 0;
top: 0;
bottom: 0;
min-height: 0;
min-width: 10px;
width: auto;
}
}
[data-simplebar-direction='rtl'] {
.simplebar-track.simplebar-vertical {
right: auto;
left: 0;
}
}
.simplebar-dummy-scrollbar-size {
direction: rtl;
position: fixed;
opacity: 0;
visibility: hidden;
height: 500px;
width: 500px;
overflow-y: hidden;
overflow-x: scroll;
-ms-overflow-style: scrollbar !important;
>div {
width: 200%;
height: 200%;
margin: 10px 0;
}
}
.simplebar-hover {
cursor: pointer;
}

View File

@@ -0,0 +1,95 @@
// file: crates/ksp-app-store-desk/frontend/sass/_variables.scss
// version: 1
// Pulse 5.3.8
// Bootswatch
$theme: "pulse" !default;
//
// Color system
//
$white: #fff !default;
$gray-100: #fafafa !default;
$gray-200: #f9f8fc !default;
$gray-300: #ededed !default;
$gray-400: #cbc8d0 !default;
$gray-500: #adb5bd !default;
$gray-600: #868e96 !default;
$gray-700: #444 !default;
$gray-800: #343a40 !default;
$gray-900: #17141f !default;
$black: #000 !default;
$blue: #007bff !default;
$indigo: #6610f2 !default;
$purple: #593196 !default;
$pink: #e83e8c !default;
$red: #fc3939 !default;
$orange: #fd7e14 !default;
$yellow: #efa31d !default;
$green: #13b955 !default;
$teal: #20c997 !default;
$cyan: #009cdc !default;
$primary: $purple !default;
$secondary: #a991d4 !default;
$success: $green !default;
$info: $cyan !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-200 !default;
$dark: $gray-900 !default;
$min-contrast-ratio: 2.1 !default;
// Options
$enable-rounded: false !default;
// Body
$body-color: $gray-700 !default;
// Links
$link-hover-color: $primary !default;
// Tables
$table-color: initial !default;
$table-border-color: rgba(0, 0, 0, .05) !default;
// Forms
$input-focus-border-color: $primary !default;
// Dropdowns
$dropdown-link-hover-color: $white !default;
$dropdown-link-hover-bg: $primary !default;
// Navs
$nav-tabs-border-color: $gray-300 !default;
$nav-tabs-link-hover-border-color: $primary !default;
// Navbar
$navbar-padding-y: 1.2rem !default;
// Progress bars
$progress-bg: $gray-300 !default;
$progress-bar-bg: $primary !default;
// List group
$list-group-bg: $gray-900 !default;
$list-group-border-color: transparent !default;
$list-group-hover-bg: lighten($list-group-bg, 10%) !default;
$list-group-active-color: $white !default;
$list-group-active-bg: $list-group-bg !default;
$list-group-disabled-color: lighten($list-group-bg, 30%) !default;

View File

@@ -0,0 +1,10 @@
// file: crates/ksp-app-store-desk/frontend/sass/main.scss
// version: 1
@import "bootstrap/scss/functions";
@import "variables";
@import "fontawesome";
@import "simplebar";
@import "bootstrap/scss/bootstrap";
@import "bootswatch";
@import "app";

View File

@@ -0,0 +1,137 @@
// file: crates/ksp-app-store-desk/frontend/sass/splash.scss
// version: 1
@font-face {
font-family: "Dos Amazigh";
src: url("../fonts/DOS_Amazigh.ttf") format("truetype");
font-weight: normal;
font-style: normal;
font-display: swap;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
body {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
font-family: Arial, sans-serif;
}
#splash-container {
position: relative;
width: 960px;
max-width: 100%;
height: 637px;
max-height: 100%;
overflow: hidden;
opacity: 0;
will-change: opacity;
}
#splash-image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 1;
object-fit: contain;
user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
pointer-events: none;
}
#app-name {
position: absolute;
top: 50%;
left: 50%;
z-index: 2;
transform: translate(-50%, -50%);
color: #fff;
font-family: "Dos Amazigh", sans-serif;
font-size: 76px;
font-weight: bold;
text-align: center;
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
white-space: nowrap;
user-select: none;
-webkit-user-select: none;
-webkit-user-drag: none;
pointer-events: none;
}
#debug-info {
position: absolute;
top: 0;
left: 0;
z-index: 3;
width: 100%;
max-height: 30%;
overflow-y: auto;
padding: 8px;
color: #fff;
background: rgba(0, 0, 0, 0.18);
font-family: monospace;
font-size: 12px;
scrollbar-width: none;
}
#messages-container {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 3;
max-height: 30%;
overflow-y: auto;
padding: 10px 10px 36px;
background: rgba(0, 0, 0, 0.18);
font-family: Arial, sans-serif;
font-size: 14px;
scrollbar-width: none;
}
#debug-info::-webkit-scrollbar,
#messages-container::-webkit-scrollbar {
display: none;
}
.splash-message {
margin-bottom: 8px;
padding: 6px 10px;
border-radius: 4px;
color: #fff;
font-weight: bold;
animation: splashMessageFadeIn 0.3s ease-in-out;
}
.splash-message.info { background: rgba(33, 150, 243, 0.22); }
.splash-message.warning { background: rgba(255, 152, 0, 0.22); }
.splash-message.error { background: rgba(244, 67, 54, 0.22); }
.splash-message.success { background: rgba(76, 175, 80, 0.22); }
@keyframes splashMessageFadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@@ -0,0 +1,24 @@
<!-- file: crates/ksp-app-store-desk/frontend/splash.html -->
<!-- version: 1 -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<link rel="icon" href="imgs/logo.png">
<link rel="stylesheet" href="sass/splash.scss">
<title>KSP Store Desk — Chargement</title>
</head>
<body>
<div id="splash-container" style="opacity: 0;">
<img id="splash-image" src="imgs/splash.png" alt="Chargement de KSP Store Desk">
<div id="app-name">Store</div>
<div id="debug-info" aria-live="off" hidden></div>
<div id="messages-container" aria-live="polite"></div>
</div>
<script type="module" src="ts/splash.ts" defer></script>
</body>
</html>

View File

@@ -0,0 +1,110 @@
// file: crates/ksp-app-store-desk/frontend/ts/frontend_log.ts
// version: 1
import { invoke } from "@tauri-apps/api/core";
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_store_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-store-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,152 @@
// file: crates/ksp-app-store-desk/frontend/ts/main.ts
// version: 1
import DataTable from "datatables.net-bs5";
import "datatables.net-bs5/css/dataTables.bootstrap5.css";
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
import "simplebar";
import type { ShellStatusDto } from "./bindings/ksp_app_store_desk/dto_common/ShellStatusDto.ts";
import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke";
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
installFrontendConsoleBridge("main");
type ViewId = "overview" | "transactions" | "accounts" | "diagnostics";
const viewTitles: Record<ViewId, string> = {
overview: "Overview",
transactions: "RAW Transactions",
accounts: "RAW Accounts",
diagnostics: "Diagnostics",
};
interface EmptyDataTable {
columns: { adjust(): void };
}
let transactionTable: EmptyDataTable | null = null;
let accountTable: EmptyDataTable | null = null;
function isViewId(value: string | undefined): value is ViewId {
return value === "overview" || value === "transactions" || value === "accounts" || value === "diagnostics";
}
function activateView(viewId: ViewId): void {
document.querySelectorAll<HTMLElement>("[data-view-panel]").forEach(panel => {
panel.hidden = panel.dataset.viewPanel !== viewId;
});
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
button.classList.toggle("active", button.dataset.view === viewId);
});
const title = document.querySelector<HTMLElement>("#headerViewTitle");
if (title) {
title.textContent = viewTitles[viewId];
}
document.title = `Store Desk — ${viewTitles[viewId]}`;
if (viewId === "transactions") {
transactionTable?.columns.adjust();
}
if (viewId === "accounts") {
accountTable?.columns.adjust();
}
frontendDebug("main", "Store Desk view activated", { viewId });
}
function initializeEmptyTables(): void {
const commonOptions = {
data: [],
paging: true,
serverSide: false,
pageLength: 25,
lengthMenu: [25, 50, 100],
searching: false,
ordering: false,
info: true,
scrollX: true,
autoWidth: false,
language: {
emptyTable: "Aucune donnée Store n'est chargée dans le scaffold pre.002.",
},
};
transactionTable = new DataTable("#rawTransactionsTable", commonOptions) as unknown as EmptyDataTable;
accountTable = new DataTable("#rawAccountsTable", commonOptions) as unknown as EmptyDataTable;
frontendTrace("main", "Store Desk empty DataTables skeletons initialized", { pagingOwner: "datatables", serverSide: false });
}
function renderShellStatus(status: ShellStatusDto): void {
const versionBadge = document.querySelector<HTMLElement>("#appVersionBadge");
if (versionBadge) {
versionBadge.textContent = status.applicationVersion;
}
const values: Record<string, string> = {
runtimeVersion: status.applicationVersion,
runtimeShellPhase: status.shellPhase,
runtimeConfigDocuments: status.configDocumentCount.toString(),
runtimeLoggingProfile: status.activeLoggingProfile ?? "fallback",
runtimeLoggingFallback: status.fallbackLoggingActive ? "oui" : "non",
};
for (const [elementId, value] of Object.entries(values)) {
const element = document.querySelector<HTMLElement>(`#${elementId}`);
if (element) {
element.textContent = value;
}
}
const diagnostic = document.querySelector<HTMLElement>("#runtimeDiagnostic");
if (diagnostic) {
diagnostic.hidden = status.startupDiagnostic === null;
diagnostic.textContent = status.startupDiagnostic ? `${status.startupDiagnostic.domain}/${status.startupDiagnostic.code}: ${status.startupDiagnostic.message}` : "";
}
frontendTrace("main", "Store Desk shell status rendered", {
fallbackLoggingActive: status.fallbackLoggingActive,
shellPhase: status.shellPhase,
});
}
async function refreshDiagnostics(): Promise<void> {
frontendDebug("main", "Store Desk diagnostics refresh started");
try {
const status = await invokeKsp<ShellStatusDto>("main", "get_shell_status");
renderShellStatus(status);
frontendDebug("main", "Store Desk diagnostics refresh completed");
} catch {
frontendError("main", "Store Desk diagnostics refresh failed");
}
}
function installInteractions(): void {
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
button.addEventListener("click", () => {
const viewId = button.dataset.view;
frontendDebug("main", "Store Desk navigation button clicked", { viewId: viewId ?? "missing" });
if (isViewId(viewId)) {
activateView(viewId);
}
});
});
const refreshButton = document.querySelector<HTMLButtonElement>("#refreshDiagnostics");
refreshButton?.addEventListener("click", () => {
frontendDebug("main", "Store Desk diagnostics refresh button clicked");
void refreshDiagnostics();
});
document.querySelectorAll<HTMLButtonElement>("button").forEach(button => {
if (button.dataset.view || button.id === "refreshDiagnostics") {
return;
}
button.addEventListener("click", () => frontendDebug("main", "Store Desk generic button clicked", { buttonId: button.id || "anonymous" }));
});
frontendTrace("main", "Store Desk frontend interactions installed");
}
async function initializeMain(): Promise<void> {
frontendInfo("main", "Store Desk main frontend loaded");
initializeEmptyTables();
installInteractions();
activateView("overview");
await refreshDiagnostics();
}
document.addEventListener("DOMContentLoaded", () => {
void initializeMain();
});

View File

@@ -0,0 +1,131 @@
// file: crates/ksp-app-store-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_store_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", "Store 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();
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,27 @@
{
"name": "ksp-app-store-desk",
"private": true,
"version": "0.3.8-pre.2",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build"
},
"dependencies": {
"@fltsci/tauri-plugin-tracing": "^0.3",
"@fortawesome/fontawesome-free": "^7.3",
"@tauri-apps/api": "^2.11",
"bootstrap": "^5.3",
"datatables.net-bs5": "^3.0",
"resize-observer-polyfill": "^1.5",
"simplebar": "^6.3"
},
"devDependencies": {
"@tauri-apps/cli": "^2.11",
"@types/bootstrap": "^5.2",
"@types/node": "^26.1",
"sass-embedded": "^1.102",
"typescript": "^7.0",
"vite": "^8.2"
}
}

View File

@@ -0,0 +1,128 @@
// file: crates/ksp-app-store-desk/src/app_state.rs
// version: 1
//! Shared backend state owned by the Store Desk Tauri scaffold.
/// Shared Store Desk application state managed by Tauri.
pub(crate) struct AppState {
config_management: ksp_config_lib::ConfigManagement,
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
splash_settings: crate::SplashSettings,
splash_sequence_started: std::sync::atomic::AtomicBool,
}
impl crate::AppState {
/// Initializes Config ownership, transient standard Logging and common splash settings.
pub(crate) fn initialize(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<Self> {
let config_management = crate::config_management(arguments);
let config_management = match config_management {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let runtime_identity = crate::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),
};
let splash_settings = crate::SplashSettings::load();
let splash_settings = match splash_settings {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
error_domain = error.code().domain(),
error_code = error.code().code(),
"managed splash timings are invalid; using transient in-memory defaults"
);
crate::SplashSettings::fallback()
},
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
minimum_ms = splash_settings.minimum_ms(),
minimum_source = splash_settings.minimum_source(),
fade_in_ms = splash_settings.fade_in_ms(),
fade_in_source = splash_settings.fade_in_source(),
fade_out_ms = splash_settings.fade_out_ms(),
fade_out_source = splash_settings.fade_out_source(),
expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(),
"resolved Store Desk splash timings"
);
return std::result::Result::Ok(Self {
config_management,
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
guard: logging_startup.guard,
active_profile_id: logging_startup.active_profile_id,
fallback_active: logging_startup.fallback_active,
startup_diagnostic: logging_startup.startup_diagnostic,
}),
splash_settings,
splash_sequence_started: std::sync::atomic::AtomicBool::new(false),
});
}
/// Builds the safe runtime status exposed by the Store Desk scaffold shell.
pub(crate) fn shell_status(&self) -> ksp_core_lib::Result<crate::ShellStatusDto> {
let document_count = self.config_management.engine().registry().descriptors().count();
let document_count = u32::try_from(document_count);
let document_count = match document_count {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Config registry contains too many descriptors for the Store Desk runtime DTO",
)
.with_source(error),
);
},
};
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,
"Store Desk Logging runtime state lock is poisoned",
));
},
};
let _keep_guard_alive = &runtime.guard;
return std::result::Result::Ok(crate::ShellStatusDto {
active_logging_profile: runtime.active_profile_id.clone(),
application_version: env!("CARGO_PKG_VERSION").to_owned(),
config_document_count: document_count,
fallback_logging_active: runtime.fallback_active,
shell_phase: "pre.002-scaffold".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
});
}
/// Returns the resolved splash timings captured during application bootstrap.
#[must_use]
pub(crate) const fn splash_settings(&self) -> crate::SplashSettings {
return self.splash_settings;
}
/// Marks the one-shot splash lifecycle as started and reports whether this caller won the transition.
pub(crate) fn begin_splash_sequence(&self) -> bool {
return self
.splash_sequence_started
.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire)
.is_ok();
}
}
struct LoggingRuntimeState {
guard: ksp_logging_lib::LoggingGuard,
active_profile_id: std::option::Option<String>,
fallback_active: bool,
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}

View File

@@ -0,0 +1,107 @@
// file: crates/ksp-app-store-desk/src/bootstrap.rs
// version: 1
//! Transient standard Config and Logging bootstrap for Store Desk scaffold.
/// Crate-internal Logging startup state shared by the application state.
pub(crate) struct LoggingStartup {
/// Guard that keeps the installed Logging runtime alive.
pub(crate) guard: ksp_logging_lib::LoggingGuard,
/// Active managed profile when Config resolved one successfully.
pub(crate) active_profile_id: std::option::Option<String>,
/// Whether a transient in-memory fallback was installed.
pub(crate) fallback_active: bool,
/// Safe startup diagnostic retained for the shell.
pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
}
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<ksp_config_lib::ConfigManagement> {
let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments);
let bootstrap = match bootstrap {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let registry = ksp_config_lib::ConfigFileRegistry::from_args(arguments);
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry);
return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine));
}
/// Initializes Logging from the standard Config document, with a trace-level in-memory fallback.
pub(crate) fn initialize_logging(
management: &ksp_config_lib::ConfigManagement,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<crate::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, 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, runtime_identity),
};
let active_profile_id = resolved.profile_id().to_owned();
let settings = resolved.into_settings();
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
return match guard {
std::result::Result::Ok(guard) => {
ksp_logging_lib::info!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_BOOTSTRAP,
active_profile = active_profile_id.as_str(),
"initialized Store Desk scaffold logging from standard Config"
);
std::result::Result::Ok(crate::LoggingStartup {
guard,
active_profile_id: std::option::Option::Some(active_profile_id),
fallback_active: false,
startup_diagnostic: std::option::Option::None,
})
},
std::result::Result::Err(error) => initialize_fallback_logging(error, runtime_identity),
};
}
fn initialize_fallback_logging(
initial_error: ksp_core_lib::Error,
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
) -> ksp_core_lib::Result<crate::LoggingStartup> {
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
let settings = ksp_logging_lib::LoggingSettings::new(
ksp_logging_lib::LogFilterLevel::Trace,
ksp_logging_lib::SpanEvents::Off,
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
std::vec::Vec::new(),
);
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) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED, "Cannot initialize Store Desk fallback Logging runtime")
.with_context("initial_error_domain", initial_error.code().domain())
.with_context("initial_error_code", initial_error.code().code())
.with_source(fallback_error),
);
},
};
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_BOOTSTRAP,
error_domain = diagnostic.domain.as_str(),
error_code = diagnostic.code.as_str(),
"Store Desk scaffold uses transient fallback Logging"
);
return std::result::Result::Ok(crate::LoggingStartup {
guard,
active_profile_id: std::option::Option::None,
fallback_active: true,
startup_diagnostic: std::option::Option::Some(diagnostic),
});
}

View File

@@ -0,0 +1,21 @@
// file: crates/ksp-app-store-desk/src/constants.rs
// version: 1
//! Application-owned tracing targets and domains.
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "store.bootstrap";
/// Structured domain used by technical frontend events.
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used by the Store Desk shell.
pub(crate) const TRACING_DOMAIN_SHELL: &str = "store.shell";
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) const TRACING_DOMAIN_WINDOWS: &str = "desktop.window";
/// Owning target for backend events emitted by Store Desk.
pub(crate) const TRACING_TARGET: &str = "ksp-app-store-desk";
/// Owning target for generic frontend events emitted through the KSP bridge.
pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-store-desk.frontend";
/// Owning target for main-window frontend events.
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-store-desk.frontend.main";
/// Owning target for splash-window frontend events.
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-store-desk.frontend.splash";

View File

@@ -0,0 +1,54 @@
// file: crates/ksp-app-store-desk/src/dto_common.rs
// version: 1
//! Common Tauri DTOs shared by the Store Desk scaffold shell.
use ts_rs::TS; // rust-rules: trait-import
/// Safe command error projection that never serializes arbitrary KSP error context or source values.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_common/CommandErrorDto.ts")]
pub(crate) struct CommandErrorDto {
/// Stable KSP error domain.
pub(crate) domain: String,
/// Stable KSP error code within the domain.
pub(crate) code: String,
/// Human-readable error message without arbitrary context fields.
pub(crate) message: String,
}
impl crate::CommandErrorDto {
/// Builds a bounded safe projection from a KSP error.
#[must_use]
pub(crate) fn from_error(error: &ksp_core_lib::Error) -> Self {
return Self {
domain: error.code().domain().to_owned(),
code: error.code().code().to_owned(),
message: error.message().to_owned(),
};
}
}
/// Safe scaffold/runtime snapshot exposed to the Store Desk shell.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/dto_common/ShellStatusDto.ts")]
pub(crate) struct ShellStatusDto {
/// Active configured Logging profile, or `None` while transient fallback Logging is active.
pub(crate) active_logging_profile: std::option::Option<String>,
/// Cargo application version.
pub(crate) application_version: String,
/// Number of logical Config resources registered by the current runtime.
pub(crate) config_document_count: u32,
/// Whether Store Desk had to install its transient in-memory Logging fallback.
pub(crate) fallback_logging_active: bool,
/// Current implementation phase exposed by the shell.
pub(crate) shell_phase: String,
/// Safe startup diagnostic that caused fallback Logging, when applicable.
pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>,
}
#[cfg(test)]
#[path = "../unit_tests/dto_common.rs"]
mod tests;

View File

@@ -0,0 +1,26 @@
// file: crates/ksp-app-store-desk/src/errors.rs
// version: 1
//! Application-local error codes for Store Desk scaffold and desktop runtime surfaces.
/// Shared Store Desk runtime state is internally inconsistent.
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "app_state_invalid");
/// Shared Store Desk runtime state cannot be locked safely.
pub(crate) const ERROR_CODE_APP_STATE_LOCK_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "app_state_lock_failed");
/// Frontend logging requested an unsupported level.
pub(crate) const ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "frontend_log_level_invalid");
/// Frontend logging requested a target outside the application whitelist.
pub(crate) const ERROR_CODE_FRONTEND_LOG_TARGET_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "frontend_log_target_invalid");
/// Store Desk could not install the managed Logging runtime or its safe fallback.
pub(crate) const ERROR_CODE_LOGGING_BOOTSTRAP_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "logging_bootstrap_failed");
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) const ERROR_CODE_SPLASH_ORIGIN_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "splash_origin_invalid");
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) const ERROR_CODE_SPLASH_SETTING_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "splash_setting_invalid");
/// Tauri runtime assembly or execution failed.
pub(crate) const ERROR_CODE_TAURI_RUNTIME_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "tauri_runtime_failed");
/// A required Tauri window is missing from the configured application runtime.
pub(crate) const ERROR_CODE_TAURI_WINDOW_MISSING: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("store_desk", "tauri_window_missing");
/// 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("store_desk", "tauri_window_operation_failed");

View File

@@ -0,0 +1,205 @@
// file: crates/ksp-app-store-desk/src/frontend_logging.rs
// version: 1
//! KSP-owned bridge for technical log events emitted by Store Desk frontend scripts.
use ts_rs::TS; // rust-rules: trait-import
/// Log payload sent by Store Desk frontend scripts.
#[derive(Clone, Debug, serde::Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/frontend_logging/FrontendLogPayloadDto.ts")]
pub(crate) struct FrontendLogPayloadDto {
/// Lowercase KSP log level.
pub(crate) level: String,
/// Whitelisted logical frontend target identifier.
pub(crate) target_id: String,
/// Rendered technical frontend message.
pub(crate) message: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FrontendLogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FrontendLogTarget {
Frontend,
Main,
Splash,
}
/// Emits one validated frontend event through the KSP Logging facade.
pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_core_lib::Result<()> {
let level = parse_level(payload.level.as_str());
let level = match level {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let target = parse_target(payload.target_id.as_str());
let target = match target {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
emit_validated_frontend_log(level, target, payload.message.as_str());
return std::result::Result::Ok(());
}
fn parse_level(level: &str) -> ksp_core_lib::Result<FrontendLogLevel> {
return match level.trim().to_ascii_lowercase().as_str() {
"trace" => std::result::Result::Ok(FrontendLogLevel::Trace),
"debug" => std::result::Result::Ok(FrontendLogLevel::Debug),
"info" => std::result::Result::Ok(FrontendLogLevel::Info),
"warn" => std::result::Result::Ok(FrontendLogLevel::Warn),
"error" => std::result::Result::Ok(FrontendLogLevel::Error),
_ => std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID, "Frontend log level is not supported")),
};
}
fn parse_target(target_id: &str) -> ksp_core_lib::Result<FrontendLogTarget> {
return match target_id.trim().to_ascii_lowercase().as_str() {
"frontend" => std::result::Result::Ok(FrontendLogTarget::Frontend),
"main" => std::result::Result::Ok(FrontendLogTarget::Main),
"splash" => std::result::Result::Ok(FrontendLogTarget::Splash),
_ => {
std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID, "Frontend log target identifier is not supported"))
},
};
}
fn emit_validated_frontend_log(level: FrontendLogLevel, target: FrontendLogTarget, message: &str) {
return match target {
FrontendLogTarget::Frontend => emit_frontend_target(level, message),
FrontendLogTarget::Main => emit_main_target(level, message),
FrontendLogTarget::Splash => emit_splash_target(level, message),
};
}
fn emit_frontend_target(level: FrontendLogLevel, message: &str) {
return match level {
FrontendLogLevel::Trace => ksp_logging_lib::trace!(
target: crate::TRACING_TARGET_FRONTEND,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "frontend",
"{message}"
),
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
target: crate::TRACING_TARGET_FRONTEND,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "frontend",
"{message}"
),
FrontendLogLevel::Info => ksp_logging_lib::info!(
target: crate::TRACING_TARGET_FRONTEND,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "frontend",
"{message}"
),
FrontendLogLevel::Warn => ksp_logging_lib::warn!(
target: crate::TRACING_TARGET_FRONTEND,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "frontend",
"{message}"
),
FrontendLogLevel::Error => ksp_logging_lib::error!(
target: crate::TRACING_TARGET_FRONTEND,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "frontend",
"{message}"
),
};
}
fn emit_main_target(level: FrontendLogLevel, message: &str) {
return match level {
FrontendLogLevel::Trace => ksp_logging_lib::trace!(
target: crate::TRACING_TARGET_FRONTEND_MAIN,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "main",
"{message}"
),
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
target: crate::TRACING_TARGET_FRONTEND_MAIN,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "main",
"{message}"
),
FrontendLogLevel::Info => ksp_logging_lib::info!(
target: crate::TRACING_TARGET_FRONTEND_MAIN,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "main",
"{message}"
),
FrontendLogLevel::Warn => ksp_logging_lib::warn!(
target: crate::TRACING_TARGET_FRONTEND_MAIN,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "main",
"{message}"
),
FrontendLogLevel::Error => ksp_logging_lib::error!(
target: crate::TRACING_TARGET_FRONTEND_MAIN,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "main",
"{message}"
),
};
}
fn emit_splash_target(level: FrontendLogLevel, message: &str) {
return match level {
FrontendLogLevel::Trace => ksp_logging_lib::trace!(
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "splash",
"{message}"
),
FrontendLogLevel::Debug => ksp_logging_lib::debug!(
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "splash",
"{message}"
),
FrontendLogLevel::Info => ksp_logging_lib::info!(
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "splash",
"{message}"
),
FrontendLogLevel::Warn => ksp_logging_lib::warn!(
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "splash",
"{message}"
),
FrontendLogLevel::Error => ksp_logging_lib::error!(
target: crate::TRACING_TARGET_FRONTEND_SPLASH,
domain = crate::TRACING_DOMAIN_FRONTEND,
action = "frontend_log",
target_id = "splash",
"{message}"
),
};
}
#[cfg(test)]
#[path = "../unit_tests/frontend_logging.rs"]
mod tests;

View File

@@ -0,0 +1,90 @@
// file: crates/ksp-app-store-desk/src/lib.rs
// version: 1
//! Tauri desktop scaffold for backend-neutral KSP Store inspection.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
mod app_state;
mod bootstrap;
mod constants;
mod dto_common;
mod errors;
mod frontend_logging;
mod logging_runtime;
mod splash;
mod tauri;
mod tw_main;
mod tw_splash;
/// Runs the KSP Store desktop application.
pub use self::tauri::run;
/// Shared Store Desk application state managed by Tauri.
pub(crate) use self::app_state::AppState;
/// Crate-internal Logging startup state shared by the application state.
pub(crate) use self::bootstrap::LoggingStartup;
/// Builds the Config management facade from the common KSP CLI bootstrap contract.
pub(crate) use self::bootstrap::config_management;
/// Initializes Logging from the standard Config document for the scaffold.
pub(crate) use self::bootstrap::initialize_logging;
/// Structured domain used while bootstrapping Config and Logging.
pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
/// Structured domain used by technical frontend events.
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
/// Structured domain used by the Store Desk shell.
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
/// Structured domain used by Tauri window lifecycle operations.
pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS;
/// Owning target for backend events emitted by Store Desk.
pub(crate) use self::constants::TRACING_TARGET;
/// Owning target for generic frontend events emitted through the KSP bridge.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND;
/// Owning target for main-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN;
/// Owning target for splash-window frontend events.
pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH;
/// Safe command error projection exposed to Tauri commands.
pub(crate) use self::dto_common::CommandErrorDto;
/// Safe scaffold/runtime snapshot exposed to the shell.
pub(crate) use self::dto_common::ShellStatusDto;
/// Shared Store Desk runtime state is internally inconsistent.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Store Desk runtime state cannot be locked safely.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
/// Frontend logging requested an unsupported level.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID;
/// Frontend logging requested a target outside the application whitelist.
pub(crate) use self::errors::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID;
/// Store Desk could not install the managed Logging runtime or its safe fallback.
pub(crate) use self::errors::ERROR_CODE_LOGGING_BOOTSTRAP_FAILED;
/// Splash readiness was invoked from a window other than the splash window.
pub(crate) use self::errors::ERROR_CODE_SPLASH_ORIGIN_INVALID;
/// A KSP desk splash environment duration is malformed or exceeds its safety bound.
pub(crate) use self::errors::ERROR_CODE_SPLASH_SETTING_INVALID;
/// Tauri runtime assembly or execution failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_RUNTIME_FAILED;
/// A required Tauri window is missing from the configured application runtime.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_MISSING;
/// A Tauri window show/focus/destroy/event operation failed.
pub(crate) use self::errors::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED;
/// Log payload sent by Store Desk frontend scripts.
pub(crate) use self::frontend_logging::FrontendLogPayloadDto;
/// Emits one validated frontend event through the KSP Logging facade.
pub(crate) use self::frontend_logging::emit_frontend_log_event;
/// Creates the stable runtime identity for this Store Desk process launch.
pub(crate) use self::logging_runtime::launch_identity;
/// Command emitted by Rust to the splash frontend.
pub(crate) use self::splash::SplashOrderDto;
/// Runtime timings used by the common desk splash lifecycle.
pub(crate) use self::splash::SplashSettings;
/// Resolves the required main window or returns a typed error.
pub(crate) use self::tw_main::require_main_window;
/// Shows and focuses the main Store Desk window.
pub(crate) use self::tw_main::show_main_window;
/// Resolves the required splash window or returns a typed error.
pub(crate) use self::tw_splash::require_splash_window;
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
pub(crate) use self::tw_splash::splash_frontend_ready_service;

View File

@@ -0,0 +1,10 @@
// file: crates/ksp-app-store-desk/src/logging_runtime.rs
// version: 1
//! Store Desk Logging runtime identity helpers.
/// Creates the stable runtime identity for this Store Desk process launch.
pub(crate) fn launch_identity() -> ksp_core_lib::Result<ksp_logging_lib::LoggingRuntimeIdentity> {
let timestamp = format!("{}-p{}", chrono::Utc::now().format("%Y%m%d-%H%M%S%.3fZ"), std::process::id());
return ksp_logging_lib::LoggingRuntimeIdentity::new(crate::TRACING_TARGET, timestamp);
}

View File

@@ -0,0 +1,58 @@
// file: crates/ksp-app-store-desk/src/main.rs
// version: 1
//! Binary entry point for the KSP Store desktop application.
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use fs2::FileExt; // rust-rules: trait-import
fn main() -> std::process::ExitCode {
let working_directory = configure_runtime_working_directory();
if let std::result::Result::Err(error) = working_directory {
eprintln!("cannot configure Store Desk runtime working directory: {error}");
return std::process::ExitCode::FAILURE;
}
let mut lock_path = std::env::temp_dir();
lock_path.push("com_sasedev_ksp_app_store_desk.lock");
let lock_file = match std::fs::OpenOptions::new().read(true).write(true).create(true).truncate(false).open(&lock_path) {
std::result::Result::Ok(file) => file,
std::result::Result::Err(error) => {
eprintln!("cannot create application lock '{}': {error}", lock_path.display());
return std::process::ExitCode::FAILURE;
},
};
if let std::result::Result::Err(error) = lock_file.try_lock_exclusive() {
if error.kind() == std::io::ErrorKind::WouldBlock {
eprintln!("another ksp-app-store-desk instance is already running");
return std::process::ExitCode::FAILURE;
}
eprintln!("cannot acquire application lock '{}': {error}", lock_path.display());
return std::process::ExitCode::FAILURE;
}
let _lock_file = lock_file;
let arguments = std::env::args_os().collect::<std::vec::Vec<std::ffi::OsString>>();
let run_result = ksp_app_store_desk_lib::run(arguments.as_slice());
return match run_result {
std::result::Result::Ok(()) => std::process::ExitCode::SUCCESS,
std::result::Result::Err(error) => {
eprintln!("application error: {error}");
std::process::ExitCode::FAILURE
},
};
}
fn configure_runtime_working_directory() -> std::io::Result<()> {
#[cfg(debug_assertions)]
{
let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
return std::env::set_current_dir(workspace_root);
}
#[cfg(not(debug_assertions))]
{
return std::result::Result::Ok(());
}
}

View File

@@ -0,0 +1,214 @@
// file: crates/ksp-app-store-desk/src/splash.rs
// version: 1
//! Common splash settings and frontend event contracts for Store Desk.
use ts_rs::TS; // rust-rules: trait-import
const DEFAULT_SPLASH_FADE_IN_MS: u32 = 300;
const DEFAULT_SPLASH_FADE_OUT_MS: u32 = 300;
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
const ENV_SPLASH_FADE_IN_MS: &str = "KSP_DESK_SPLASH_FADE_IN_MS";
const ENV_SPLASH_FADE_OUT_MS: &str = "KSP_DESK_SPLASH_FADE_OUT_MS";
const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS";
const MAX_SPLASH_FADE_MS: u32 = 10_000;
const MAX_SPLASH_MINIMUM_MS: u64 = 60_000;
/// Runtime timings used by the common desk splash lifecycle.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct SplashSettings {
fade_in_ms: u32,
minimum_ms: u64,
fade_out_ms: u32,
fade_in_source: ksp_config_lib::ConfigEnvironmentSource,
minimum_source: ksp_config_lib::ConfigEnvironmentSource,
fade_out_source: ksp_config_lib::ConfigEnvironmentSource,
}
impl SplashSettings {
/// Resolves splash timings through the Config-owned environment snapshot.
pub(crate) fn load() -> ksp_core_lib::Result<Self> {
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 fade_in = environment.resolve_variable(ENV_SPLASH_FADE_IN_MS, std::option::Option::Some("300"));
let fade_in = match fade_in {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let minimum = environment.resolve_variable(ENV_SPLASH_MINIMUM_MS, std::option::Option::Some("1200"));
let minimum = match minimum {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fade_out = environment.resolve_variable(ENV_SPLASH_FADE_OUT_MS, std::option::Option::Some("300"));
let fade_out = match fade_out {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fade_in_ms = parse_u32_setting(ENV_SPLASH_FADE_IN_MS, fade_in.value(), MAX_SPLASH_FADE_MS);
let fade_in_ms = match fade_in_ms {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let minimum_ms = parse_u64_setting(ENV_SPLASH_MINIMUM_MS, minimum.value(), MAX_SPLASH_MINIMUM_MS);
let minimum_ms = match minimum_ms {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fade_out_ms = parse_u32_setting(ENV_SPLASH_FADE_OUT_MS, fade_out.value(), MAX_SPLASH_FADE_MS);
let fade_out_ms = match fade_out_ms {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(Self {
fade_in_ms,
minimum_ms,
fade_out_ms,
fade_in_source: fade_in.source(),
minimum_source: minimum.source(),
fade_out_source: fade_out.source(),
});
}
/// Returns safe in-memory timings used when the managed environment cannot be resolved.
#[must_use]
pub(crate) const fn fallback() -> Self {
return Self {
fade_in_ms: DEFAULT_SPLASH_FADE_IN_MS,
minimum_ms: DEFAULT_SPLASH_MINIMUM_MS,
fade_out_ms: DEFAULT_SPLASH_FADE_OUT_MS,
fade_in_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
fade_out_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
};
}
/// Returns the configured fade-in duration.
#[must_use]
pub(crate) const fn fade_in_ms(self) -> u32 {
return self.fade_in_ms;
}
/// Returns the minimum fully-visible splash duration.
#[must_use]
pub(crate) const fn minimum_ms(self) -> u64 {
return self.minimum_ms;
}
/// Returns the configured fade-out duration.
#[must_use]
pub(crate) const fn fade_out_ms(self) -> u32 {
return self.fade_out_ms;
}
/// Returns the safe provenance code for the fade-in duration.
#[must_use]
pub(crate) const fn fade_in_source(self) -> &'static str {
return environment_source_code(self.fade_in_source);
}
/// Returns the safe provenance code for the minimum duration.
#[must_use]
pub(crate) const fn minimum_source(self) -> &'static str {
return environment_source_code(self.minimum_source);
}
/// Returns the safe provenance code for the fade-out duration.
#[must_use]
pub(crate) const fn fade_out_source(self) -> &'static str {
return environment_source_code(self.fade_out_source);
}
/// Returns the expected additive backend lifecycle from readiness through fade-out.
#[must_use]
pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 {
return u64::from(self.fade_in_ms) + self.minimum_ms + u64::from(self.fade_out_ms);
}
}
/// Command emitted by Rust to the splash frontend.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_store_desk/splash/SplashOrderDto.ts")]
pub(crate) struct SplashOrderDto {
/// Splash action name.
pub(crate) action: String,
/// Optional status text.
pub(crate) message: std::option::Option<String>,
/// Optional message severity used by the bottom message feed.
pub(crate) status: std::option::Option<String>,
/// Optional animation duration in milliseconds.
pub(crate) duration_ms: std::option::Option<u32>,
}
impl SplashOrderDto {
/// Creates a new `SplashOrderDto` value.
#[must_use]
pub(crate) fn new(action: &str, message: std::option::Option<&str>, status: std::option::Option<&str>, duration_ms: std::option::Option<u32>) -> Self {
return Self {
action: action.to_owned(),
message: message.map(std::string::ToString::to_string),
status: status.map(std::string::ToString::to_string),
duration_ms,
};
}
}
const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str {
return match source {
ksp_config_lib::ConfigEnvironmentSource::Process => "process",
ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv",
ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback",
};
}
fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result<u64> {
let parsed = value.parse::<u64>();
let parsed = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
.with_context("variable_name", variable_name)
.with_source(error),
);
},
};
if parsed > maximum {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
.with_context("variable_name", variable_name)
.with_context("maximum_ms", maximum.to_string()),
);
}
return std::result::Result::Ok(parsed);
}
fn parse_u32_setting(variable_name: &str, value: &str, maximum: u32) -> ksp_core_lib::Result<u32> {
let parsed = value.parse::<u32>();
let parsed = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration is not a valid unsigned integer")
.with_context("variable_name", variable_name)
.with_source(error),
);
},
};
if parsed > maximum {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_SETTING_INVALID, "KSP desk splash duration exceeds the allowed bound")
.with_context("variable_name", variable_name)
.with_context("maximum_ms", maximum.to_string()),
);
}
return std::result::Result::Ok(parsed);
}
#[cfg(test)]
#[path = "../unit_tests/splash.rs"]
mod tests;

View File

@@ -0,0 +1,100 @@
// file: crates/ksp-app-store-desk/src/tauri.rs
// version: 1
//! Tauri runtime assembly for the KSP Store desktop scaffold.
/// Runs the Store desktop application.
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
let context = tauri::generate_context!();
let app_state = crate::AppState::initialize(arguments);
let app_state = match app_state {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut builder = tauri::Builder::default();
builder = configure_state(builder, app_state);
builder = configure_plugins(builder);
builder = configure_commands(builder);
builder = configure_setup(builder);
let run_result = builder.run(context);
return match run_result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot run KSP Store Desk Tauri runtime")
.with_context("tauri_error", error.to_string()),
),
};
}
fn configure_state(builder: tauri::Builder<tauri::Wry>, app_state: crate::AppState) -> tauri::Builder<tauri::Wry> {
return builder.manage(app_state);
}
fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
let tracing_plugin = tauri_plugin_tracing::Builder::new().build::<tauri::Wry>();
return builder.plugin(tracing_plugin);
}
#[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![emit_frontend_log, get_shell_status, splash_frontend_ready]);
}
fn configure_setup(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.setup(|app| {
let splash = crate::require_splash_window(app);
if let std::result::Result::Err(error) = splash {
return std::result::Result::Err(std::boxed::Box::new(error));
}
let main = crate::require_main_window(app);
if let std::result::Result::Err(error) = main {
return std::result::Result::Err(std::boxed::Box::new(error));
}
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_SHELL, "Store Desk scaffold Tauri shell setup completed");
return std::result::Result::Ok(());
});
}
fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = domain,
command = command,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Store Desk Tauri command failed"
);
return crate::CommandErrorDto::from_error(error);
}
#[tauri::command]
fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::emit_frontend_log_event(payload);
return match result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("emit_frontend_log", crate::TRACING_DOMAIN_FRONTEND, &error)),
};
}
#[tauri::command]
fn get_shell_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::ShellStatusDto, crate::CommandErrorDto> {
let result = state.shell_status();
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_shell_status", crate::TRACING_DOMAIN_SHELL, &error)),
};
}
#[tauri::command]
async fn splash_frontend_ready(
app: tauri::AppHandle,
webview_window: tauri::WebviewWindow,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<(), crate::CommandErrorDto> {
let result = crate::splash_frontend_ready_service(app, webview_window, &state).await;
return match result {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(project_command_error("splash_frontend_ready", crate::TRACING_DOMAIN_WINDOWS, &error)),
};
}

View File

@@ -0,0 +1,53 @@
// file: crates/ksp-app-store-desk/src/tw_main.rs
// version: 1
//! Tauri-window helpers for the Store Desk main window.
use tauri::Manager; // rust-rules: trait-import
/// Crate-internal `WINDOW_LABEL_MAIN` constant.
pub(crate) const WINDOW_LABEL_MAIN: &str = "main";
/// Resolves the required main window or returns a typed error.
pub(crate) fn require_main_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
let window = manager.get_webview_window(WINDOW_LABEL_MAIN);
return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Store Desk main window is missing")
.with_context("window_label", WINDOW_LABEL_MAIN),
),
};
}
/// Shows main window.
pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> {
let window = crate::require_main_window(app);
let window = match window {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let shown = window.show();
if let std::result::Result::Err(error) = shown {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot show Store Desk main window")
.with_context("window_label", WINDOW_LABEL_MAIN)
.with_source(error),
);
}
let focused = window.set_focus();
if let std::result::Result::Err(error) = focused {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot focus Store Desk main window")
.with_context("window_label", WINDOW_LABEL_MAIN)
.with_source(error),
);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = WINDOW_LABEL_MAIN,
"main window shown and focused"
);
return std::result::Result::Ok(());
}

View File

@@ -0,0 +1,173 @@
// file: crates/ksp-app-store-desk/src/tw_splash.rs
// version: 1
//! Tauri-window lifecycle for the Store Desk splash window.
use tauri::Emitter; // rust-rules: trait-import
use tauri::Manager; // rust-rules: trait-import
/// Crate-internal splash window label.
pub(crate) const WINDOW_LABEL_SPLASH: &str = "splash";
const SPLASH_EVENT_NAME: &str = "ksp-splash-order";
/// Resolves the required splash window or returns a typed error.
pub(crate) fn require_splash_window(manager: &impl Manager<tauri::Wry>) -> ksp_core_lib::Result<tauri::WebviewWindow> {
let window = manager.get_webview_window(WINDOW_LABEL_SPLASH);
return match window {
std::option::Option::Some(value) => std::result::Result::Ok(value),
std::option::Option::None => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_MISSING, "Store Desk splash window is missing")
.with_context("window_label", WINDOW_LABEL_SPLASH),
),
};
}
/// Starts the one-shot splash lifecycle after the splash frontend reports readiness.
pub(crate) async fn splash_frontend_ready_service(
app: tauri::AppHandle,
invoking_window: tauri::WebviewWindow,
state: &crate::AppState,
) -> ksp_core_lib::Result<()> {
if invoking_window.label() != WINDOW_LABEL_SPLASH {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window")
.with_context("window_label", invoking_window.label()),
);
}
if !state.begin_splash_sequence() {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, "duplicate splash frontend readiness ignored");
return std::result::Result::Ok(());
}
let settings = state.splash_settings();
let lifecycle_started = std::time::Instant::now();
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = WINDOW_LABEL_SPLASH,
fade_in_ms = settings.fade_in_ms(),
fade_in_source = settings.fade_in_source(),
minimum_ms = settings.minimum_ms(),
minimum_source = settings.minimum_source(),
fade_out_ms = settings.fade_out_ms(),
fade_out_source = settings.fade_out_source(),
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
"splash frontend readiness accepted; starting splash to main lifecycle"
);
let emitted = emit_order(
&invoking_window,
crate::SplashOrderDto::new("fade_in", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_in_ms())),
);
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_message(&invoking_window, "Initialisation de Store Desk...", "info");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_debug(&invoking_window, "Listener frontend prêt; fade-in piloté par Rust.");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
sleep_and_log(&invoking_window, "fade-in", u64::from(settings.fade_in_ms()), lifecycle_started).await;
let emitted = emit_message(&invoking_window, "Configuration KSP chargée.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_debug(&invoking_window, "Configuration et provenance des timings résolues via ksp-config-lib.");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_message(&invoking_window, "Runtime Logging initialisé.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_message(&invoking_window, "Scaffold desktop prêt.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
sleep_and_log(&invoking_window, "minimum", settings.minimum_ms(), lifecycle_started).await;
let emitted = emit_message(&invoking_window, "Initialisation terminée.", "success");
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
let emitted = emit_order(
&invoking_window,
crate::SplashOrderDto::new("fade_out", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_out_ms())),
);
if let std::result::Result::Err(error) = emitted {
return std::result::Result::Err(error);
}
sleep_and_log(&invoking_window, "fade-out", u64::from(settings.fade_out_ms()), lifecycle_started).await;
let show_main = crate::show_main_window(&app);
if let std::result::Result::Err(error) = show_main {
return std::result::Result::Err(error);
}
let destroyed = invoking_window.destroy();
if let std::result::Result::Err(error) = destroyed {
return std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot destroy Store Desk splash window")
.with_context("window_label", WINDOW_LABEL_SPLASH)
.with_source(error),
);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = WINDOW_LABEL_SPLASH,
expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(),
actual_backend_lifecycle_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
"splash window destroyed after main activation"
);
return std::result::Result::Ok(());
}
fn emit_message(window: &tauri::WebviewWindow, message: &str, status: &str) -> ksp_core_lib::Result<()> {
return emit_order(
window,
crate::SplashOrderDto::new("add_message", std::option::Option::Some(message), std::option::Option::Some(status), std::option::Option::None),
);
}
fn emit_debug(window: &tauri::WebviewWindow, message: &str) -> ksp_core_lib::Result<()> {
if !cfg!(debug_assertions) {
return std::result::Result::Ok(());
}
return emit_order(
window,
crate::SplashOrderDto::new("add_debug", std::option::Option::Some(message), std::option::Option::None, std::option::Option::None),
);
}
async fn sleep_and_log(window: &tauri::WebviewWindow, phase: &str, duration_ms: u64, lifecycle_started: std::time::Instant) {
let wait_started = std::time::Instant::now();
tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_WINDOWS,
window_label = window.label(),
phase,
configured_wait_ms = duration_ms,
actual_wait_ms = wait_started.elapsed().as_secs_f64() * 1000.0,
lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0,
"splash lifecycle wait completed"
);
return;
}
fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> {
let action = order.action.clone();
let emitted = window.emit(SPLASH_EVENT_NAME, order);
return match emitted {
std::result::Result::Ok(()) => {
ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, action = action.as_str(), "splash order emitted");
std::result::Result::Ok(())
},
std::result::Result::Err(error) => std::result::Result::Err(
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot emit Store Desk splash order")
.with_context("window_label", WINDOW_LABEL_SPLASH)
.with_context("action", action)
.with_source(error),
),
};
}

View File

@@ -0,0 +1,55 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "KSP Store Desk",
"identifier": "com.sasedev.ksp-app-store-desk",
"build": {
"beforeDevCommand": {
"script": "npm run dev",
"cwd": "."
},
"devUrl": "http://localhost:1438",
"beforeBuildCommand": {
"script": "npm run build",
"cwd": "."
},
"frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-store-desk/dist"
},
"app": {
"windows": [
{
"label": "splash",
"url": "splash.html",
"title": "Chargement — KSP Store Desk",
"width": 960,
"height": 637,
"resizable": false,
"decorations": false,
"transparent": true,
"center": true,
"alwaysOnTop": true
},
{
"label": "main",
"url": "main.html",
"title": "KSP Store Desk",
"width": 1500,
"height": 960,
"minWidth": 1024,
"minHeight": 768,
"center": true,
"visible": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/favicon.png",
"icons/favicon.ico"
]
}
}

View File

@@ -0,0 +1,33 @@
// file: crates/ksp-app-store-desk/tests/dependency_boundary.rs
// version: 1
#[test]
fn pre_002_manifest_keeps_store_and_transport_runtime_dependencies_out_of_scaffold() {
let manifest = include_str!("../Cargo.toml");
for forbidden in [
"ksp-store-lib",
"ksp-store-api",
"ksp-store-postgres-lib",
"ksp-onchain-transport-lib",
"ksp-offchain-transport-lib",
"ksp-job-api",
"ksp-job-backfill-lib",
"reqwest",
"tokio-postgres",
"deadpool-postgres",
] {
assert!(!manifest.contains(forbidden), "forbidden pre.002 dependency present: {forbidden}");
}
for required in ["ksp-config-lib", "ksp-core-lib", "ksp-logging-lib", "tauri-plugin-tracing"] {
assert!(manifest.contains(required), "required scaffold dependency missing: {required}");
}
}
#[test]
fn pre_002_sources_expose_no_sql_or_physical_store_escape_hatch() {
let sources =
[include_str!("../src/app_state.rs"), include_str!("../src/bootstrap.rs"), include_str!("../src/tauri.rs"), include_str!("../src/lib.rs")].join("\n");
for forbidden in ["Store::open", "tokio_postgres", "deadpool_postgres", "SELECT ", "INSERT ", "UPDATE ", "DELETE ", "ksp_store_api", "ksp_store_postgres"] {
assert!(!sources.contains(forbidden), "pre.002 scaffold leaked forbidden Store/runtime surface: {forbidden}");
}
}

View File

@@ -0,0 +1,92 @@
// file: crates/ksp-app-store-desk/tests/desktop_contract.rs
// version: 1
fn parse_json(source: &str) -> serde_json::Result<serde_json::Value> {
serde_json::from_str::<serde_json::Value>(source)
}
#[test]
fn pre_002_tauri_package_and_ports_match_store_desk_identity() {
let parsed = parse_json(include_str!("../tauri.conf.json"));
assert!(parsed.is_ok(), "committed Tauri JSON must be valid");
let config = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(config["productName"], "KSP Store Desk");
assert_eq!(config["identifier"], "com.sasedev.ksp-app-store-desk");
assert_eq!(config["build"]["devUrl"], "http://localhost:1438");
assert!(config.get("version").is_none());
let vite = include_str!("../vite.config.ts");
assert!(vite.contains("port: 1438"));
assert!(vite.contains("strictPort: true"));
assert!(vite.contains("port: 1439"));
}
#[test]
fn pre_002_frontend_dependency_baseline_is_ksp_owned_and_select_free() {
let parsed = parse_json(include_str!("../package.json"));
assert!(parsed.is_ok(), "committed package JSON must be valid");
let package = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let dependencies = &package["dependencies"];
for required in [
"@fltsci/tauri-plugin-tracing",
"@fortawesome/fontawesome-free",
"@tauri-apps/api",
"bootstrap",
"datatables.net-bs5",
"resize-observer-polyfill",
"simplebar",
] {
assert!(dependencies.get(required).is_some(), "required frontend dependency missing: {required}");
}
assert!(dependencies.get("datatables.net-select-bs5").is_none());
let dev_dependencies = &package["devDependencies"];
let expected = ["@tauri-apps/cli", "@types/bootstrap", "@types/node", "sass-embedded", "typescript", "vite"];
let count = match dev_dependencies.as_object() {
std::option::Option::Some(value) => value.len(),
std::option::Option::None => 0,
};
assert_eq!(count, expected.len());
for dependency in expected {
assert!(dev_dependencies.get(dependency).is_some(), "required dev dependency missing: {dependency}");
}
}
#[test]
fn pre_002_binary_assets_reuse_the_ksp_desktop_template_byte_exact() {
assert_eq!(include_bytes!("../frontend/imgs/logo.png"), include_bytes!("../../ksp-app-backfill-desk/frontend/imgs/logo.png"));
assert_eq!(include_bytes!("../frontend/imgs/splash.png"), include_bytes!("../../ksp-app-backfill-desk/frontend/imgs/splash.png"));
assert_eq!(include_bytes!("../frontend/fonts/DOS_Amazigh.ttf"), include_bytes!("../../ksp-app-backfill-desk/frontend/fonts/DOS_Amazigh.ttf"));
assert_eq!(include_bytes!("../icons/favicon.png"), include_bytes!("../../ksp-app-backfill-desk/icons/favicon.png"));
assert_eq!(include_bytes!("../icons/favicon.ico"), include_bytes!("../../ksp-app-backfill-desk/icons/favicon.ico"));
}
#[test]
fn pre_002_shell_exposes_only_planned_store_desk_views_and_empty_table_skeletons() {
let html = include_str!("../frontend/main.html");
for view in ["overview", "transactions", "accounts", "diagnostics"] {
assert!(html.contains(&format!("data-view=\"{view}\"")));
assert!(html.contains(&format!("data-view-panel=\"{view}\"")));
}
assert!(html.contains("id=\"rawTransactionsTable\""));
assert!(html.contains("id=\"rawAccountsTable\""));
assert!(html.contains("Aucune capability Store n'est encore invoquée"));
}
#[test]
fn pre_002_datatables_are_scaffold_only_with_one_pager_owner_and_no_store_pager() {
let source = include_str!("../frontend/ts/main.ts");
assert!(source.contains("datatables.net-bs5"));
assert!(source.contains("paging: true"));
assert!(source.contains("pageLength: 25"));
assert!(source.contains("lengthMenu: [25, 50, 100]"));
assert!(source.contains("serverSide: false"));
let html = include_str!("../frontend/main.html");
for forbidden in ["Premier", "Précédent", "Suivant", "storePager", "store-pagination"] {
assert!(!html.contains(forbidden), "second Store pager marker present: {forbidden}");
}
}

View File

@@ -0,0 +1,51 @@
// file: crates/ksp-app-store-desk/tests/desktop_security.rs
// version: 1
fn parse_json(source: &str) -> serde_json::Result<serde_json::Value> {
serde_json::from_str::<serde_json::Value>(source)
}
#[test]
fn pre_002_capabilities_are_core_and_tracing_only() {
let parsed = parse_json(include_str!("../capabilities/default.json"));
assert!(parsed.is_ok(), "committed capability JSON must be valid");
let capability = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let permissions = capability["permissions"].as_array();
assert!(permissions.is_some(), "capability permissions must be an array");
let permissions = match permissions {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert_eq!(permissions.len(), 2);
assert_eq!(permissions[0], "core:default");
assert_eq!(permissions[1], "tracing:default");
}
#[test]
fn pre_002_frontend_avoids_network_browser_storage_and_blocking_dialogs() {
let frontend = [
include_str!("../frontend/ts/frontend_log.ts"),
include_str!("../frontend/ts/invoke.ts"),
include_str!("../frontend/ts/main.ts"),
include_str!("../frontend/ts/splash.ts"),
]
.join("\n");
for forbidden in ["fetch(", "XMLHttpRequest", "localStorage", "sessionStorage", "window.alert", "window.confirm", "window.prompt"] {
assert!(!frontend.contains(forbidden), "forbidden frontend primitive present: {forbidden}");
}
}
#[test]
fn pre_002_frontend_tracing_covers_navigation_buttons_refresh_and_ipc() {
let main = include_str!("../frontend/ts/main.ts");
let invoke = include_str!("../frontend/ts/invoke.ts");
assert!(main.contains("navigation button clicked"));
assert!(main.contains("diagnostics refresh button clicked"));
assert!(main.contains("view activated"));
assert!(invoke.contains("IPC command requested"));
assert!(invoke.contains("IPC command completed"));
assert!(invoke.contains("IPC command failed"));
}

View File

@@ -0,0 +1,8 @@
// file: crates/ksp-app-store-desk/tests/public_api.rs
// version: 1
#[test]
fn pre_002_public_surface_exposes_only_application_run_entry_point() {
let run: fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()> = ksp_app_store_desk_lib::run;
let _ = run;
}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"allowJs": false,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["frontend/ts/**/*.ts", "vite.config.ts"]
}

View File

@@ -0,0 +1,13 @@
// file: crates/ksp-app-store-desk/unit_tests/dto_common.rs
// version: 1
#[test]
fn command_error_projection_keeps_only_stable_public_fields() {
let source = ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "safe message").with_context("secret_context", "must-not-cross-ipc");
let projected = crate::CommandErrorDto::from_error(&source);
assert_eq!(projected.domain, "store_desk");
assert_eq!(projected.code, "app_state_invalid");
assert_eq!(projected.message, "safe message");
let debug = format!("{projected:?}");
assert!(!debug.contains("must-not-cross-ipc"));
}

View File

@@ -0,0 +1,40 @@
// file: crates/ksp-app-store-desk/unit_tests/frontend_logging.rs
// version: 1
#[test]
fn frontend_log_levels_are_explicit_and_case_insensitive() {
assert!(matches!(super::parse_level(" TRACE "), std::result::Result::Ok(super::FrontendLogLevel::Trace)));
assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(super::FrontendLogLevel::Debug)));
assert!(matches!(super::parse_level("Info"), std::result::Result::Ok(super::FrontendLogLevel::Info)));
assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(super::FrontendLogLevel::Warn)));
assert!(matches!(super::parse_level("ERROR"), std::result::Result::Ok(super::FrontendLogLevel::Error)));
}
#[test]
fn unsupported_frontend_log_level_is_rejected() {
let result = super::parse_level("notice");
assert!(result.is_err());
let error = match result {
std::result::Result::Ok(_) => return,
std::result::Result::Err(error) => error,
};
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID);
}
#[test]
fn frontend_log_targets_are_whitelisted() {
assert!(matches!(super::parse_target("frontend"), std::result::Result::Ok(super::FrontendLogTarget::Frontend)));
assert!(matches!(super::parse_target(" MAIN "), std::result::Result::Ok(super::FrontendLogTarget::Main)));
assert!(matches!(super::parse_target("Splash"), std::result::Result::Ok(super::FrontendLogTarget::Splash)));
}
#[test]
fn arbitrary_frontend_log_target_is_rejected() {
let result = super::parse_target("ksp-app-store-desk.test.logging");
assert!(result.is_err());
let error = match result {
std::result::Result::Ok(_) => return,
std::result::Result::Err(error) => error,
};
assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID);
}

View File

@@ -0,0 +1,36 @@
// file: crates/ksp-app-store-desk/unit_tests/splash.rs
// version: 1
#[test]
fn fallback_splash_settings_are_short_and_ordered() {
let settings = crate::SplashSettings::fallback();
assert_eq!(settings.fade_in_ms(), 300);
assert_eq!(settings.minimum_ms(), 1200);
assert_eq!(settings.fade_out_ms(), 300);
assert_eq!(settings.expected_backend_lifecycle_ms(), 1800);
assert_eq!(settings.fade_in_source(), "fallback");
assert_eq!(settings.minimum_source(), "fallback");
assert_eq!(settings.fade_out_source(), "fallback");
}
#[test]
fn splash_order_contract_keeps_action_message_status_and_duration() {
let order =
crate::SplashOrderDto::new("add_message", std::option::Option::Some("ready"), std::option::Option::Some("success"), std::option::Option::Some(300));
assert_eq!(order.action, "add_message");
assert_eq!(order.message.as_deref(), std::option::Option::Some("ready"));
assert_eq!(order.status.as_deref(), std::option::Option::Some("success"));
assert_eq!(order.duration_ms, std::option::Option::Some(300));
}
#[test]
fn splash_duration_parser_rejects_invalid_or_unbounded_values() {
let malformed = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "invalid", 60_000);
let oversized_minimum = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "60001", 60_000);
let oversized_fade_in = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_IN_MS", "10001", 10_000);
let oversized_fade_out = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_OUT_MS", "10001", 10_000);
assert!(malformed.is_err());
assert!(oversized_minimum.is_err());
assert!(oversized_fade_in.is_err());
assert!(oversized_fade_out.is_err());
}

View File

@@ -0,0 +1,75 @@
// file: crates/ksp-app-store-desk/vite.config.ts
// version: 1
import { NodePackageImporter } from "sass-embedded";
import { fileURLToPath } from "node:url";
import { resolve } from "node:path";
import { defineConfig, normalizePath } from "vite";
const appRoot = fileURLToPath(new URL(".", import.meta.url));
const frontendRoot = normalizePath(resolve(appRoot, "frontend"));
const frontendDist = normalizePath(resolve(appRoot, "../../../builds/khadhroony-solana-project/ksp-app-store-desk/dist"));
const devHost = process.env.TAURI_DEV_HOST;
export default defineConfig({
clearScreen: false,
root: frontendRoot,
publicDir: false,
input: {
main: normalizePath(resolve(frontendRoot, "main.html")),
splash: normalizePath(resolve(frontendRoot, "splash.html")),
},
build: {
outDir: frontendDist,
emptyOutDir: true,
minify: true,
sourcemap: false,
cssCodeSplit: true,
rolldownOptions: {
output: {
entryFileNames: "js/[name]-[hash].js",
chunkFileNames: "js/chunks/[name]-[hash].js",
assetFileNames: assetInfo => {
const originalName = assetInfo.names[0] ?? "";
const extension = originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase();
if (extension === "css") {
return "css/[name]-[hash][extname]";
}
if (["eot", "otf", "ttf", "woff", "woff2"].includes(extension)) {
return "fonts/[name]-[hash][extname]";
}
if (["png", "jpg", "jpeg", "gif", "svg", "webp", "ico"].includes(extension)) {
return "imgs/[name][extname]";
}
return "otherassets/[name][hash][extname]";
},
},
},
},
css: {
preprocessorOptions: {
scss: {
importers: [new NodePackageImporter()],
},
},
},
server: {
port: 1438,
strictPort: true,
host: devHost || false,
hmr: devHost
? {
protocol: "ws",
host: devHost,
port: 1439,
}
: {
protocol: "ws",
host: "localhost",
port: 1439,
},
watch: {
ignored: ["**/src/**"],
},
},
});