v0.2.6-pre.002
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 164
|
# version: 165
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.6-pre.1"
|
version = "0.2.6-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"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 56 -->
|
<!-- version: 58 -->
|
||||||
|
|
||||||
# Roadmap KSP
|
# Roadmap KSP
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ Le roadmap décrit les objectifs à atteindre et les grandes étapes prévues. U
|
|||||||
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
|
- [X] `0.2.3` — HTTP Transactions stable : 11/11 wrappers typés publiés, classification `8 Read / 2 WriteSubmission / 1 Simulation`, no-resend ambigu prouvé pour les write submissions, `KSP-TRANSPORT-007` réaudité conforme sur les 37 wrappers HTTP courants, graphes Cargo et deux smokes Devnet validés ; `0.2.4` reprend les 15 Blocks/Economics restants.
|
||||||
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
|
- [X] `0.2.4` — HTTP Blocks + Economics stable : 15/15 wrappers `V0_2_4` publiés, surface typed complète à 52/52 méthodes courantes, 14/14 historiques conservées, réaudit SIMD/inventaire final et `KSP-TRANSPORT-007` global validés ; deux smokes Devnet passés avant publication.
|
||||||
- [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001`–`fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe l’audit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
|
- [X] `0.2.5` — Wallet foundation stable : `.kspwallet` V1, VIEW/OWNER indépendants, Argon2id/XChaCha20-Poly1305, autorité Ed25519 OWNER, persistence no-clobber, signature, administration/rotations/révocation VIEW forte, import/export Solana CLI JSON + Base58, canaris adversariaux, interop externe et documentation durable publiés. La clôture `pre.010-fix.001`–`fix.003` ajoute `ed25519-dalek 3.0.0` direct, normalise le Rust workspace et installe l’audit structurel Python complémentaire à rustfmt/Clippy. `Pubkey` reste via `ksp-core-lib`, la keypair reste encapsulée dans Wallet et Config/Transport/ExecutionPolicy/Store/Tauri restent hors Wallet.
|
||||||
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP. Le gate `pre.001` retient `cfg.std.wallet` + composite Wallet Desk, inventory `.kspwallet` root-scoped/symlink-safe, handles VIEW/OWNER côté Rust, identité autorisée + `getBalance`, administration OWNER/import/export déjà possédés par Wallet, CSP/capabilities Tauri minimales et un forecast regranularisé jusqu’à `pre.013`.
|
- [ ] `0.2.6` — Introduire `ksp-app-wallet-desk` utilisant Config composite + Wallet + transport HTTP. `pre.001` fixe le sizing et le gabarit ; `pre.002` matérialise la crate Tauri et son shell splash/main avec Config + Logging bootstrap, ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, resize-observer-polyfill, TS-RS et bridge frontend Logging. Les tranches suivantes ajoutent `cfg.std.wallet`, inventory, lifecycle VIEW/OWNER, secrets `KSP_SECRET_WALLET_PASS_*`, identité autorisée + `getBalance`, puis administration/import/export, avec une dernière tranche prévue pour README/USAGE/docs/validation, prompt `0.2.7` et build Tauri final.
|
||||||
- [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe.
|
- [ ] `0.2.7` — Étendre `ksp-onchain-transport-lib` au WebSocket Solana standard complet ; permettre plusieurs sessions sur une même URL sans imposer encore un pool automatique complexe.
|
||||||
- [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
|
- [ ] `0.2.8` — Ajouter Helius LaserStream WebSocket comme extension du moteur WebSocket standard, sans duplication de client.
|
||||||
- [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.
|
- [ ] `0.2.9` — Ajouter une première fondation Yellowstone gRPC standard/provider-neutral ; dimensionner la surface exacte à `pre.001` selon la documentation normative actuelle.
|
||||||
|
|||||||
40
crates/ksp-app-wallet-desk/Cargo.toml
Normal file
40
crates/ksp-app-wallet-desk/Cargo.toml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# file: crates/ksp-app-wallet-desk/Cargo.toml
|
||||||
|
# version: 1
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "ksp-app-wallet-desk"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "ksp_app_wallet_desk_lib"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "ksp-app-wallet-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-wallet-desk/build.rs
Normal file
12
crates/ksp-app-wallet-desk/build.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/build.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Build script for the KSP wallet desktop application.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
13
crates/ksp-app-wallet-desk/capabilities/default.json
Normal file
13
crates/ksp-app-wallet-desk/capabilities/default.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capability for KSP Wallet Desk",
|
||||||
|
"windows": [
|
||||||
|
"splash",
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"tracing:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
crates/ksp-app-wallet-desk/frontend/fonts/DOS_Amazigh.ttf
Normal file
BIN
crates/ksp-app-wallet-desk/frontend/fonts/DOS_Amazigh.ttf
Normal file
Binary file not shown.
25
crates/ksp-app-wallet-desk/frontend/fonts/README.md
Normal file
25
crates/ksp-app-wallet-desk/frontend/fonts/README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<!-- file: crates/ksp-app-wallet-desk/frontend/fonts/README.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Fonts du splash Wallet Desk
|
||||||
|
|
||||||
|
Wallet Desk réutilise le gabarit de splash KSP déjà stabilisé dans Config Desk et conserve la police locale :
|
||||||
|
|
||||||
|
```text
|
||||||
|
DOS_Amazigh.ttf
|
||||||
|
font-family: Dos Amazigh
|
||||||
|
```
|
||||||
|
|
||||||
|
Destination :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-app-wallet-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-wallet-desk/frontend/imgs/logo.png
Normal file
BIN
crates/ksp-app-wallet-desk/frontend/imgs/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
BIN
crates/ksp-app-wallet-desk/frontend/imgs/splash.png
Normal file
BIN
crates/ksp-app-wallet-desk/frontend/imgs/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 611 KiB |
103
crates/ksp-app-wallet-desk/frontend/main.html
Normal file
103
crates/ksp-app-wallet-desk/frontend/main.html
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Wallet Desk — Dashboard</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="app-header bg-dark text-white shadow-sm">
|
||||||
|
<div class="container-fluid h-100 d-flex align-items-center gap-3 px-4">
|
||||||
|
<img class="app-logo" src="./imgs/logo.png" alt="KSP">
|
||||||
|
<div class="d-flex flex-column me-auto">
|
||||||
|
<span class="fw-semibold">Wallet Desk — <span id="headerViewTitle">Dashboard</span></span>
|
||||||
|
<small id="shellStatus" class="text-white-50 app-shell-status">Initialisation du shell…</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-light btn-sm" type="button" data-shell-action="lock" disabled>
|
||||||
|
<i class="fa-solid fa-lock me-2" aria-hidden="true"></i>Lock
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="app-main container-fluid p-0">
|
||||||
|
<div class="row g-0 h-100">
|
||||||
|
<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 Wallet Desk">
|
||||||
|
<button class="nav-link text-start active" type="button" data-view="dashboard"><i class="fa-solid fa-gauge-high me-2" aria-hidden="true"></i>Dashboard</button>
|
||||||
|
<button class="nav-link text-start" type="button" data-view="wallets"><i class="fa-solid fa-wallet me-2" aria-hidden="true"></i>Wallets</button>
|
||||||
|
<button class="nav-link text-start" type="button" data-view="create-import"><i class="fa-solid fa-circle-plus me-2" aria-hidden="true"></i>Create / Import</button>
|
||||||
|
<button class="nav-link text-start" type="button" data-view="details"><i class="fa-solid fa-id-card me-2" aria-hidden="true"></i>Details</button>
|
||||||
|
<button class="nav-link text-start" type="button" data-view="security"><i class="fa-solid fa-shield-halved me-2" aria-hidden="true"></i>Security</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 h-100" data-simplebar>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h1 id="viewTitle" class="h3 mb-1">Dashboard</h1>
|
||||||
|
<p class="text-body-secondary mb-0">Fondation UI de Wallet Desk — fonctionnalités Wallet branchées dans les tranches suivantes.</p>
|
||||||
|
</div>
|
||||||
|
<span class="badge text-bg-secondary">0.2.6-pre.002</span>
|
||||||
|
</div>
|
||||||
|
<section data-view-panel="dashboard">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-xl-8">
|
||||||
|
<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 enregistrés</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>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-4">
|
||||||
|
<div class="card shadow-sm h-100">
|
||||||
|
<div class="card-header fw-semibold">Wallet courant</div>
|
||||||
|
<div class="card-body d-flex flex-column align-items-center justify-content-center text-center text-body-secondary">
|
||||||
|
<i class="fa-solid fa-lock fa-2x mb-3" aria-hidden="true"></i>
|
||||||
|
<p class="mb-0">Aucun wallet sélectionné.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section data-view-panel="wallets" hidden>
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<span class="fw-semibold">Wallets disponibles</span>
|
||||||
|
<small class="text-body-secondary"><i class="fa-solid fa-lock me-1" aria-hidden="true"></i>verrouillé · <i class="fa-solid fa-lock-open me-1" aria-hidden="true"></i>ouvert</small>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-primary btn-sm" type="button" data-shell-action="refresh-wallets" disabled>
|
||||||
|
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Refresh
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table id="walletInventoryTable" class="table table-striped table-hover align-middle w-100">
|
||||||
|
<thead><tr><th>État</th><th>Filename</th><th>Format</th><th>VIEW</th><th>Inspection</th></tr></thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section data-view-panel="create-import" hidden><div class="app-placeholder"><i class="fa-solid fa-circle-plus"></i><p>Create / Import sera branché après la fondation Config et inventory.</p></div></section>
|
||||||
|
<section data-view-panel="details" hidden><div class="app-placeholder"><i class="fa-solid fa-id-card"></i><p>Les détails protégés seront affichés uniquement après VIEW/OWNER.</p></div></section>
|
||||||
|
<section data-view-panel="security" hidden><div class="app-placeholder"><i class="fa-solid fa-shield-halved"></i><p>Les opérations OWNER seront ajoutées après le MVP identité + balance.</p></div></section>
|
||||||
|
<section data-view-panel="diagnostics" hidden><div class="app-placeholder"><i class="fa-solid fa-stethoscope"></i><p>Les diagnostics Config/Wallet/Transport seront enrichis progressivement.</p></div></section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="app-footer bg-dark text-white-50 d-flex align-items-center px-4">
|
||||||
|
<span>KSP Wallet Desk · shell pre.002</span>
|
||||||
|
</footer>
|
||||||
|
<script type="module" src="./ts/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
94
crates/ksp-app-wallet-desk/frontend/sass/_app.scss
Normal file
94
crates/ksp-app-wallet-desk/frontend/sass/_app.scss
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// file: crates/ksp-app-wallet-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-logo {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell-status,
|
||||||
|
.app-runtime-list dd,
|
||||||
|
#walletInventoryTable td:nth-child(2),
|
||||||
|
#walletInventoryTable td:nth-child(3) {
|
||||||
|
font-family: var(--bs-font-monospace);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-sidebar .nav-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-placeholder {
|
||||||
|
min-height: 320px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 2rem;
|
||||||
|
border: 1px dashed var(--bs-border-color);
|
||||||
|
border-radius: var(--bs-border-radius-lg);
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
background: var(--bs-body-bg);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-placeholder i {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#walletInventoryTable tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
160
crates/ksp-app-wallet-desk/frontend/sass/_bootswatch.scss
Normal file
160
crates/ksp-app-wallet-desk/frontend/sass/_bootswatch.scss
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// file: crates/ksp-app-wallet-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-wallet-desk/frontend/sass/_fontawesome.scss
Normal file
19
crates/ksp-app-wallet-desk/frontend/sass/_fontawesome.scss
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// file: crates/ksp-app-wallet-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-wallet-desk/frontend/sass/_simplebar.scss
Normal file
248
crates/ksp-app-wallet-desk/frontend/sass/_simplebar.scss
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
// file: crates/ksp-app-wallet-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-wallet-desk/frontend/sass/_variables.scss
Normal file
95
crates/ksp-app-wallet-desk/frontend/sass/_variables.scss
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
// file: crates/ksp-app-wallet-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-wallet-desk/frontend/sass/main.scss
Normal file
10
crates/ksp-app-wallet-desk/frontend/sass/main.scss
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// file: crates/ksp-app-wallet-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";
|
||||||
65
crates/ksp-app-wallet-desk/frontend/sass/splash.scss
Normal file
65
crates/ksp-app-wallet-desk/frontend/sass/splash.scss
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// file: crates/ksp-app-wallet-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#splash-container {
|
||||||
|
position: relative;
|
||||||
|
width: 960px;
|
||||||
|
height: 637px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#splash-image {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#splash-status {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
z-index: 3;
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
color: #fff;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
16
crates/ksp-app-wallet-desk/frontend/splash.html
Normal file
16
crates/ksp-app-wallet-desk/frontend/splash.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Chargement — KSP Wallet Desk</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="splash-container">
|
||||||
|
<img id="splash-image" src="./imgs/splash.png" alt="">
|
||||||
|
<div id="app-name">Wallet Desk</div>
|
||||||
|
<div id="splash-status">Initialisation…</div>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="./ts/splash.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
110
crates/ksp-app-wallet-desk/frontend/ts/frontend_log.ts
Normal file
110
crates/ksp-app-wallet-desk/frontend/ts/frontend_log.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/frontend/ts/frontend_log.ts
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { FrontendLogPayloadDto } from "./bindings/ksp_app_wallet_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-wallet-desk/frontend/ts/invoke.ts
Normal file
17
crates/ksp-app-wallet-desk/frontend/ts/invoke.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// file: crates/ksp-app-wallet-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-wallet-desk/frontend/ts/main.ts
Normal file
152
crates/ksp-app-wallet-desk/frontend/ts/main.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
import "bootstrap";
|
||||||
|
import DataTable from "datatables.net-bs5";
|
||||||
|
import "datatables.net-select-bs5";
|
||||||
|
import ResizeObserver from "resize-observer-polyfill";
|
||||||
|
import "simplebar";
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
import type { RuntimeStatusDto } from "./bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts";
|
||||||
|
import { frontendDebug, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
|
||||||
|
import { invokeKsp } from "./invoke";
|
||||||
|
import "../sass/main.scss";
|
||||||
|
|
||||||
|
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||||
|
installFrontendConsoleBridge("main");
|
||||||
|
|
||||||
|
type ViewId = "dashboard" | "wallets" | "create-import" | "details" | "security" | "diagnostics";
|
||||||
|
|
||||||
|
const viewTitles: Record<ViewId, string> = {
|
||||||
|
dashboard: "Dashboard",
|
||||||
|
wallets: "Wallets",
|
||||||
|
"create-import": "Create / Import",
|
||||||
|
details: "Details",
|
||||||
|
security: "Security",
|
||||||
|
diagnostics: "Diagnostics",
|
||||||
|
};
|
||||||
|
|
||||||
|
function isViewId(value: string): value is ViewId {
|
||||||
|
return value in viewTitles;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateView(viewId: ViewId, source: "startup" | "user"): void {
|
||||||
|
if (source === "user") {
|
||||||
|
frontendDebug("main", "Wallet Desk navigation activated", { viewId });
|
||||||
|
}
|
||||||
|
const title = viewTitles[viewId];
|
||||||
|
const headerTitle = document.querySelector<HTMLElement>("#headerViewTitle");
|
||||||
|
const viewTitle = document.querySelector<HTMLElement>("#viewTitle");
|
||||||
|
if (headerTitle) {
|
||||||
|
headerTitle.textContent = title;
|
||||||
|
}
|
||||||
|
if (viewTitle) {
|
||||||
|
viewTitle.textContent = title;
|
||||||
|
}
|
||||||
|
document.title = `Wallet Desk — ${title}`;
|
||||||
|
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
||||||
|
const active = button.dataset.view === viewId;
|
||||||
|
button.classList.toggle("active", active);
|
||||||
|
button.setAttribute("aria-current", active ? "page" : "false");
|
||||||
|
});
|
||||||
|
document.querySelectorAll<HTMLElement>("[data-view-panel]").forEach(panel => {
|
||||||
|
panel.hidden = panel.dataset.viewPanel !== viewId;
|
||||||
|
});
|
||||||
|
frontendTrace("main", "Wallet Desk view DOM updated", { viewId, source });
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindNavigation(): void {
|
||||||
|
document.querySelectorAll<HTMLButtonElement>("[data-view]").forEach(button => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const requestedView = button.dataset.view;
|
||||||
|
frontendTrace("main", "Wallet Desk navigation clicked", { requestedView: requestedView ?? null });
|
||||||
|
if (requestedView && isViewId(requestedView)) {
|
||||||
|
activateView(requestedView, "user");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
frontendTrace("main", "Wallet Desk navigation handlers installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeWalletTable(): void {
|
||||||
|
new DataTable("#walletInventoryTable", {
|
||||||
|
order: [[1, "asc"]],
|
||||||
|
pageLength: 10,
|
||||||
|
select: {
|
||||||
|
style: "single",
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
emptyTable: "L'inventaire Wallet sera branché en pre.004.",
|
||||||
|
search: "Filtrer :",
|
||||||
|
zeroRecords: "Aucun wallet correspondant.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.002-shell" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRuntimeStatus(status: RuntimeStatusDto): void {
|
||||||
|
const version = document.querySelector<HTMLElement>("#runtimeVersion");
|
||||||
|
const profile = document.querySelector<HTMLElement>("#runtimeLoggingProfile");
|
||||||
|
const fallback = document.querySelector<HTMLElement>("#runtimeLoggingFallback");
|
||||||
|
const documents = document.querySelector<HTMLElement>("#runtimeConfigDocuments");
|
||||||
|
const phase = document.querySelector<HTMLElement>("#runtimeShellPhase");
|
||||||
|
const shellStatus = document.querySelector<HTMLElement>("#shellStatus");
|
||||||
|
if (version) {
|
||||||
|
version.textContent = status.applicationVersion;
|
||||||
|
}
|
||||||
|
if (profile) {
|
||||||
|
profile.textContent = status.activeLoggingProfile ?? "fallback transitoire";
|
||||||
|
}
|
||||||
|
if (fallback) {
|
||||||
|
fallback.textContent = status.fallbackLoggingActive ? "oui" : "non";
|
||||||
|
}
|
||||||
|
if (documents) {
|
||||||
|
documents.textContent = status.configDocumentCount.toString();
|
||||||
|
}
|
||||||
|
if (phase) {
|
||||||
|
phase.textContent = status.shellPhase;
|
||||||
|
}
|
||||||
|
if (shellStatus) {
|
||||||
|
shellStatus.textContent = "Shell Wallet Desk prêt.";
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Wallet Desk runtime status rendered", {
|
||||||
|
fallbackLoggingActive: status.fallbackLoggingActive,
|
||||||
|
shellPhase: status.shellPhase,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRuntimeStatus(): Promise<void> {
|
||||||
|
const status = await invokeKsp<RuntimeStatusDto>("main", "get_runtime_status");
|
||||||
|
renderRuntimeStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindShellActions(): void {
|
||||||
|
document.querySelectorAll<HTMLButtonElement>("[data-shell-action]").forEach(button => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
frontendDebug("main", "Wallet Desk shell action clicked", { action: button.dataset.shellAction ?? "unknown", enabled: !button.disabled });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
frontendTrace("main", "Wallet Desk shell action handlers installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeMain(): Promise<void> {
|
||||||
|
const windowLabel = getCurrentWindow().label;
|
||||||
|
frontendInfo("main", "Wallet Desk main frontend loaded", { windowLabel });
|
||||||
|
bindNavigation();
|
||||||
|
bindShellActions();
|
||||||
|
initializeWalletTable();
|
||||||
|
activateView("dashboard", "startup");
|
||||||
|
try {
|
||||||
|
await loadRuntimeStatus();
|
||||||
|
} catch {
|
||||||
|
const shellStatus = document.querySelector<HTMLElement>("#shellStatus");
|
||||||
|
if (shellStatus) {
|
||||||
|
shellStatus.textContent = "Le statut runtime n'a pas pu être chargé.";
|
||||||
|
}
|
||||||
|
frontendTrace("main", "Wallet Desk shell status replaced", { status: "runtime_error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
void initializeMain();
|
||||||
|
});
|
||||||
107
crates/ksp-app-wallet-desk/frontend/ts/splash.ts
Normal file
107
crates/ksp-app-wallet-desk/frontend/ts/splash.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// file: crates/ksp-app-wallet-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_wallet_desk/splash/SplashOrderDto.ts";
|
||||||
|
import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log";
|
||||||
|
import { invokeKsp } from "./invoke";
|
||||||
|
import "../sass/splash.scss";
|
||||||
|
|
||||||
|
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 replaceStatus(message: string | null): void {
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const status = document.querySelector<HTMLElement>("#splash-status");
|
||||||
|
if (status) {
|
||||||
|
status.textContent = message;
|
||||||
|
frontendTrace("splash", "Splash status replaced", { message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSplashOrder(order: SplashOrderDto): Promise<void> {
|
||||||
|
frontendTrace("splash", "Splash order received", { action: order.action });
|
||||||
|
const container = document.querySelector<HTMLElement>("#splash-container");
|
||||||
|
replaceStatus(order.message);
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (order.action === "fade_in") {
|
||||||
|
await animateOpacity(container, 0, 1, normalizeDurationMs(order.durationMs));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (order.action === "fade_out") {
|
||||||
|
await animateOpacity(container, 1, 0, normalizeDurationMs(order.durationMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeSplash(): Promise<void> {
|
||||||
|
const windowLabel = getCurrentWindow().label;
|
||||||
|
frontendInfo("splash", "Wallet Desk splash frontend loaded", { windowLabel });
|
||||||
|
const container = document.querySelector<HTMLElement>("#splash-container");
|
||||||
|
if (container) {
|
||||||
|
container.style.opacity = "0";
|
||||||
|
container.style.willChange = "opacity";
|
||||||
|
frontendTrace("splash", "Splash container prepared for managed fade-in");
|
||||||
|
}
|
||||||
|
await listen<SplashOrderDto>("ksp-splash-order", event => {
|
||||||
|
void handleSplashOrder(event.payload);
|
||||||
|
});
|
||||||
|
frontendDebug("splash", "Splash lifecycle listener installed");
|
||||||
|
try {
|
||||||
|
await invokeKsp<void>("splash", "splash_frontend_ready");
|
||||||
|
} catch {
|
||||||
|
replaceStatus("Le lifecycle du splash n'a pas pu démarrer.");
|
||||||
|
if (container) {
|
||||||
|
container.style.opacity = "1";
|
||||||
|
container.style.willChange = "auto";
|
||||||
|
}
|
||||||
|
frontendError("splash", "Splash readiness command failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
void initializeSplash();
|
||||||
|
});
|
||||||
BIN
crates/ksp-app-wallet-desk/icons/favicon.ico
Normal file
BIN
crates/ksp-app-wallet-desk/icons/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 448 KiB |
BIN
crates/ksp-app-wallet-desk/icons/favicon.png
Normal file
BIN
crates/ksp-app-wallet-desk/icons/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
28
crates/ksp-app-wallet-desk/package.json
Normal file
28
crates/ksp-app-wallet-desk/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "ksp-app-wallet-desk",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.2.6-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",
|
||||||
|
"datatables.net-select-bs5": "^4.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
111
crates/ksp-app-wallet-desk/src/app_state.rs
Normal file
111
crates/ksp-app-wallet-desk/src/app_state.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/app_state.rs
|
||||||
|
// version: 2
|
||||||
|
|
||||||
|
//! Shared backend state owned by the Wallet Desk Tauri application.
|
||||||
|
|
||||||
|
/// Shared Wallet 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 AppState {
|
||||||
|
/// Initializes Config ownership, Logging and the common desktop splash state.
|
||||||
|
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_ms = splash_settings.fade_ms(), fade_source = splash_settings.fade_source(), expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(), "resolved Wallet 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 shell status DTO exposed during pre.002.
|
||||||
|
pub(crate) fn runtime_status(&self) -> ksp_core_lib::Result<crate::RuntimeStatusDto> {
|
||||||
|
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 Wallet Desk shell 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,
|
||||||
|
"Wallet Desk Logging runtime state lock is poisoned",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let _keep_guard_alive = &runtime.guard;
|
||||||
|
return std::result::Result::Ok(crate::RuntimeStatusDto {
|
||||||
|
application_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||||
|
config_document_count: document_count,
|
||||||
|
active_logging_profile: runtime.active_profile_id.clone(),
|
||||||
|
fallback_logging_active: runtime.fallback_active,
|
||||||
|
startup_diagnostic: runtime.startup_diagnostic.clone(),
|
||||||
|
shell_phase: "pre.002-shell".to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the resolved common splash timings captured during 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>,
|
||||||
|
}
|
||||||
135
crates/ksp-app-wallet-desk/src/bootstrap.rs
Normal file
135
crates/ksp-app-wallet-desk/src/bootstrap.rs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/bootstrap.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Config and Logging bootstrap for Wallet Desk.
|
||||||
|
|
||||||
|
/// 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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LoggingStartupPlan {
|
||||||
|
Managed {
|
||||||
|
active_profile_id: String,
|
||||||
|
settings: ksp_logging_lib::LoggingSettings,
|
||||||
|
},
|
||||||
|
Fallback {
|
||||||
|
initial_error: ksp_core_lib::Error,
|
||||||
|
diagnostic: crate::CommandErrorDto,
|
||||||
|
settings: ksp_logging_lib::LoggingSettings,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 the Logging runtime from Config, with the same bounded fallback policy as Config Desk.
|
||||||
|
pub(crate) fn initialize_logging(
|
||||||
|
management: &ksp_config_lib::ConfigManagement,
|
||||||
|
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||||
|
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||||
|
let plan = resolve_logging_startup(management);
|
||||||
|
return match plan {
|
||||||
|
LoggingStartupPlan::Managed { active_profile_id, settings } => {
|
||||||
|
let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity);
|
||||||
|
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 Wallet Desk logging from managed configuration");
|
||||||
|
std::result::Result::Ok(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),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LoggingStartupPlan::Fallback { initial_error, diagnostic, settings } => {
|
||||||
|
initialize_planned_fallback_logging(initial_error, diagnostic, settings, runtime_identity)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_logging_settings() -> ksp_logging_lib::LoggingSettings {
|
||||||
|
return ksp_logging_lib::LoggingSettings::new(
|
||||||
|
ksp_logging_lib::LogFilterLevel::Info,
|
||||||
|
ksp_logging_lib::SpanEvents::Off,
|
||||||
|
std::option::Option::Some(ksp_logging_lib::ConsoleSettings::stderr()),
|
||||||
|
std::vec::Vec::new(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_logging_startup(management: &ksp_config_lib::ConfigManagement) -> LoggingStartupPlan {
|
||||||
|
let environment = ksp_config_lib::ConfigEnvironment::load();
|
||||||
|
let environment = match environment {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return fallback_startup_plan(error),
|
||||||
|
};
|
||||||
|
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 fallback_startup_plan(error),
|
||||||
|
};
|
||||||
|
return LoggingStartupPlan::Managed { active_profile_id: resolved.profile_id().to_owned(), settings: resolved.into_settings() };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fallback_startup_plan(initial_error: ksp_core_lib::Error) -> LoggingStartupPlan {
|
||||||
|
let diagnostic = crate::CommandErrorDto::from_error(&initial_error);
|
||||||
|
return LoggingStartupPlan::Fallback { initial_error, diagnostic, settings: fallback_logging_settings() };
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return initialize_planned_fallback_logging(initial_error, diagnostic, fallback_logging_settings(), runtime_identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_planned_fallback_logging(
|
||||||
|
initial_error: ksp_core_lib::Error,
|
||||||
|
diagnostic: crate::CommandErrorDto,
|
||||||
|
settings: ksp_logging_lib::LoggingSettings,
|
||||||
|
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||||
|
) -> ksp_core_lib::Result<crate::LoggingStartup> {
|
||||||
|
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 Wallet 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(), "managed Logging configuration is unavailable; using transient in-memory fallback");
|
||||||
|
return std::result::Result::Ok(LoggingStartup {
|
||||||
|
guard,
|
||||||
|
active_profile_id: std::option::Option::None,
|
||||||
|
fallback_active: true,
|
||||||
|
startup_diagnostic: std::option::Option::Some(diagnostic),
|
||||||
|
});
|
||||||
|
}
|
||||||
21
crates/ksp-app-wallet-desk/src/constants.rs
Normal file
21
crates/ksp-app-wallet-desk/src/constants.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/constants.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Logging targets and domains owned by Wallet Desk.
|
||||||
|
|
||||||
|
/// Structured domain used while bootstrapping Config and Logging.
|
||||||
|
pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "wallet.bootstrap";
|
||||||
|
/// Structured domain used by technical frontend events.
|
||||||
|
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
|
||||||
|
/// Structured domain used by the Wallet Desk shell.
|
||||||
|
pub(crate) const TRACING_DOMAIN_SHELL: &str = "wallet.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 Wallet Desk.
|
||||||
|
pub(crate) const TRACING_TARGET: &str = "ksp-app-wallet-desk";
|
||||||
|
/// Owning target for generic frontend events emitted through the KSP bridge.
|
||||||
|
pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-wallet-desk.frontend";
|
||||||
|
/// Owning target for main-window frontend events.
|
||||||
|
pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-wallet-desk.frontend.main";
|
||||||
|
/// Owning target for splash-window frontend events.
|
||||||
|
pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-wallet-desk.frontend.splash";
|
||||||
54
crates/ksp-app-wallet-desk/src/dto_common.rs
Normal file
54
crates/ksp-app-wallet-desk/src/dto_common.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/dto_common.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Common Tauri DTOs shared by Wallet Desk shell commands.
|
||||||
|
|
||||||
|
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_wallet_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 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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initial application/runtime snapshot exposed to the Wallet Desk shell.
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_desk/dto_common/RuntimeStatusDto.ts")]
|
||||||
|
pub(crate) struct RuntimeStatusDto {
|
||||||
|
/// Cargo application version.
|
||||||
|
pub(crate) application_version: String,
|
||||||
|
/// Number of documents registered by Config before Wallet-specific documents are introduced.
|
||||||
|
pub(crate) config_document_count: u32,
|
||||||
|
/// Active configured Logging profile, or `None` while the transient fallback runtime is active.
|
||||||
|
pub(crate) active_logging_profile: std::option::Option<String>,
|
||||||
|
/// Whether Wallet Desk had to install its transient in-memory Logging fallback.
|
||||||
|
pub(crate) fallback_logging_active: bool,
|
||||||
|
/// Safe startup diagnostic that caused fallback Logging, when applicable.
|
||||||
|
pub(crate) startup_diagnostic: std::option::Option<CommandErrorDto>,
|
||||||
|
/// Current implementation phase exposed for the pre.002 shell.
|
||||||
|
pub(crate) shell_phase: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/dto_common.rs"]
|
||||||
|
mod tests;
|
||||||
26
crates/ksp-app-wallet-desk/src/errors.rs
Normal file
26
crates/ksp-app-wallet-desk/src/errors.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/errors.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Application-local error codes for the Wallet Desk shell.
|
||||||
|
|
||||||
|
/// Shared Wallet Desk application state is internally inconsistent.
|
||||||
|
pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("wallet_desk", "app_state_invalid");
|
||||||
|
/// Shared Wallet 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("wallet_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("wallet_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("wallet_desk", "frontend_log_target_invalid");
|
||||||
|
/// Wallet 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("wallet_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("wallet_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("wallet_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("wallet_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("wallet_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("wallet_desk", "tauri_window_operation_failed");
|
||||||
145
crates/ksp-app-wallet-desk/src/frontend_logging.rs
Normal file
145
crates/ksp-app-wallet-desk/src/frontend_logging.rs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/frontend_logging.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! KSP-owned bridge for technical log events emitted by Wallet Desk frontend scripts.
|
||||||
|
|
||||||
|
use ts_rs::TS; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// Log payload sent by Wallet Desk frontend scripts.
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_wallet_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 {
|
||||||
|
Debug,
|
||||||
|
Error,
|
||||||
|
Info,
|
||||||
|
Trace,
|
||||||
|
Warn,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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() {
|
||||||
|
"debug" => std::result::Result::Ok(FrontendLogLevel::Debug),
|
||||||
|
"error" => std::result::Result::Ok(FrontendLogLevel::Error),
|
||||||
|
"info" => std::result::Result::Ok(FrontendLogLevel::Info),
|
||||||
|
"trace" => std::result::Result::Ok(FrontendLogLevel::Trace),
|
||||||
|
"warn" => std::result::Result::Ok(FrontendLogLevel::Warn),
|
||||||
|
_ => 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::Debug => {
|
||||||
|
ksp_logging_lib::debug!(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}")
|
||||||
|
},
|
||||||
|
FrontendLogLevel::Info => {
|
||||||
|
ksp_logging_lib::info!(target: crate::TRACING_TARGET_FRONTEND, domain = crate::TRACING_DOMAIN_FRONTEND, action = "frontend_log", target_id = "frontend", "{message}")
|
||||||
|
},
|
||||||
|
FrontendLogLevel::Trace => {
|
||||||
|
ksp_logging_lib::trace!(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}")
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_main_target(level: FrontendLogLevel, message: &str) {
|
||||||
|
return match level {
|
||||||
|
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::Error => {
|
||||||
|
ksp_logging_lib::error!(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::Trace => {
|
||||||
|
ksp_logging_lib::trace!(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}")
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_splash_target(level: FrontendLogLevel, message: &str) {
|
||||||
|
return match level {
|
||||||
|
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::Error => {
|
||||||
|
ksp_logging_lib::error!(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::Trace => {
|
||||||
|
ksp_logging_lib::trace!(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}")
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/frontend_logging.rs"]
|
||||||
|
mod tests;
|
||||||
90
crates/ksp-app-wallet-desk/src/lib.rs
Normal file
90
crates/ksp-app-wallet-desk/src/lib.rs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/lib.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Tauri desktop application shell for KSP Wallet management and 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 wallet desktop application.
|
||||||
|
pub use self::tauri::run;
|
||||||
|
|
||||||
|
/// Shared Wallet 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 the Logging runtime from Config, with the same bounded fallback policy as Config Desk.
|
||||||
|
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 Wallet 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 Wallet 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;
|
||||||
|
/// Initial application/runtime snapshot exposed to the Wallet Desk shell.
|
||||||
|
pub(crate) use self::dto_common::RuntimeStatusDto;
|
||||||
|
/// Shared Wallet Desk application state is internally inconsistent.
|
||||||
|
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
|
||||||
|
/// Shared Wallet 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;
|
||||||
|
/// Wallet 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 Wallet 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 Wallet 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 Wallet 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-wallet-desk/src/logging_runtime.rs
Normal file
10
crates/ksp-app-wallet-desk/src/logging_runtime.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/logging_runtime.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Wallet Desk Logging runtime identity helpers.
|
||||||
|
|
||||||
|
/// Creates the stable runtime identity for this Wallet 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-wallet-desk/src/main.rs
Normal file
58
crates/ksp-app-wallet-desk/src/main.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/main.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Binary entry point for the KSP wallet 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 Wallet 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_wallet_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-wallet-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_wallet_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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
172
crates/ksp-app-wallet-desk/src/splash.rs
Normal file
172
crates/ksp-app-wallet-desk/src/splash.rs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/splash.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Common splash settings and frontend event contracts for Wallet Desk.
|
||||||
|
|
||||||
|
use ts_rs::TS; // rust-rules: trait-import
|
||||||
|
|
||||||
|
const DEFAULT_SPLASH_FADE_MS: u32 = 300;
|
||||||
|
const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200;
|
||||||
|
const ENV_SPLASH_FADE_MS: &str = "KSP_DESK_SPLASH_FADE_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 {
|
||||||
|
minimum_ms: u64,
|
||||||
|
fade_ms: u32,
|
||||||
|
minimum_source: ksp_config_lib::ConfigEnvironmentSource,
|
||||||
|
fade_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 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 = environment.resolve_variable(ENV_SPLASH_FADE_MS, std::option::Option::Some("300"));
|
||||||
|
let fade = match fade {
|
||||||
|
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_ms = parse_u32_setting(ENV_SPLASH_FADE_MS, fade.value(), MAX_SPLASH_FADE_MS);
|
||||||
|
let fade_ms = match fade_ms {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
return std::result::Result::Ok(Self { minimum_ms, fade_ms, minimum_source: minimum.source(), fade_source: fade.source() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns safe in-memory timings used when the managed environment cannot be resolved.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn fallback() -> Self {
|
||||||
|
return Self {
|
||||||
|
minimum_ms: DEFAULT_SPLASH_MINIMUM_MS,
|
||||||
|
fade_ms: DEFAULT_SPLASH_FADE_MS,
|
||||||
|
minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||||
|
fade_source: ksp_config_lib::ConfigEnvironmentSource::Fallback,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the minimum visible duration after splash frontend readiness.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn minimum_ms(self) -> u64 {
|
||||||
|
return self.minimum_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the fade duration used for both fade-in and fade-out.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn fade_ms(self) -> u32 {
|
||||||
|
return self.fade_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 duration.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn fade_source(self) -> &'static str {
|
||||||
|
return environment_source_code(self.fade_source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the minimum backend lifecycle duration from readiness until main activation.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 {
|
||||||
|
return self.minimum_ms + u64::from(self.fade_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_wallet_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 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>, duration_ms: std::option::Option<u32>) -> Self {
|
||||||
|
return Self { action: action.to_owned(), message: message.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;
|
||||||
87
crates/ksp-app-wallet-desk/src/tauri.rs
Normal file
87
crates/ksp-app-wallet-desk/src/tauri.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/tauri.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Tauri runtime assembly for the KSP wallet desktop application.
|
||||||
|
|
||||||
|
/// Runs the Wallet Desk application.
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
||||||
|
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(tauri::generate_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 Wallet 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_runtime_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, "Wallet Desk Tauri shell setup completed");
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(crate::CommandErrorDto::from_error(&error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::RuntimeStatusDto, crate::CommandErrorDto> {
|
||||||
|
let result = state.runtime_status();
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn splash_frontend_ready(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
window: tauri::WebviewWindow,
|
||||||
|
state: tauri::State<'_, crate::AppState>,
|
||||||
|
) -> std::result::Result<(), crate::CommandErrorDto> {
|
||||||
|
let result = crate::splash_frontend_ready_service(app, window, &state).await;
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)),
|
||||||
|
};
|
||||||
|
}
|
||||||
48
crates/ksp-app-wallet-desk/src/tw_main.rs
Normal file
48
crates/ksp-app-wallet-desk/src/tw_main.rs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/tw_main.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Tauri-window helpers for the Wallet Desk main window.
|
||||||
|
|
||||||
|
use tauri::Manager; // rust-rules: trait-import
|
||||||
|
|
||||||
|
/// Crate-internal main window label.
|
||||||
|
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, "Wallet Desk main window is missing")
|
||||||
|
.with_context("window_label", WINDOW_LABEL_MAIN),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows and focuses the main Wallet Desk 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 Wallet 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 Wallet 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(());
|
||||||
|
}
|
||||||
96
crates/ksp-app-wallet-desk/src/tw_splash.rs
Normal file
96
crates/ksp-app-wallet-desk/src/tw_splash.rs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/src/tw_splash.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Tauri-window lifecycle for the Wallet 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, "Wallet 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, minimum_ms = settings.minimum_ms(), minimum_source = settings.minimum_source(), fade_ms = settings.fade_ms(), fade_source = settings.fade_source(), expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(), "splash frontend readiness accepted; starting splash to main lifecycle");
|
||||||
|
let fade_in = emit_order(
|
||||||
|
&invoking_window,
|
||||||
|
crate::SplashOrderDto::new("fade_in", std::option::Option::Some("Initialisation de KSP Wallet Desk..."), std::option::Option::Some(settings.fade_ms())),
|
||||||
|
);
|
||||||
|
if let std::result::Result::Err(error) = fade_in {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let minimum_wait_started = std::time::Instant::now();
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(settings.minimum_ms())).await;
|
||||||
|
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, configured_wait_ms = settings.minimum_ms(), actual_wait_ms = minimum_wait_started.elapsed().as_secs_f64() * 1000.0, lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0, "splash minimum wait completed");
|
||||||
|
let fade_out = emit_order(
|
||||||
|
&invoking_window,
|
||||||
|
crate::SplashOrderDto::new("fade_out", std::option::Option::Some("Initialisation terminée."), std::option::Option::Some(settings.fade_ms())),
|
||||||
|
);
|
||||||
|
if let std::result::Result::Err(error) = fade_out {
|
||||||
|
return std::result::Result::Err(error);
|
||||||
|
}
|
||||||
|
let fade_wait_started = std::time::Instant::now();
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(u64::from(settings.fade_ms()))).await;
|
||||||
|
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, configured_wait_ms = settings.fade_ms(), actual_wait_ms = fade_wait_started.elapsed().as_secs_f64() * 1000.0, lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0, "splash fade-out wait completed");
|
||||||
|
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 Wallet 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_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 Wallet Desk splash order")
|
||||||
|
.with_context("window_label", WINDOW_LABEL_SPLASH)
|
||||||
|
.with_context("action", action)
|
||||||
|
.with_source(error),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
50
crates/ksp-app-wallet-desk/tauri.conf.json
Normal file
50
crates/ksp-app-wallet-desk/tauri.conf.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "KSP Wallet Desk",
|
||||||
|
"version": "0.2.6-pre.2",
|
||||||
|
"identifier": "com.sasedev.ksp-app-wallet-desk",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"devUrl": "http://localhost:1432",
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-wallet-desk/dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"label": "splash",
|
||||||
|
"url": "splash.html",
|
||||||
|
"title": "Chargement — KSP Wallet Desk",
|
||||||
|
"width": 960,
|
||||||
|
"height": 637,
|
||||||
|
"resizable": false,
|
||||||
|
"decorations": false,
|
||||||
|
"transparent": true,
|
||||||
|
"center": true,
|
||||||
|
"alwaysOnTop": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "main",
|
||||||
|
"url": "main.html",
|
||||||
|
"title": "KSP Wallet 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
85
crates/ksp-app-wallet-desk/tests/desktop_contract.rs
Normal file
85
crates/ksp-app-wallet-desk/tests/desktop_contract.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Desktop build and shell contract audits for Wallet Desk pre.002.
|
||||||
|
|
||||||
|
fn app_root() -> std::path::PathBuf {
|
||||||
|
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_text(path: &std::path::Path) -> String {
|
||||||
|
let source = std::fs::read_to_string(path);
|
||||||
|
assert!(source.is_ok(), "unable to read {}", path.display());
|
||||||
|
return match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => String::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_json(path: &std::path::Path) -> serde_json::Value {
|
||||||
|
let source = read_text(path);
|
||||||
|
let parsed = serde_json::from_str::<serde_json::Value>(source.as_str());
|
||||||
|
assert!(parsed.is_ok(), "unable to parse {}", path.display());
|
||||||
|
return match parsed {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => serde_json::Value::Null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tauri_shell_uses_reserved_wallet_desk_ports_and_template_windows() {
|
||||||
|
let root = app_root();
|
||||||
|
let tauri = read_json(root.join("tauri.conf.json").as_path());
|
||||||
|
assert_eq!(tauri.pointer("/build/devUrl").and_then(serde_json::Value::as_str), std::option::Option::Some("http://localhost:1432"));
|
||||||
|
assert_eq!(tauri.pointer("/build/beforeDevCommand").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run dev"));
|
||||||
|
assert_eq!(tauri.pointer("/build/beforeBuildCommand").and_then(serde_json::Value::as_str), std::option::Option::Some("npm run build"));
|
||||||
|
assert_eq!(tauri.pointer("/app/security/csp"), std::option::Option::Some(&serde_json::Value::Null));
|
||||||
|
let windows = tauri.pointer("/app/windows").and_then(serde_json::Value::as_array);
|
||||||
|
assert!(windows.is_some());
|
||||||
|
let windows = match windows {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
let labels = windows.iter().filter_map(|window| return window.get("label").and_then(serde_json::Value::as_str)).collect::<std::vec::Vec<_>>();
|
||||||
|
assert_eq!(labels, ["splash", "main"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn package_reuses_config_desk_frontend_baseline_without_standalone_check_script() {
|
||||||
|
let root = app_root();
|
||||||
|
let package = read_json(root.join("package.json").as_path());
|
||||||
|
assert!(package.pointer("/scripts/check").is_none());
|
||||||
|
let dependencies = package.get("dependencies").and_then(serde_json::Value::as_object);
|
||||||
|
assert!(dependencies.is_some());
|
||||||
|
let dependencies = match dependencies {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => return,
|
||||||
|
};
|
||||||
|
for dependency in [
|
||||||
|
"@fltsci/tauri-plugin-tracing",
|
||||||
|
"@fortawesome/fontawesome-free",
|
||||||
|
"@tauri-apps/api",
|
||||||
|
"bootstrap",
|
||||||
|
"datatables.net-bs5",
|
||||||
|
"datatables.net-select-bs5",
|
||||||
|
"resize-observer-polyfill",
|
||||||
|
"simplebar",
|
||||||
|
] {
|
||||||
|
assert!(dependencies.contains_key(dependency), "missing dependency {dependency}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_contains_wallet_navigation_datatable_and_lock_state_icons() {
|
||||||
|
let root = app_root();
|
||||||
|
let html = read_text(root.join("frontend/main.html").as_path());
|
||||||
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
assert!(html.contains("id=\"walletInventoryTable\""));
|
||||||
|
assert!(html.contains("fa-lock"));
|
||||||
|
assert!(html.contains("fa-lock-open"));
|
||||||
|
assert!(html.contains("data-view=\"wallets\""));
|
||||||
|
assert!(main.contains("new DataTable"));
|
||||||
|
assert!(main.contains("datatables.net-select-bs5"));
|
||||||
|
assert!(main.contains("ResizeObserver"));
|
||||||
|
assert!(main.contains("simplebar"));
|
||||||
|
}
|
||||||
38
crates/ksp-app-wallet-desk/tests/desktop_security.rs
Normal file
38
crates/ksp-app-wallet-desk/tests/desktop_security.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
|
||||||
|
|
||||||
|
fn app_root() -> std::path::PathBuf {
|
||||||
|
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_text(path: &std::path::Path) -> String {
|
||||||
|
let source = std::fs::read_to_string(path);
|
||||||
|
assert!(source.is_ok(), "unable to read {}", path.display());
|
||||||
|
return match source {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => String::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn initial_capability_surface_is_core_plus_tracing_only() {
|
||||||
|
let root = app_root();
|
||||||
|
let capability = read_text(root.join("capabilities/default.json").as_path());
|
||||||
|
assert!(capability.contains("\"core:default\""));
|
||||||
|
assert!(capability.contains("\"tracing:default\""));
|
||||||
|
assert!(!capability.contains("dialog:"));
|
||||||
|
assert!(!capability.contains("fs:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frontend_shell_uses_no_browser_native_dialog_or_secret_storage() {
|
||||||
|
let root = app_root();
|
||||||
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
||||||
|
let html = read_text(root.join("frontend/main.html").as_path());
|
||||||
|
for forbidden in ["window.alert", "window.confirm", "window.prompt", "localStorage", "sessionStorage"] {
|
||||||
|
assert!(!main.contains(forbidden), "forbidden frontend primitive {forbidden}");
|
||||||
|
assert!(!html.contains(forbidden), "forbidden frontend primitive {forbidden}");
|
||||||
|
}
|
||||||
|
}
|
||||||
14
crates/ksp-app-wallet-desk/tests/public_api.rs
Normal file
14
crates/ksp-app-wallet-desk/tests/public_api.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/tests/public_api.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Public API checks for the KSP wallet desktop application library.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_entry_point_is_available_from_crate_root() {
|
||||||
|
let run_entry_point: fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()> = ksp_app_wallet_desk_lib::run;
|
||||||
|
assert_eq!(std::mem::size_of_val(&run_entry_point), std::mem::size_of::<fn(&[std::ffi::OsString]) -> ksp_core_lib::Result<()>>());
|
||||||
|
}
|
||||||
31
crates/ksp-app-wallet-desk/tsconfig.json
Normal file
31
crates/ksp-app-wallet-desk/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"types": [
|
||||||
|
"vite/client",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"frontend",
|
||||||
|
"vite.config.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
11
crates/ksp-app-wallet-desk/unit_tests/dto_common.rs
Normal file
11
crates/ksp-app-wallet-desk/unit_tests/dto_common.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/unit_tests/dto_common.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn command_error_projection_excludes_context_values() {
|
||||||
|
let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failure"), "safe message").with_context("secret_canary", "must-not-be-exported");
|
||||||
|
let dto = crate::CommandErrorDto::from_error(&error);
|
||||||
|
assert_eq!(dto.domain, "test");
|
||||||
|
assert_eq!(dto.code, "failure");
|
||||||
|
assert_eq!(dto.message, "safe message");
|
||||||
|
}
|
||||||
40
crates/ksp-app-wallet-desk/unit_tests/frontend_logging.rs
Normal file
40
crates/ksp-app-wallet-desk/unit_tests/frontend_logging.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// file: crates/ksp-app-wallet-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 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 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 arbitrary_frontend_log_target_is_rejected() {
|
||||||
|
let result = super::parse_target("ksp-app-wallet-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);
|
||||||
|
}
|
||||||
37
crates/ksp-app-wallet-desk/unit_tests/splash.rs
Normal file
37
crates/ksp-app-wallet-desk/unit_tests/splash.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// file: crates/ksp-app-wallet-desk/unit_tests/splash.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fallback_splash_settings_are_short_and_ordered() {
|
||||||
|
let settings = crate::SplashSettings::fallback();
|
||||||
|
assert_eq!(settings.minimum_ms(), 1200);
|
||||||
|
assert_eq!(settings.fade_ms(), 300);
|
||||||
|
assert_eq!(settings.minimum_source(), "fallback");
|
||||||
|
assert_eq!(settings.fade_source(), "fallback");
|
||||||
|
assert_eq!(settings.expected_backend_lifecycle_ms(), 1500);
|
||||||
|
assert!(settings.minimum_ms() >= u64::from(settings.fade_ms()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn splash_order_contract_keeps_action_message_and_duration() {
|
||||||
|
let order = crate::SplashOrderDto::new("fade_out", std::option::Option::Some("done"), std::option::Option::Some(300));
|
||||||
|
assert_eq!(order.action, "fade_out");
|
||||||
|
assert_eq!(order.message.as_deref(), std::option::Option::Some("done"));
|
||||||
|
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 = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_MS", "10001", 10_000);
|
||||||
|
assert!(malformed.is_err());
|
||||||
|
assert!(oversized_minimum.is_err());
|
||||||
|
assert!(oversized_fade.is_err());
|
||||||
|
if let std::result::Result::Err(error) = oversized_minimum {
|
||||||
|
assert!(error.context().iter().any(|field| return field.key() == "maximum_ms" && field.value() == "60000"));
|
||||||
|
}
|
||||||
|
if let std::result::Result::Err(error) = oversized_fade {
|
||||||
|
assert!(error.context().iter().any(|field| return field.key() == "maximum_ms" && field.value() == "10000"));
|
||||||
|
}
|
||||||
|
}
|
||||||
72
crates/ksp-app-wallet-desk/vite.config.ts
Normal file
72
crates/ksp-app-wallet-desk/vite.config.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// file: crates/ksp-app-wallet-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-wallet-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][extname]";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
quietDeps: true,
|
||||||
|
silenceDeprecations: ["import", "color-functions", "global-builtin"],
|
||||||
|
verbose: false,
|
||||||
|
importers: [new NodePackageImporter()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 1432,
|
||||||
|
strictPort: true,
|
||||||
|
host: devHost || false,
|
||||||
|
ws: {
|
||||||
|
protocol: "ws",
|
||||||
|
host: devHost || "localhost",
|
||||||
|
port: 1433,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
ignored: ["**/src/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
202
deltas/0.2.6/pre.002.md
Normal file
202
deltas/0.2.6/pre.002.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<!-- file: deltas/0.2.6/pre.002.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.2.6-pre.002` — crate Tauri et shell Wallet Desk
|
||||||
|
|
||||||
|
## Base requise
|
||||||
|
|
||||||
|
```text
|
||||||
|
0.2.6-pre.001-fix.002 appliquée
|
||||||
|
workspace.package.version = 0.2.6-pre.1
|
||||||
|
```
|
||||||
|
|
||||||
|
Les validations opérateur de `pre.001` sont vertes : `cargo fmt --all`, audit structurel Python, `cargo check --workspace` et `cargo clippy --workspace --all-targets`.
|
||||||
|
|
||||||
|
## Type de livraison
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-general-0.2.6-pre.002.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette tranche est technique : création de crate, Rust/Tauri, frontend et configuration de build. Elle incrémente donc `workspace.package.version` vers `0.2.6-pre.2`.
|
||||||
|
|
||||||
|
## Objet
|
||||||
|
|
||||||
|
Matérialiser la première application `ksp-app-wallet-desk` sans commencer les fonctionnalités Wallet métier prévues à partir de `pre.003`/`pre.004`.
|
||||||
|
|
||||||
|
Le delta fournit :
|
||||||
|
|
||||||
|
```text
|
||||||
|
crate Rust mixte lib + bin
|
||||||
|
single-instance launcher
|
||||||
|
normalisation CWD debug workspace
|
||||||
|
bootstrap Config + Logging commun
|
||||||
|
fallback Logging borné
|
||||||
|
splash + main window
|
||||||
|
bridge frontend -> Rust -> ksp-logging-lib
|
||||||
|
DTO TS-RS foundation
|
||||||
|
ports Vite/HMR 1432/1433
|
||||||
|
Bootstrap
|
||||||
|
Font Awesome
|
||||||
|
DataTables Bootstrap 5 + Select
|
||||||
|
SimpleBar
|
||||||
|
resize-observer-polyfill
|
||||||
|
shell Dashboard/Wallets/Create-Import/Details/Security/Diagnostics
|
||||||
|
DataTable Wallet réelle mais volontairement vide en pre.002
|
||||||
|
capabilities core:default + tracing:default
|
||||||
|
CSP null comme Config Desk
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontières conservées
|
||||||
|
|
||||||
|
`pre.002` ne dépend pas encore de `ksp-wallet-lib` ni de `ksp-onchain-transport-lib`. La seule composition KSP active dans la nouvelle app est celle nécessaire au gabarit desktop :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-app-wallet-desk
|
||||||
|
-> ksp-config-lib
|
||||||
|
-> ksp-core-lib
|
||||||
|
-> ksp-logging-lib
|
||||||
|
-> Tauri
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune logique `.kspwallet`, aucun inventory filesystem, aucun secret Wallet et aucun RPC ne sont simulés dans le frontend.
|
||||||
|
|
||||||
|
## Frontend et npm
|
||||||
|
|
||||||
|
Le `package.json` reprend la baseline Config Desk :
|
||||||
|
|
||||||
|
```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
|
||||||
|
datatables.net-select-bs5 ^4.0
|
||||||
|
resize-observer-polyfill ^1.5
|
||||||
|
simplebar ^6.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Les outils restent dans `devDependencies`. Aucun script `check` standalone n'est créé. Les scripts `dev` et `build` existent uniquement parce que Tauri les appelle via `beforeDevCommand` et `beforeBuildCommand`.
|
||||||
|
|
||||||
|
Lors de l'application de ce delta, l'installation initiale des packages se fait depuis la crate avec :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd crates/ksp-app-wallet-desk
|
||||||
|
npm i -D
|
||||||
|
cd ../..
|
||||||
|
```
|
||||||
|
|
||||||
|
Le lockfile npm et `node_modules` restent non versionnés.
|
||||||
|
|
||||||
|
## Dépendances auditées
|
||||||
|
|
||||||
|
Au `2026-08-20`, les versions actuelles vérifiées incluent :
|
||||||
|
|
||||||
|
```text
|
||||||
|
@tauri-apps/api 2.11.1
|
||||||
|
@tauri-apps/cli 2.11.4
|
||||||
|
@fortawesome/fontawesome-free 7.3.1
|
||||||
|
datatables.net-bs5 3.0.1
|
||||||
|
datatables.net-select-bs5 4.0.0
|
||||||
|
resize-observer-polyfill 1.5.1
|
||||||
|
simplebar 6.3.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Les ranges du gabarit couvrent ces versions sans changement de génération.
|
||||||
|
|
||||||
|
## Shell et logging
|
||||||
|
|
||||||
|
Le splash reprend les paramètres Config-owned existants :
|
||||||
|
|
||||||
|
```text
|
||||||
|
KSP_DESK_SPLASH_MINIMUM_MS
|
||||||
|
KSP_DESK_SPLASH_FADE_MS
|
||||||
|
```
|
||||||
|
|
||||||
|
Le frontend instrumente :
|
||||||
|
|
||||||
|
```text
|
||||||
|
chargement main/splash
|
||||||
|
navigation
|
||||||
|
mise à jour DOM des vues
|
||||||
|
initialisation DataTables
|
||||||
|
clics d'actions shell
|
||||||
|
IPC runtime status
|
||||||
|
ordres/animations splash
|
||||||
|
```
|
||||||
|
|
||||||
|
Les targets frontend sont whitelisted (`frontend`, `main`, `splash`) avant réémission via `ksp-logging-lib`.
|
||||||
|
|
||||||
|
## Tests ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
public API run()
|
||||||
|
contrat Tauri ports/windows/beforeDev/beforeBuild/CSP
|
||||||
|
baseline dependencies et absence de script npm check
|
||||||
|
présence DataTables/Select/SimpleBar/ResizeObserver/Font Awesome
|
||||||
|
capabilities initiales core + tracing seulement
|
||||||
|
absence window.alert/confirm/prompt/localStorage/sessionStorage
|
||||||
|
DTO CommandError sans context arbitraire
|
||||||
|
whitelist frontend logging
|
||||||
|
contrats splash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fichiers principaux ajoutés
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/ksp-app-wallet-desk/Cargo.toml
|
||||||
|
crates/ksp-app-wallet-desk/build.rs
|
||||||
|
crates/ksp-app-wallet-desk/capabilities/default.json
|
||||||
|
crates/ksp-app-wallet-desk/package.json
|
||||||
|
crates/ksp-app-wallet-desk/tauri.conf.json
|
||||||
|
crates/ksp-app-wallet-desk/tsconfig.json
|
||||||
|
crates/ksp-app-wallet-desk/vite.config.ts
|
||||||
|
crates/ksp-app-wallet-desk/src/**
|
||||||
|
crates/ksp-app-wallet-desk/frontend/**
|
||||||
|
crates/ksp-app-wallet-desk/tests/**
|
||||||
|
crates/ksp-app-wallet-desk/unit_tests/**
|
||||||
|
```
|
||||||
|
|
||||||
|
Les assets fonts/logo/splash/favicon sont repris du gabarit Config Desk.
|
||||||
|
|
||||||
|
## Fichiers modifiés hors crate
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cargo.toml
|
||||||
|
ROADMAP.md
|
||||||
|
docs/000-README.md
|
||||||
|
docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version technique
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.2.6-pre.2
|
||||||
|
commit = v0.2.6-pre.002
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucun tag stable.
|
||||||
|
|
||||||
|
## Validation opérateur requise
|
||||||
|
|
||||||
|
Après application :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd crates/ksp-app-wallet-desk
|
||||||
|
npm i -D
|
||||||
|
cd ../..
|
||||||
|
cargo fmt --all
|
||||||
|
python3 scripts/audit_rust_workspace_rules.py
|
||||||
|
cargo check --workspace
|
||||||
|
cargo clippy --workspace --all-targets
|
||||||
|
cargo test -p ksp-app-wallet-desk
|
||||||
|
cargo tauri dev -c crates/ksp-app-wallet-desk/tauri.conf.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Le parcours `cargo tauri dev` doit vérifier visuellement et dans les logs : splash, ouverture de la fenêtre principale, navigation des six vues, DataTable vide fonctionnelle, icônes Font Awesome, scrolling SimpleBar et bridge frontend Logging.
|
||||||
|
|
||||||
|
`cargo tauri build -c crates/ksp-app-wallet-desk/tauri.conf.json` **n'est pas exécuté en `pre.002`** : il reste réservé à la toute dernière validation de la release conformément à `KSP-APP-034`.
|
||||||
|
|
||||||
|
## Suite
|
||||||
|
|
||||||
|
`pre.003` ajoute `cfg.std.wallet`, le composite Wallet Desk, `wallets_directory` global, `wallets_subdirectory` par profil et la préparation/création du répertoire effectif.
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md -->
|
<!-- file: docs/plans/013-V0_2_6_WALLET_DESK_PLAN.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Plan `0.2.6` — Wallet Desk
|
# Plan `0.2.6` — Wallet Desk
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ La base auditée est la release stable fournie `v0.2.5`, avec `workspace.package
|
|||||||
|
|
||||||
Les validations opérateur du `2026-08-20` ont ensuite confirmé la base `pre.001` : `cargo fmt --all`, `python3 scripts/audit_rust_workspace_rules.py`, `cargo check --workspace` et `cargo clippy --workspace --all-targets` sont verts, sans warning signalé. Le présent plan intègre les corrections documentaires de `pre.001-fix.001` puis `pre.001-fix.002` sans modifier la version Cargo, car aucun fichier code/build/runtime/configuration exécutable n'est touché. `pre.001-fix.002` aligne en particulier le workflow frontend sur la règle KSP : npm direct sert uniquement à installer ou mettre à jour les dépendances ; les cycles dev/build passent exclusivement par Cargo/Tauri.
|
Les validations opérateur du `2026-08-20` ont ensuite confirmé la base `pre.001` : `cargo fmt --all`, `python3 scripts/audit_rust_workspace_rules.py`, `cargo check --workspace` et `cargo clippy --workspace --all-targets` sont verts, sans warning signalé. Le présent plan intègre les corrections documentaires de `pre.001-fix.001` puis `pre.001-fix.002` sans modifier la version Cargo, car aucun fichier code/build/runtime/configuration exécutable n'est touché. `pre.001-fix.002` aligne en particulier le workflow frontend sur la règle KSP : npm direct sert uniquement à installer ou mettre à jour les dépendances ; les cycles dev/build passent exclusivement par Cargo/Tauri.
|
||||||
|
|
||||||
|
`0.2.6-pre.002` matérialise ensuite la première tranche technique : la crate `ksp-app-wallet-desk` rejoint le workspace avec le shell splash/main du gabarit Config Desk, le bootstrap Config + Logging commun, les ports `1432/1433`, Bootstrap, Font Awesome, DataTables/Select, SimpleBar, `resize-observer-polyfill`, TS-RS et le bridge frontend -> Rust -> `ksp-logging-lib`. Cette tranche ne branche encore ni `std.wallet`, ni inventory filesystem réel, ni `ksp-wallet-lib`, ni Transport : ces responsabilités restent dans les prereleases déjà dimensionnées.
|
||||||
|
|
||||||
## 2. Sources relues et hiérarchie appliquée
|
## 2. Sources relues et hiérarchie appliquée
|
||||||
|
|
||||||
L'audit suit l'ordre d'autorité demandé :
|
L'audit suit l'ordre d'autorité demandé :
|
||||||
@@ -1218,7 +1220,9 @@ Cette tranche se termine par le présent plan/delta et les validations opérateu
|
|||||||
|
|
||||||
### `pre.002` — crate Tauri et shell complet du gabarit
|
### `pre.002` — crate Tauri et shell complet du gabarit
|
||||||
|
|
||||||
Objectifs :
|
État : **matérialisé dans le delta `0.2.6-pre.002`**, sous réserve des validations opérateur Cargo/Tauri.
|
||||||
|
|
||||||
|
Objectifs/livrables :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
crates/ksp-app-wallet-desk
|
crates/ksp-app-wallet-desk
|
||||||
|
|||||||
Reference in New Issue
Block a user