v0.3.8-pre.002
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 445
|
# version: 446
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.8-pre.1"
|
version = "0.3.8-pre.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
40
crates/ksp-app-store-desk/Cargo.toml
Normal file
40
crates/ksp-app-store-desk/Cargo.toml
Normal 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
|
||||||
12
crates/ksp-app-store-desk/build.rs
Normal file
12
crates/ksp-app-store-desk/build.rs
Normal 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()
|
||||||
|
}
|
||||||
13
crates/ksp-app-store-desk/capabilities/default.json
Normal file
13
crates/ksp-app-store-desk/capabilities/default.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
crates/ksp-app-store-desk/frontend/fonts/DOS_Amazigh.ttf
Normal file
BIN
crates/ksp-app-store-desk/frontend/fonts/DOS_Amazigh.ttf
Normal file
Binary file not shown.
25
crates/ksp-app-store-desk/frontend/fonts/README.md
Normal file
25
crates/ksp-app-store-desk/frontend/fonts/README.md
Normal 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`.
|
||||||
BIN
crates/ksp-app-store-desk/frontend/imgs/logo.png
Normal file
BIN
crates/ksp-app-store-desk/frontend/imgs/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
BIN
crates/ksp-app-store-desk/frontend/imgs/splash.png
Normal file
BIN
crates/ksp-app-store-desk/frontend/imgs/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 611 KiB |
137
crates/ksp-app-store-desk/frontend/main.html
Normal file
137
crates/ksp-app-store-desk/frontend/main.html
Normal 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>© 2026 SASEDEV</small>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script type="module" src="ts/main.ts" defer></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
92
crates/ksp-app-store-desk/frontend/sass/_app.scss
Normal file
92
crates/ksp-app-store-desk/frontend/sass/_app.scss
Normal 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;
|
||||||
|
}
|
||||||
160
crates/ksp-app-store-desk/frontend/sass/_bootswatch.scss
Normal file
160
crates/ksp-app-store-desk/frontend/sass/_bootswatch.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
crates/ksp-app-store-desk/frontend/sass/_fontawesome.scss
Normal file
19
crates/ksp-app-store-desk/frontend/sass/_fontawesome.scss
Normal 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;
|
||||||
248
crates/ksp-app-store-desk/frontend/sass/_simplebar.scss
Normal file
248
crates/ksp-app-store-desk/frontend/sass/_simplebar.scss
Normal 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;
|
||||||
|
}
|
||||||
95
crates/ksp-app-store-desk/frontend/sass/_variables.scss
Normal file
95
crates/ksp-app-store-desk/frontend/sass/_variables.scss
Normal 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;
|
||||||
10
crates/ksp-app-store-desk/frontend/sass/main.scss
Normal file
10
crates/ksp-app-store-desk/frontend/sass/main.scss
Normal 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";
|
||||||
137
crates/ksp-app-store-desk/frontend/sass/splash.scss
Normal file
137
crates/ksp-app-store-desk/frontend/sass/splash.scss
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
crates/ksp-app-store-desk/frontend/splash.html
Normal file
24
crates/ksp-app-store-desk/frontend/splash.html
Normal 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>
|
||||||
110
crates/ksp-app-store-desk/frontend/ts/frontend_log.ts
Normal file
110
crates/ksp-app-store-desk/frontend/ts/frontend_log.ts
Normal 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);
|
||||||
|
}
|
||||||
17
crates/ksp-app-store-desk/frontend/ts/invoke.ts
Normal file
17
crates/ksp-app-store-desk/frontend/ts/invoke.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
152
crates/ksp-app-store-desk/frontend/ts/main.ts
Normal file
152
crates/ksp-app-store-desk/frontend/ts/main.ts
Normal 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();
|
||||||
|
});
|
||||||
131
crates/ksp-app-store-desk/frontend/ts/splash.ts
Normal file
131
crates/ksp-app-store-desk/frontend/ts/splash.ts
Normal 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();
|
||||||
|
});
|
||||||
BIN
crates/ksp-app-store-desk/icons/favicon.ico
Normal file
BIN
crates/ksp-app-store-desk/icons/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 448 KiB |
BIN
crates/ksp-app-store-desk/icons/favicon.png
Normal file
BIN
crates/ksp-app-store-desk/icons/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
27
crates/ksp-app-store-desk/package.json
Normal file
27
crates/ksp-app-store-desk/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
128
crates/ksp-app-store-desk/src/app_state.rs
Normal file
128
crates/ksp-app-store-desk/src/app_state.rs
Normal 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>,
|
||||||
|
}
|
||||||
107
crates/ksp-app-store-desk/src/bootstrap.rs
Normal file
107
crates/ksp-app-store-desk/src/bootstrap.rs
Normal 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),
|
||||||
|
});
|
||||||
|
}
|
||||||
21
crates/ksp-app-store-desk/src/constants.rs
Normal file
21
crates/ksp-app-store-desk/src/constants.rs
Normal 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";
|
||||||
54
crates/ksp-app-store-desk/src/dto_common.rs
Normal file
54
crates/ksp-app-store-desk/src/dto_common.rs
Normal 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;
|
||||||
26
crates/ksp-app-store-desk/src/errors.rs
Normal file
26
crates/ksp-app-store-desk/src/errors.rs
Normal 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");
|
||||||
205
crates/ksp-app-store-desk/src/frontend_logging.rs
Normal file
205
crates/ksp-app-store-desk/src/frontend_logging.rs
Normal 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;
|
||||||
90
crates/ksp-app-store-desk/src/lib.rs
Normal file
90
crates/ksp-app-store-desk/src/lib.rs
Normal 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;
|
||||||
10
crates/ksp-app-store-desk/src/logging_runtime.rs
Normal file
10
crates/ksp-app-store-desk/src/logging_runtime.rs
Normal 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);
|
||||||
|
}
|
||||||
58
crates/ksp-app-store-desk/src/main.rs
Normal file
58
crates/ksp-app-store-desk/src/main.rs
Normal 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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
214
crates/ksp-app-store-desk/src/splash.rs
Normal file
214
crates/ksp-app-store-desk/src/splash.rs
Normal 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;
|
||||||
100
crates/ksp-app-store-desk/src/tauri.rs
Normal file
100
crates/ksp-app-store-desk/src/tauri.rs
Normal 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)),
|
||||||
|
};
|
||||||
|
}
|
||||||
53
crates/ksp-app-store-desk/src/tw_main.rs
Normal file
53
crates/ksp-app-store-desk/src/tw_main.rs
Normal 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(());
|
||||||
|
}
|
||||||
173
crates/ksp-app-store-desk/src/tw_splash.rs
Normal file
173
crates/ksp-app-store-desk/src/tw_splash.rs
Normal 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),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
55
crates/ksp-app-store-desk/tauri.conf.json
Normal file
55
crates/ksp-app-store-desk/tauri.conf.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
33
crates/ksp-app-store-desk/tests/dependency_boundary.rs
Normal file
33
crates/ksp-app-store-desk/tests/dependency_boundary.rs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
92
crates/ksp-app-store-desk/tests/desktop_contract.rs
Normal file
92
crates/ksp-app-store-desk/tests/desktop_contract.rs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
51
crates/ksp-app-store-desk/tests/desktop_security.rs
Normal file
51
crates/ksp-app-store-desk/tests/desktop_security.rs
Normal 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"));
|
||||||
|
}
|
||||||
8
crates/ksp-app-store-desk/tests/public_api.rs
Normal file
8
crates/ksp-app-store-desk/tests/public_api.rs
Normal 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;
|
||||||
|
}
|
||||||
19
crates/ksp-app-store-desk/tsconfig.json
Normal file
19
crates/ksp-app-store-desk/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
13
crates/ksp-app-store-desk/unit_tests/dto_common.rs
Normal file
13
crates/ksp-app-store-desk/unit_tests/dto_common.rs
Normal 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"));
|
||||||
|
}
|
||||||
40
crates/ksp-app-store-desk/unit_tests/frontend_logging.rs
Normal file
40
crates/ksp-app-store-desk/unit_tests/frontend_logging.rs
Normal 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);
|
||||||
|
}
|
||||||
36
crates/ksp-app-store-desk/unit_tests/splash.rs
Normal file
36
crates/ksp-app-store-desk/unit_tests/splash.rs
Normal 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());
|
||||||
|
}
|
||||||
75
crates/ksp-app-store-desk/vite.config.ts
Normal file
75
crates/ksp-app-store-desk/vite.config.ts
Normal 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/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
205
deltas/0.3.8/pre.002.md
Normal file
205
deltas/0.3.8/pre.002.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<!-- file: deltas/0.3.8/pre.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta v0.3.8-pre.2 — scaffold KSP Store Desk
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.3.8-pre.1
|
||||||
|
workspace.package.version = 0.3.8-pre.1
|
||||||
|
```
|
||||||
|
|
||||||
|
La base `pre.001` a été validée par l’opérateur avec `cargo fmt`, audits Rust/Markdown, `cargo check --workspace` et `cargo clippy --workspace --all-targets` verts avant ouverture de cette tranche.
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
Matérialiser uniquement le scaffold desktop `ksp-app-store-desk` défini par le plan `pre.001`, sans ouvrir Store ni anticiper les contrats d’inspection de `pre.003`.
|
||||||
|
|
||||||
|
Le scaffold fixe dès maintenant l’invariant UX de pagination :
|
||||||
|
|
||||||
|
```text
|
||||||
|
DataTables = unique owner visuel du pager
|
||||||
|
Store pager parallèle = absent
|
||||||
|
Store serverSide réel = différé à pre.007 / pre.008
|
||||||
|
```
|
||||||
|
|
||||||
|
Les DataTables de `pre.002` sont volontairement vides et locales ; elles valident le shell et le rendu, pas encore la sémantique métier d’inspection.
|
||||||
|
|
||||||
|
## Recalibrage documentaire issu du retour opérateur
|
||||||
|
|
||||||
|
Le sizing `pre.001` contenait un placeholder préventif `pre.003-fix.NNN`. Il est supprimé : conformément au workflow KSP, un `pre.NNN-fix.MMM` n’existe que lorsqu’un défaut réel de la tranche correspondante doit être corrigé. Aucun fix futur n’est réservé à l’avance.
|
||||||
|
|
||||||
|
La screen-map est également corrigée pour supprimer le résidu `Premier / Précédent / Suivant` Store. Les seuls contrôles de pagination visibles prévus pour les tables métier sont ceux de DataTables ; `Refresh` reste un contrôle applicatif indépendant.
|
||||||
|
|
||||||
|
Enfin, `ksp-core-lib` est admis au scaffold comme dépendance KSP pour les erreurs applicatives typées, conformément au pattern des Desks existants. Cette dépendance ne donne aucun accès Store et ne modifie pas la règle selon laquelle la future application métier ouvrira Store exclusivement via `ksp-store-lib`.
|
||||||
|
|
||||||
|
## Scaffold livré
|
||||||
|
|
||||||
|
`ksp-app-store-desk` est un package Rust mixte lib + bin avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ports Vite/HMR 1438/1439 stricts
|
||||||
|
fenêtres splash + main
|
||||||
|
single-instance crate-local
|
||||||
|
bootstrap Config standard
|
||||||
|
runtime Logging KSP + fallback sûr
|
||||||
|
tracing backend/frontend
|
||||||
|
assets/fonts/SASS KSP
|
||||||
|
capabilities core:default + tracing:default
|
||||||
|
Overview / RAW Transactions / RAW Accounts / Diagnostics
|
||||||
|
deux skeletons DataTables vides
|
||||||
|
aucun Store ouvert
|
||||||
|
aucun SQL
|
||||||
|
aucun accès réseau frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
Le shell expose Diagnostics comme surface technique du gabarit Desk ; les trois écrans métier du screen-map restent Overview / RAW Transactions / RAW Accounts.
|
||||||
|
|
||||||
|
## Dépendances Rust
|
||||||
|
|
||||||
|
Autorisées dans cette tranche :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-config-lib
|
||||||
|
ksp-core-lib
|
||||||
|
ksp-logging-lib
|
||||||
|
chrono
|
||||||
|
fs2
|
||||||
|
serde
|
||||||
|
tauri
|
||||||
|
tauri-plugin-tracing
|
||||||
|
tokio(time)
|
||||||
|
ts-rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Explicitement absentes :
|
||||||
|
|
||||||
|
```text
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Store est volontairement absent de `pre.002`. Le branchement Config composite + `ksp-store-lib` reste dans `pre.006`, après matérialisation et implémentation des capabilities d’inspection.
|
||||||
|
|
||||||
|
## Dépendances frontend
|
||||||
|
|
||||||
|
Socle runtime :
|
||||||
|
|
||||||
|
```text
|
||||||
|
@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
|
||||||
|
```
|
||||||
|
|
||||||
|
Les devDependencies restent celles du gabarit KSP stable. `datatables.net-select-bs5` n’est pas ajouté : aucune fonction de sélection ne le justifie dans le scaffold.
|
||||||
|
|
||||||
|
## Pagination visible
|
||||||
|
|
||||||
|
Les deux tables RAW sont initialisées avec :
|
||||||
|
|
||||||
|
```text
|
||||||
|
paging = true
|
||||||
|
pageLength = 25
|
||||||
|
lengthMenu = 25 / 50 / 100
|
||||||
|
searching = false
|
||||||
|
ordering = false
|
||||||
|
dataset = vide
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun bouton de pagination Store n’existe dans le HTML. En `pre.007`/`pre.008`, ce même pager DataTables passera en `serverSide: true` et sera alimenté par la primitive d’inspection Store définie à partir de `pre.003`. Il ne sera pas doublé.
|
||||||
|
|
||||||
|
## Sécurité et frontières
|
||||||
|
|
||||||
|
- aucune source kbot3 n’est copiée ;
|
||||||
|
- aucun SQL n’entre dans l’application ;
|
||||||
|
- aucune URI Store, credential, pool ou handle backend n’est exposé ;
|
||||||
|
- aucune dépendance Store physique ou API directe n’est admise ;
|
||||||
|
- aucun `fetch`, `XMLHttpRequest`, storage navigateur ou dialogue `alert/confirm/prompt` ;
|
||||||
|
- frontend tracing couvre navigation, boutons, refresh et IPC ;
|
||||||
|
- nouveau Rust sans `unsafe`, `panic!`, `unwrap` ni `expect` ;
|
||||||
|
- assets binaires principaux repris byte-exact du gabarit KSP.
|
||||||
|
|
||||||
|
## Fichiers ajoutés
|
||||||
|
|
||||||
|
- `crates/ksp-app-store-desk/**` ;
|
||||||
|
- `deltas/0.3.8/pre.002.md`.
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
|
||||||
|
- `Cargo.toml` : workspace member Store Desk, header `445` vers `446`, version `0.3.8-pre.1` vers `0.3.8-pre.2` ;
|
||||||
|
- `docs/plans/029-V0_3_8_STORE_DESK_PLAN.md` : version documentaire 2, suppression du fix préventif, correction pager unique et dépendance core scaffold ;
|
||||||
|
- `docs/validation/025-V0_3_8_STORE_DESK.md` : preuve du gate opérateur `pre.001` et validation d’assemblage `pre.002`.
|
||||||
|
|
||||||
|
## Validations exécutées dans l’environnement d’assemblage
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
-> General Rust rule audit: clean
|
||||||
|
-> Rust export completeness audit: 0 candidate(s)
|
||||||
|
-> KSP workspace Rust rule audit: clean
|
||||||
|
```
|
||||||
|
|
||||||
|
Contrôles complémentaires exécutés avant empaquetage :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Markdown table audit: clean (283 table(s), 155 file(s))
|
||||||
|
Manifest parse audit: clean (4 JSON, 2 TOML)
|
||||||
|
Desk package baseline audit: clean (7 mandatory dependencies; devDependencies exact across 4 references; Select absent)
|
||||||
|
Desk binary asset audit: clean (5 assets byte-identical across 4 references)
|
||||||
|
Desk shared SASS audit: clean (5 shared files body-identical across 4 references)
|
||||||
|
Desk splash template audit: clean (body identical modulo Store Desk identity and file metadata)
|
||||||
|
Desk shell/DataTables skeleton audit: clean (header/sidebar/content/footer + SimpleBar + one visible pager owner)
|
||||||
|
Frontend interaction/security audit: clean
|
||||||
|
pre.002 dependency/capability boundary audit: clean
|
||||||
|
Desk port audit: clean (1438/1439 strict and collision-free)
|
||||||
|
Rust scaffold primitive audit: clean (no panic!/unwrap/expect/unsafe block)
|
||||||
|
TypeScript syntax audit: clean (5 file(s), transpileModule 5.8.3)
|
||||||
|
Delta scope audit: clean (47 added, 3 modified, 0 deleted; 46 app files + delta)
|
||||||
|
Final delta archive integrity: clean (`unzip -t`)
|
||||||
|
```
|
||||||
|
|
||||||
|
Le contrôle TypeScript est syntaxique par transpilation ; il ne remplace pas le typecheck npm complet avec dépendances installées et bindings TS-RS générés, ni un build Tauri.
|
||||||
|
|
||||||
|
## Validations non exécutables localement
|
||||||
|
|
||||||
|
`cargo` n’est pas installé dans l’environnement d’assemblage. Les gates suivants doivent donc être exécutés par l’opérateur après application du delta :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-app-store-desk
|
||||||
|
cargo tree -p ksp-app-store-desk --edges normal
|
||||||
|
cargo tree -p ksp-app-store-desk -e features
|
||||||
|
```
|
||||||
|
|
||||||
|
## Non inclus
|
||||||
|
|
||||||
|
- aucune capability Store d’inspection ;
|
||||||
|
- aucune modification de `ksp-store-api`, `ksp-store-lib` ou `ksp-store-postgres-lib` ;
|
||||||
|
- aucun composite Store Desk ;
|
||||||
|
- aucun Store ouvert au runtime ;
|
||||||
|
- aucune query métier ou donnée RAW ;
|
||||||
|
- aucun détail d’entité ou observation ;
|
||||||
|
- aucun `serverSide` Store réel ;
|
||||||
|
- aucune modification CHANGELOG/ROADMAP ;
|
||||||
|
- aucune reprise de code ou package kbot3.
|
||||||
|
|
||||||
|
## Suite prévue
|
||||||
|
|
||||||
|
`pre.003` introduit les contrats backend-neutral d’inspection offset/count/summaries dans Store. Si un défaut réel de `pre.003` apparaît ensuite, et seulement dans ce cas, un `pre.003-fix.NNN` pourra être créé conformément aux règles de versionnement.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/029-V0_3_8_STORE_DESK_PLAN.md -->
|
<!-- file: docs/plans/029-V0_3_8_STORE_DESK_PLAN.md -->
|
||||||
<!-- version: 1 -->
|
<!-- version: 2 -->
|
||||||
|
|
||||||
# Plan v0.3.8 — Store Desk V1 RAW
|
# Plan v0.3.8 — Store Desk V1 RAW
|
||||||
|
|
||||||
@@ -60,17 +60,17 @@ Store Desk -> composition + DTO + commands + UX
|
|||||||
|
|
||||||
### 3.2 Dépendances applicatives prévues
|
### 3.2 Dépendances applicatives prévues
|
||||||
|
|
||||||
| Dépendance | Statut Store Desk | Justification |
|
| Dépendance | Statut Store Desk | Justification |
|
||||||
|--------------------------|-------------------|----------------------------------------------------|
|
|--------------------------|-------------------|-------------------------------------------------|
|
||||||
| `ksp-config-lib` | REQUISE | composite, profils, bootstrap Config |
|
| `ksp-config-lib` | REQUISE | composite, profils, bootstrap Config |
|
||||||
| `ksp-logging-lib` | REQUISE | runtime Logging et tracing KSP |
|
| `ksp-logging-lib` | REQUISE | runtime Logging et tracing KSP |
|
||||||
| `ksp-store-lib` | REQUISE | unique façade Store autorisée |
|
| `ksp-store-lib` | REQUISE | unique façade Store autorisée |
|
||||||
| `ksp-core-lib` | NON REQUISE V1 | réexports Store suffisants pour les contrats visés |
|
| `ksp-core-lib` | REQUISE SCAFFOLD | erreurs applicatives typées et gabarit Desk KSP |
|
||||||
| `ksp-store-api` | INTERDITE DIRECTE | la Desk ne contourne pas la façade |
|
| `ksp-store-api` | INTERDITE DIRECTE | la Desk ne contourne pas la façade |
|
||||||
| `ksp-store-postgres-lib` | INTERDITE | backend physique privé |
|
| `ksp-store-postgres-lib` | INTERDITE | backend physique privé |
|
||||||
| `tokio-postgres` | INTERDITE | driver physique |
|
| `tokio-postgres` | INTERDITE | driver physique |
|
||||||
| `deadpool-postgres` | INTERDITE | pool physique |
|
| `deadpool-postgres` | INTERDITE | pool physique |
|
||||||
| Transport RPC/gRPC/WS | INTERDIT V1 | inspection Store uniquement |
|
| Transport RPC/gRPC/WS | INTERDIT V1 | inspection Store uniquement |
|
||||||
|
|
||||||
## 4. Gabarit KSP Store Desk
|
## 4. Gabarit KSP Store Desk
|
||||||
|
|
||||||
@@ -338,8 +338,9 @@ refresh
|
|||||||
### 10.2 RAW Transactions
|
### 10.2 RAW Transactions
|
||||||
|
|
||||||
```text
|
```text
|
||||||
filtres Store : slot min/max, direction, page size
|
filtres Store : slot min/max, direction
|
||||||
contrôles Store : Premier, Précédent, Suivant, Refresh
|
page size/pagination : contrôles DataTables 25/50/100 + pager unique
|
||||||
|
contrôle applicatif : Refresh
|
||||||
filtres Store explicites : dataset complet de la query
|
filtres Store explicites : dataset complet de la query
|
||||||
table
|
table
|
||||||
bouton/interaction Détail
|
bouton/interaction Détail
|
||||||
@@ -541,16 +542,12 @@ Archive/règles/prompt, gabarit KSP/npm, audit fonctionnel kbot3, capability/gap
|
|||||||
|
|
||||||
### pre.002 — scaffold `ksp-app-store-desk`
|
### pre.002 — scaffold `ksp-app-store-desk`
|
||||||
|
|
||||||
Package lib/bin, ports 1438/1439, shell/splash/assets/fonts/SASS KSP, tracing frontend, capabilities minimales, `datatables.net-bs5` et table skeleton sans données Store. Aucun Store ouvert.
|
Package lib/bin, ports 1438/1439, shell/splash/assets/fonts/SASS KSP, tracing frontend, capabilities minimales, `datatables.net-bs5` et table skeleton sans données Store. Aucun Store ouvert. Le skeleton peut paginer son dataset vide/local pour valider le rendu DataTables, mais ne matérialise aucun second pager Store et ne préjuge pas du branchement `serverSide` de `pre.007`/`pre.008`.
|
||||||
|
|
||||||
### pre.003 — contrats Store d'inspection
|
### pre.003 — contrats Store d'inspection
|
||||||
|
|
||||||
Ajouter dans `ksp-store-api` les types page offset/count, queries Tx/Account, summaries sans gros bytes et deux traits read d'inspection. Réexports `ksp-store-lib`, canaris external backend/public API/security. Aucun SQL encore.
|
Ajouter dans `ksp-store-api` les types page offset/count, queries Tx/Account, summaries sans gros bytes et deux traits read d'inspection. Réexports `ksp-store-lib`, canaris external backend/public API/security. Aucun SQL encore.
|
||||||
|
|
||||||
#### pre.003-fix.NNN — correctifs éventuels de contrat
|
|
||||||
|
|
||||||
Lane strictement bornée aux invariants des nouveaux contrats d'inspection.
|
|
||||||
|
|
||||||
### pre.004 — inspection PostgreSQL RawTransaction
|
### pre.004 — inspection PostgreSQL RawTransaction
|
||||||
|
|
||||||
Implémenter summary query, counts exacts, offset/limit checked et trait Tx dans `ksp-store-postgres-lib`, puis dispatch façade Store. Conserver intégralement le listage keyset existant. Tests no-payload/N+1, counts filtres, offset hostile et conformance.
|
Implémenter summary query, counts exacts, offset/limit checked et trait Tx dans `ksp-store-postgres-lib`, puis dispatch façade Store. Conserver intégralement le listage keyset existant. Tests no-payload/N+1, counts filtres, offset hostile et conformance.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/025-V0_3_8_STORE_DESK.md -->
|
<!-- file: docs/validation/025-V0_3_8_STORE_DESK.md -->
|
||||||
<!-- version: 1 -->
|
<!-- version: 2 -->
|
||||||
|
|
||||||
# Validation v0.3.8 — Store Desk V1 RAW
|
# Validation v0.3.8 — Store Desk V1 RAW
|
||||||
|
|
||||||
@@ -111,3 +111,77 @@ Le journal opérateur fourni pour v0.3.7 rapporte un gate complet. Il reste une
|
|||||||
- [X] diff contre l'archive v0.3.7 : exactement quatre fichiers ajoutés/modifiés, aucune suppression ;
|
- [X] diff contre l'archive v0.3.7 : exactement quatre fichiers ajoutés/modifiés, aucune suppression ;
|
||||||
- [X] archive delta testée par `unzip -t` ;
|
- [X] archive delta testée par `unzip -t` ;
|
||||||
- [ ] gates Cargo : non exécutés dans l'environnement d'assemblage, `cargo` absent.
|
- [ ] gates Cargo : non exécutés dans l'environnement d'assemblage, `cargo` absent.
|
||||||
|
|
||||||
|
## 10. Gate opérateur de la base `pre.001`
|
||||||
|
|
||||||
|
Après application de `pre.001`, l’opérateur a exécuté :
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
```
|
||||||
|
|
||||||
|
Résultats fournis :
|
||||||
|
|
||||||
|
```text
|
||||||
|
General Rust rule audit: clean
|
||||||
|
Rust export completeness audit: 0 candidate(s)
|
||||||
|
KSP workspace Rust rule audit: clean
|
||||||
|
Markdown table audit: clean (314 table(s), 690 file(s))
|
||||||
|
cargo check --workspace: PASS
|
||||||
|
cargo clippy --workspace --all-targets: PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette preuve opérateur ferme le gate technique de la base `0.3.8-pre.1` avant matérialisation de `pre.002`. Aucun test crate ciblé n’est inféré au-delà des commandes effectivement fournies.
|
||||||
|
|
||||||
|
## 11. `pre.002` — scaffold Store Desk matérialisé
|
||||||
|
|
||||||
|
- [X] `crates/ksp-app-store-desk` créé comme package Rust lib + bin ;
|
||||||
|
- [X] workspace avancé à `0.3.8-pre.2` ;
|
||||||
|
- [X] ports Vite/HMR stricts 1438/1439 ;
|
||||||
|
- [X] identité Tauri `KSP Store Desk` / `com.sasedev.ksp-app-store-desk` ;
|
||||||
|
- [X] splash, logo, favicon et font repris byte-exact depuis le gabarit Desk KSP ;
|
||||||
|
- [X] shell et SASS dérivés du gabarit KSP, sans source kbot3 ;
|
||||||
|
- [X] Config + Logging initialisés selon le pattern Desk KSP ;
|
||||||
|
- [X] `ksp-core-lib` admis au scaffold pour les erreurs applicatives typées, sans contourner Store ;
|
||||||
|
- [X] aucune dépendance `ksp-store-lib`, `ksp-store-api`, `ksp-store-postgres-lib`, Transport ou Job dans cette tranche ;
|
||||||
|
- [X] capabilities limitées à `core:default` + `tracing:default` ;
|
||||||
|
- [X] frontend tracing installé pour navigation, boutons, refresh et IPC ;
|
||||||
|
- [X] dépendances npm KSP stables conservées ;
|
||||||
|
- [X] `datatables.net-bs5` ajouté sans Select ;
|
||||||
|
- [X] skeletons RAW Transactions / RAW Accounts vides créés ;
|
||||||
|
- [X] aucun Store n’est ouvert et aucune donnée Store n’est chargée ;
|
||||||
|
- [X] aucun contrôle `Premier / Précédent / Suivant` Store n’est rendu ;
|
||||||
|
- [X] DataTables reste l’unique owner visuel du paging dès le scaffold ;
|
||||||
|
- [X] le mode local vide de `pre.002` ne préjuge pas du `serverSide` prévu en `pre.007`/`pre.008` ;
|
||||||
|
- [X] plan corrigé : suppression du placeholder préventif `pre.003-fix.NNN` ;
|
||||||
|
- [X] plan corrigé : screen-map alignée sur le pager DataTables unique.
|
||||||
|
|
||||||
|
## 12. Gates d’assemblage `pre.002`
|
||||||
|
|
||||||
|
- [X] `python3 scripts/audit_rust_workspace_rules.py` : General Rust clean, export completeness 0, KSP workspace Rust clean ;
|
||||||
|
- [X] audit Markdown : clean, 283 tables / 155 fichiers sur le périmètre `deltas/0.3.8` ;
|
||||||
|
- [X] manifest parse : 4 JSON + 2 TOML ;
|
||||||
|
- [X] package baseline : 7 dépendances runtime obligatoires, devDependencies exactes sur 4 Desks KSP, Select absent ;
|
||||||
|
- [X] assets : 5 fichiers binaires byte-identical sur 4 Desks KSP ;
|
||||||
|
- [X] SASS partagé : 5 fichiers body-identical au gabarit KSP ;
|
||||||
|
- [X] splash : body identique au gabarit modulo identité Store Desk et metadata fichier ;
|
||||||
|
- [X] audit shell/DataTables : structure KSP + deux skeletons + owner visuel unique du pager ;
|
||||||
|
- [X] audit statique des dépendances/scopes Store/Transport/Job : aucun bypass ;
|
||||||
|
- [X] ports : 1438/1439 stricts et sans collision avec les autres Desks ;
|
||||||
|
- [X] audit frontend : pas de `fetch`, storage navigateur ni dialogue bloquant ;
|
||||||
|
- [X] audit TypeScript syntaxique par `transpileModule` 5.8.3 : 5 fichiers propres ;
|
||||||
|
- [X] audit Rust source du nouveau scaffold : aucun `panic!`, `unwrap`, `expect` ou bloc `unsafe` ;
|
||||||
|
- [X] delta scope : 47 ajouts, 3 modifications, 0 suppression ; 46 fichiers applicatifs + delta ;
|
||||||
|
- [X] archive delta finale vérifiée par `unzip -t` ;
|
||||||
|
- [ ] `cargo fmt --all` : non exécuté dans l’environnement d’assemblage, `cargo` absent ;
|
||||||
|
- [ ] `cargo check --workspace` : non exécuté dans l’environnement d’assemblage, `cargo` absent ;
|
||||||
|
- [ ] `cargo clippy --workspace --all-targets` : non exécuté dans l’environnement d’assemblage, `cargo` absent ;
|
||||||
|
- [ ] `cargo test -p ksp-app-store-desk` : à exécuter par l’opérateur ;
|
||||||
|
- [ ] `cargo tree -p ksp-app-store-desk --edges normal` : à exécuter par l’opérateur ;
|
||||||
|
- [ ] `cargo tree -p ksp-app-store-desk -e features` : à exécuter par l’opérateur.
|
||||||
|
|
||||||
|
La case `pre.002` de la section « Gates futures » reste ouverte jusqu’au retour de ces gates Cargo opérateur.
|
||||||
|
|||||||
Reference in New Issue
Block a user