diff --git a/Cargo.toml b/Cargo.toml index a93bc86..c54fcc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 416 +# version: 417 [workspace] resolver = "3" -members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] +members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.3.7-pre.1" +version = "0.3.7-pre.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-app-backfill-desk/Cargo.toml b/crates/ksp-app-backfill-desk/Cargo.toml new file mode 100644 index 0000000..b55f071 --- /dev/null +++ b/crates/ksp-app-backfill-desk/Cargo.toml @@ -0,0 +1,40 @@ +# file: crates/ksp-app-backfill-desk/Cargo.toml +# version: 1 + +[package] +name = "ksp-app-backfill-desk" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +publish.workspace = true + +[lib] +name = "ksp_app_backfill_desk_lib" +path = "src/lib.rs" +crate-type = ["staticlib", "cdylib", "rlib"] + +[[bin]] +name = "ksp-app-backfill-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 diff --git a/crates/ksp-app-backfill-desk/build.rs b/crates/ksp-app-backfill-desk/build.rs new file mode 100644 index 0000000..6916657 --- /dev/null +++ b/crates/ksp-app-backfill-desk/build.rs @@ -0,0 +1,12 @@ +// file: crates/ksp-app-backfill-desk/build.rs +// version: 1 + +//! Build script for the KSP Backfill desktop application. + +#![forbid(unsafe_code)] +#![deny(unreachable_pub)] +#![warn(missing_docs)] + +fn main() { + tauri_build::build() +} diff --git a/crates/ksp-app-backfill-desk/capabilities/default.json b/crates/ksp-app-backfill-desk/capabilities/default.json new file mode 100644 index 0000000..f44cb76 --- /dev/null +++ b/crates/ksp-app-backfill-desk/capabilities/default.json @@ -0,0 +1,13 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default capability for KSP Backfill Desk", + "windows": [ + "splash", + "main" + ], + "permissions": [ + "core:default", + "tracing:default" + ] +} diff --git a/crates/ksp-app-backfill-desk/frontend/fonts/DOS_Amazigh.ttf b/crates/ksp-app-backfill-desk/frontend/fonts/DOS_Amazigh.ttf new file mode 100644 index 0000000..e558a29 Binary files /dev/null and b/crates/ksp-app-backfill-desk/frontend/fonts/DOS_Amazigh.ttf differ diff --git a/crates/ksp-app-backfill-desk/frontend/fonts/README.md b/crates/ksp-app-backfill-desk/frontend/fonts/README.md new file mode 100644 index 0000000..d5b6078 --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/fonts/README.md @@ -0,0 +1,25 @@ + + + +# Fonts du splash Backfill Desk + +Backfill Desk réutilise le gabarit de splash KSP déjà stabilisé dans Config Desk et Wallet Desk et conserve la police locale : + +```text +DOS_Amazigh.ttf +font-family: Dos Amazigh +``` + +Destination : + +```text +crates/ksp-app-backfill-desk/frontend/fonts/DOS_Amazigh.ttf +``` + +Empreinte SHA-256 de l'asset repris du gabarit : + +```text +a01ff4b5a0d699db7cefcf5336bf0e379ff6fbf0d096087571a13c8782f9aa02 +``` + +`splash.scss` déclare `Dos Amazigh` avec `@font-face` et l'applique au titre `#app-name`. diff --git a/crates/ksp-app-backfill-desk/frontend/imgs/logo.png b/crates/ksp-app-backfill-desk/frontend/imgs/logo.png new file mode 100644 index 0000000..94e89ca Binary files /dev/null and b/crates/ksp-app-backfill-desk/frontend/imgs/logo.png differ diff --git a/crates/ksp-app-backfill-desk/frontend/imgs/splash.png b/crates/ksp-app-backfill-desk/frontend/imgs/splash.png new file mode 100644 index 0000000..a27d1eb Binary files /dev/null and b/crates/ksp-app-backfill-desk/frontend/imgs/splash.png differ diff --git a/crates/ksp-app-backfill-desk/frontend/main.html b/crates/ksp-app-backfill-desk/frontend/main.html new file mode 100644 index 0000000..f78993c --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/main.html @@ -0,0 +1,101 @@ + + + + + + + + + + + Backfill Desk — Backfill + + + +
+ +
+ +
+
+ +
+
+
+
+
+
+
+

Backfill

+

Scaffold desktop prêt. Les contrôles Backfill seront branchés dans les tranches runtime suivantes.

+
+ +
+
+ +
+

Backfill runtime non ouvert dans pre.002

+

Aucun Transport, Store ou Job Backfill n'est encore créé par le frontend.

+
+
+
+ +
+
+
+
+
+
+ + + + + + diff --git a/crates/ksp-app-backfill-desk/frontend/sass/_app.scss b/crates/ksp-app-backfill-desk/frontend/sass/_app.scss new file mode 100644 index 0000000..f8f60d4 --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/sass/_app.scss @@ -0,0 +1,113 @@ +// file: crates/ksp-app-backfill-desk/frontend/sass/_app.scss +// version: 1 + +$app-header-height: 72px; +$app-footer-height: 42px; +$app-sidebar-width: 250px; + +html, +body { + width: 100%; + height: 100%; +} + +body { + margin: 0; + overflow: hidden; + background: $gray-100; +} + +.app-header { + position: fixed; + inset: 0 0 auto; + height: $app-header-height; + z-index: 1020; + border-bottom: 2px solid rgba($primary, 0.3); +} + +.app-footer { + position: fixed; + inset: auto 0 0; + height: $app-footer-height; + z-index: 1020; + border-top: 2px solid rgba($primary, 0.3); +} + +.app-main { + position: relative; + height: calc(100vh - $app-header-height - $app-footer-height); + margin-top: $app-header-height; + margin-bottom: $app-footer-height; + overflow: hidden; +} + +.app-main > .row { + flex-wrap: nowrap; + min-width: 0; +} + +.app-logo { + display: block; + width: auto; + height: 42px; +} + +.app-scrollable { + height: 100%; + max-height: 100%; +} + +.app-shell-card { + max-width: 1320px; +} + +.app-runtime-list dd { + 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-content { + min-width: 0; +} + +.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; +} + +@media (max-width: 991.98px) { + .app-sidebar { + width: 190px; + min-width: 190px; + max-width: 190px; + } +} diff --git a/crates/ksp-app-backfill-desk/frontend/sass/_bootswatch.scss b/crates/ksp-app-backfill-desk/frontend/sass/_bootswatch.scss new file mode 100644 index 0000000..647c7c0 --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/sass/_bootswatch.scss @@ -0,0 +1,160 @@ +// file: crates/ksp-app-backfill-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; + } + } +} diff --git a/crates/ksp-app-backfill-desk/frontend/sass/_fontawesome.scss b/crates/ksp-app-backfill-desk/frontend/sass/_fontawesome.scss new file mode 100644 index 0000000..978c638 --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/sass/_fontawesome.scss @@ -0,0 +1,19 @@ +// file: crates/ksp-app-backfill-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; diff --git a/crates/ksp-app-backfill-desk/frontend/sass/_simplebar.scss b/crates/ksp-app-backfill-desk/frontend/sass/_simplebar.scss new file mode 100644 index 0000000..19c64b1 --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/sass/_simplebar.scss @@ -0,0 +1,248 @@ +// file: crates/ksp-app-backfill-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; +} diff --git a/crates/ksp-app-backfill-desk/frontend/sass/_variables.scss b/crates/ksp-app-backfill-desk/frontend/sass/_variables.scss new file mode 100644 index 0000000..04b3685 --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/sass/_variables.scss @@ -0,0 +1,95 @@ +// file: crates/ksp-app-backfill-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; diff --git a/crates/ksp-app-backfill-desk/frontend/sass/main.scss b/crates/ksp-app-backfill-desk/frontend/sass/main.scss new file mode 100644 index 0000000..b1ba60a --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/sass/main.scss @@ -0,0 +1,10 @@ +// file: crates/ksp-app-backfill-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"; diff --git a/crates/ksp-app-backfill-desk/frontend/sass/splash.scss b/crates/ksp-app-backfill-desk/frontend/sass/splash.scss new file mode 100644 index 0000000..3fab8d8 --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/sass/splash.scss @@ -0,0 +1,137 @@ +// file: crates/ksp-app-backfill-desk/frontend/sass/splash.scss +// version: 1 + +@font-face { + font-family: "Dos Amazigh"; + src: url("../fonts/DOS_Amazigh.ttf") format("truetype"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + overflow: hidden; +} + +body { + display: flex; + align-items: center; + justify-content: center; + background: transparent; + font-family: Arial, sans-serif; +} + +#splash-container { + position: relative; + width: 960px; + max-width: 100%; + height: 637px; + max-height: 100%; + overflow: hidden; + opacity: 0; + will-change: opacity; +} + +#splash-image { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + z-index: 1; + object-fit: contain; + user-select: none; + -webkit-user-select: none; + -webkit-user-drag: none; + pointer-events: none; +} + +#app-name { + position: absolute; + top: 50%; + left: 50%; + z-index: 2; + transform: translate(-50%, -50%); + color: #fff; + font-family: "Dos Amazigh", sans-serif; + font-size: 76px; + font-weight: bold; + text-align: center; + text-shadow: 0 0 10px rgba(0, 0, 0, 0.5); + white-space: nowrap; + user-select: none; + -webkit-user-select: none; + -webkit-user-drag: none; + pointer-events: none; +} + +#debug-info { + position: absolute; + top: 0; + left: 0; + z-index: 3; + width: 100%; + max-height: 30%; + overflow-y: auto; + padding: 8px; + color: #fff; + background: rgba(0, 0, 0, 0.18); + font-family: monospace; + font-size: 12px; + scrollbar-width: none; +} + +#messages-container { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 3; + max-height: 30%; + overflow-y: auto; + padding: 10px 10px 36px; + background: rgba(0, 0, 0, 0.18); + font-family: Arial, sans-serif; + font-size: 14px; + scrollbar-width: none; +} + +#debug-info::-webkit-scrollbar, +#messages-container::-webkit-scrollbar { + display: none; +} + +.splash-message { + margin-bottom: 8px; + padding: 6px 10px; + border-radius: 4px; + color: #fff; + font-weight: bold; + animation: splashMessageFadeIn 0.3s ease-in-out; +} + +.splash-message.info { background: rgba(33, 150, 243, 0.22); } +.splash-message.warning { background: rgba(255, 152, 0, 0.22); } +.splash-message.error { background: rgba(244, 67, 54, 0.22); } +.splash-message.success { background: rgba(76, 175, 80, 0.22); } + +@keyframes splashMessageFadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/crates/ksp-app-backfill-desk/frontend/splash.html b/crates/ksp-app-backfill-desk/frontend/splash.html new file mode 100644 index 0000000..2b9258c --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/splash.html @@ -0,0 +1,24 @@ + + + + + + + + + + + KSP Backfill Desk — Chargement + + + +
+ Chargement de KSP Backfill Desk +
Backfill Desk
+ +
+
+ + + + diff --git a/crates/ksp-app-backfill-desk/frontend/ts/frontend_log.ts b/crates/ksp-app-backfill-desk/frontend/ts/frontend_log.ts new file mode 100644 index 0000000..fe1ede5 --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/ts/frontend_log.ts @@ -0,0 +1,110 @@ +// file: crates/ksp-app-backfill-desk/frontend/ts/frontend_log.ts +// version: 1 + +import { invoke } from "@tauri-apps/api/core"; +import type { FrontendLogPayloadDto } from "./bindings/ksp_app_backfill_desk/frontend_logging/FrontendLogPayloadDto.ts"; + +export type FrontendLogLevel = "trace" | "debug" | "info" | "warn" | "error"; +export type FrontendLogTargetId = "frontend" | "main" | "splash"; + +type ConsoleMethod = (...items: unknown[]) => void; + +const originalConsole = { + trace: console.trace.bind(console), + debug: console.debug.bind(console), + log: console.log.bind(console), + info: console.info.bind(console), + warn: console.warn.bind(console), + error: console.error.bind(console), +}; + +function stringifyItem(item: unknown): string { + if (item instanceof Error) { + return item.stack ?? item.message; + } + if (typeof item === "string") { + return item; + } + try { + const serialized = JSON.stringify(item); + return serialized ?? String(item); + } catch { + return String(item); + } +} + +function formatMessage(items: unknown[]): string { + return items.map(item => stringifyItem(item)).join(" "); +} + +async function sendFrontendLog(level: FrontendLogLevel, targetId: FrontendLogTargetId, message: string): Promise { + 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 { + 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); +} diff --git a/crates/ksp-app-backfill-desk/frontend/ts/invoke.ts b/crates/ksp-app-backfill-desk/frontend/ts/invoke.ts new file mode 100644 index 0000000..44aa600 --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/ts/invoke.ts @@ -0,0 +1,17 @@ +// file: crates/ksp-app-backfill-desk/frontend/ts/invoke.ts +// version: 1 + +import { invoke } from "@tauri-apps/api/core"; +import { frontendDebug, frontendError, frontendTrace, type FrontendLogTargetId } from "./frontend_log"; + +export async function invokeKsp(targetId: FrontendLogTargetId, command: string, args?: Record): Promise { + frontendDebug(targetId, "Frontend IPC command requested", { command }); + try { + const result = await invoke(command, args); + frontendTrace(targetId, "Frontend IPC command completed", { command }); + return result; + } catch (caughtError) { + frontendError(targetId, "Frontend IPC command failed", { command }); + throw caughtError; + } +} diff --git a/crates/ksp-app-backfill-desk/frontend/ts/main.ts b/crates/ksp-app-backfill-desk/frontend/ts/main.ts new file mode 100644 index 0000000..7763899 --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/ts/main.ts @@ -0,0 +1,144 @@ +// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts +// version: 1 + +import "bootstrap"; +import ResizeObserver from "resize-observer-polyfill"; +import "simplebar"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import type { ShellStatusDto } from "./bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts"; +import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log"; +import { invokeKsp } from "./invoke"; + +(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver; +installFrontendConsoleBridge("main"); + +type ViewId = "backfill" | "diagnostics"; + +const viewTitles: Record = { + backfill: "Backfill", + diagnostics: "Diagnostics", +}; + +function isViewId(value: string | undefined): value is ViewId { + return value === "backfill" || value === "diagnostics"; +} + +function activateView(viewId: ViewId, source: "startup" | "user"): void { + document.querySelectorAll("[data-view-panel]").forEach(panel => { + panel.hidden = panel.dataset.viewPanel !== viewId; + }); + document.querySelectorAll("[data-view]").forEach(button => { + const active = button.dataset.view === viewId; + button.classList.toggle("active", active); + button.setAttribute("aria-current", active ? "page" : "false"); + }); + const header = document.querySelector("#headerViewTitle"); + if (header) { + header.textContent = viewTitles[viewId]; + } + document.title = `Backfill Desk — ${viewTitles[viewId]}`; + frontendTrace("main", "Backfill Desk view DOM updated", { viewId, source }); +} + +function bindFrontendInteractions(): void { + document.addEventListener( + "click", + event => { + const source = event.target; + if (!(source instanceof Element)) { + return; + } + const control = source.closest('button, a, input, select, textarea, [role="button"], [role="tab"], [data-view]'); + if (!control) { + return; + } + frontendTrace("main", "Backfill Desk frontend control clicked", { + controlId: control.id || null, + role: control.getAttribute("role"), + tagName: control.tagName.toLowerCase(), + viewId: control.dataset.view ?? null, + }); + }, + true, + ); + frontendTrace("main", "Backfill Desk frontend control interaction logger installed"); +} + +function bindNavigation(): void { + document.querySelectorAll("[data-view]").forEach(button => { + button.addEventListener("click", () => { + const viewId = button.dataset.view; + frontendDebug("main", "Backfill Desk navigation tab clicked", { viewId: viewId ?? "unknown" }); + if (isViewId(viewId)) { + activateView(viewId, "user"); + } + }); + }); + frontendTrace("main", "Backfill Desk navigation handlers installed"); +} + +function renderRuntimeStatus(status: ShellStatusDto): void { + const values: Record = { + runtimeVersion: status.applicationVersion, + runtimeShellPhase: status.shellPhase, + runtimeConfigDocuments: status.configDocumentCount.toString(), + runtimeLoggingProfile: status.activeLoggingProfile ?? "fallback transitoire", + runtimeLoggingFallback: status.fallbackLoggingActive ? "oui" : "non", + }; + for (const [id, value] of Object.entries(values)) { + const element = document.querySelector(`#${id}`); + if (element) { + element.textContent = value; + } + } + const badge = document.querySelector("#appVersionBadge"); + if (badge) { + badge.textContent = status.applicationVersion; + } + const diagnostic = document.querySelector("#runtimeDiagnostic"); + if (diagnostic) { + diagnostic.hidden = status.startupDiagnostic === null; + diagnostic.textContent = status.startupDiagnostic === null ? "" : `${status.startupDiagnostic.domain}/${status.startupDiagnostic.code}: ${status.startupDiagnostic.message}`; + } + frontendTrace("main", "Backfill Desk runtime status rendered", { + fallbackLoggingActive: status.fallbackLoggingActive, + configDocumentCount: status.configDocumentCount, + }); +} + +async function loadRuntimeStatus(source: "startup" | "user"): Promise { + frontendDebug("main", "Backfill Desk runtime status load started", { source }); + const status = await invokeKsp("main", "get_runtime_status"); + renderRuntimeStatus(status); + frontendDebug("main", "Backfill Desk runtime status load completed", { source }); +} + +function bindRuntimeStatusRefresh(): void { + const button = document.querySelector("#refreshRuntimeStatus"); + if (!button) { + return; + } + button.addEventListener("click", () => { + frontendDebug("main", "Backfill Desk runtime status refresh button clicked"); + void loadRuntimeStatus("user").catch(() => frontendWarn("main", "Backfill Desk runtime status refresh failed")); + }); + frontendTrace("main", "Backfill Desk runtime status refresh handler installed"); +} + +async function initializeMain(): Promise { + const windowLabel = getCurrentWindow().label; + frontendInfo("main", "Backfill Desk main frontend loaded", { windowLabel }); + bindFrontendInteractions(); + bindNavigation(); + bindRuntimeStatusRefresh(); + activateView("backfill", "startup"); + try { + await loadRuntimeStatus("startup"); + } catch { + frontendWarn("main", "Backfill Desk startup runtime status load failed"); + } +} + +document.addEventListener("DOMContentLoaded", () => { + void initializeMain(); +}); diff --git a/crates/ksp-app-backfill-desk/frontend/ts/splash.ts b/crates/ksp-app-backfill-desk/frontend/ts/splash.ts new file mode 100644 index 0000000..185d598 --- /dev/null +++ b/crates/ksp-app-backfill-desk/frontend/ts/splash.ts @@ -0,0 +1,131 @@ +// file: crates/ksp-app-backfill-desk/frontend/ts/splash.ts +// version: 1 + +import { listen } from "@tauri-apps/api/event"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import type { SplashOrderDto } from "./bindings/ksp_app_backfill_desk/splash/SplashOrderDto.ts"; +import { frontendDebug, frontendError, frontendInfo, frontendTrace, installFrontendConsoleBridge } from "./frontend_log"; +import { invokeKsp } from "./invoke"; + +installFrontendConsoleBridge("splash"); + +let activeOpacityFrame: number | null = null; + +function easeInOut(value: number): number { + return value < 0.5 ? 2 * value * value : 1 - Math.pow(-2 * value + 2, 2) / 2; +} + +function normalizeDurationMs(value: number | null): number { + return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0; +} + +async function animateOpacity(element: HTMLElement, fromOpacity: number, toOpacity: number, durationMs: number): Promise { + 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(resolve => requestAnimationFrame(() => resolve())); + await new Promise(resolve => { + const startedAt = performance.now(); + const opacityDelta = toOpacity - fromOpacity; + const updateOpacity = (currentTime: number): void => { + const elapsedMs = currentTime - startedAt; + const rawProgress = durationMs === 0 ? 1 : Math.min(elapsedMs / durationMs, 1); + element.style.opacity = (fromOpacity + opacityDelta * easeInOut(rawProgress)).toString(); + if (rawProgress >= 1) { + element.style.opacity = toOpacity.toString(); + element.style.willChange = "auto"; + activeOpacityFrame = null; + resolve(); + return; + } + activeOpacityFrame = requestAnimationFrame(updateOpacity); + }; + activeOpacityFrame = requestAnimationFrame(updateOpacity); + }); + frontendTrace("splash", "Splash opacity animation completed", { toOpacity, durationMs }); +} + +function scrollToLatest(element: HTMLElement): void { + element.scrollTop = element.scrollHeight; +} + +function addMessage(message: string, status: string | null): void { + const container = document.querySelector("#messages-container"); + if (!container) { + return; + } + const line = document.createElement("div"); + const safeStatus = status === "warning" || status === "error" || status === "success" ? status : "info"; + line.className = `splash-message ${safeStatus}`; + line.textContent = message; + container.appendChild(line); + scrollToLatest(container); +} + +function addDebugMessage(message: string): void { + const container = document.querySelector("#debug-info"); + if (!container) { + return; + } + container.hidden = false; + const line = document.createElement("div"); + line.textContent = `${new Date().toLocaleTimeString()}: ${message}`; + container.appendChild(line); + scrollToLatest(container); +} + +async function handleSplashOrder(order: SplashOrderDto): Promise { + frontendTrace("splash", "Splash order received", { action: order.action }); + const container = document.querySelector("#splash-container"); + if (order.action === "add_message" && order.message) { + addMessage(order.message, order.status); + return; + } + if (order.action === "add_debug" && order.message) { + addDebugMessage(order.message); + return; + } + if (!container) { + return; + } + if (order.action === "fade_in") { + await animateOpacity(container, 0, 1, normalizeDurationMs(order.durationMs)); + return; + } + if (order.action === "fade_out") { + await animateOpacity(container, 1, 0, normalizeDurationMs(order.durationMs)); + } +} + +async function initializeSplash(): Promise { + const windowLabel = getCurrentWindow().label; + frontendInfo("splash", "Backfill Desk splash frontend loaded", { windowLabel }); + const container = document.querySelector("#splash-container"); + if (container) { + container.style.opacity = "0"; + container.style.willChange = "opacity"; + frontendTrace("splash", "Splash container prepared for managed fade-in"); + } + await listen("ksp-splash-order", event => { + void handleSplashOrder(event.payload); + }); + frontendDebug("splash", "Splash lifecycle listener installed"); + try { + await invokeKsp("splash", "splash_frontend_ready"); + } catch { + addMessage("Le lifecycle du splash n'a pas pu démarrer.", "error"); + if (container) { + container.style.opacity = "1"; + container.style.willChange = "auto"; + } + frontendError("splash", "Splash readiness command failed"); + } +} + +document.addEventListener("DOMContentLoaded", () => { + void initializeSplash(); +}); diff --git a/crates/ksp-app-backfill-desk/icons/favicon.ico b/crates/ksp-app-backfill-desk/icons/favicon.ico new file mode 100644 index 0000000..9dfa172 Binary files /dev/null and b/crates/ksp-app-backfill-desk/icons/favicon.ico differ diff --git a/crates/ksp-app-backfill-desk/icons/favicon.png b/crates/ksp-app-backfill-desk/icons/favicon.png new file mode 100644 index 0000000..94e89ca Binary files /dev/null and b/crates/ksp-app-backfill-desk/icons/favicon.png differ diff --git a/crates/ksp-app-backfill-desk/package.json b/crates/ksp-app-backfill-desk/package.json new file mode 100644 index 0000000..2cec18b --- /dev/null +++ b/crates/ksp-app-backfill-desk/package.json @@ -0,0 +1,26 @@ +{ + "name": "ksp-app-backfill-desk", + "private": true, + "version": "0.3.7-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", + "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" + } +} diff --git a/crates/ksp-app-backfill-desk/src/app_state.rs b/crates/ksp-app-backfill-desk/src/app_state.rs new file mode 100644 index 0000000..f2e15bb --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/app_state.rs @@ -0,0 +1,125 @@ +// file: crates/ksp-app-backfill-desk/src/app_state.rs +// version: 1 + +//! Shared backend state owned by the Backfill Desk Tauri application. + +/// Shared Backfill Desk application state managed by Tauri. +pub(crate) struct AppState { + config_management: ksp_config_lib::ConfigManagement, + logging_runtime: std::sync::Mutex, + splash_settings: crate::SplashSettings, + splash_sequence_started: std::sync::atomic::AtomicBool, +} + +impl crate::AppState { + /// Initializes the scaffold Config ownership, Logging runtime and splash settings. + pub(crate) fn initialize(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result { + let config_management = crate::config_management(arguments); + let config_management = match config_management { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let runtime_identity = crate::launch_identity(); + let runtime_identity = match runtime_identity { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let logging_startup = crate::initialize_logging(&config_management, &runtime_identity); + let logging_startup = match logging_startup { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let splash_settings = crate::SplashSettings::load(); + let splash_settings = match splash_settings { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + ksp_logging_lib::warn!( + target: crate::TRACING_TARGET, + domain = crate::TRACING_DOMAIN_WINDOWS, + error_domain = error.code().domain(), + error_code = error.code().code(), + "managed splash timings are invalid; using transient in-memory defaults" + ); + crate::SplashSettings::fallback() + }, + }; + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + domain = crate::TRACING_DOMAIN_WINDOWS, + minimum_ms = splash_settings.minimum_ms(), + minimum_source = splash_settings.minimum_source(), + fade_in_ms = splash_settings.fade_in_ms(), + fade_in_source = splash_settings.fade_in_source(), + fade_out_ms = splash_settings.fade_out_ms(), + fade_out_source = splash_settings.fade_out_source(), + expected_backend_lifecycle_ms = splash_settings.expected_backend_lifecycle_ms(), + "resolved Backfill 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 scaffold status exposed by the Backfill Desk shell. + pub(crate) fn shell_status(&self) -> ksp_core_lib::Result { + 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 count cannot be represented by Backfill Desk") + .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, + "Backfill Desk Logging runtime state lock is poisoned", + )); + }, + }; + let _keep_guard_alive = &runtime.guard; + return std::result::Result::Ok(crate::ShellStatusDto { + active_logging_profile: runtime.active_profile_id.clone(), + application_version: env!("CARGO_PKG_VERSION").to_owned(), + config_document_count: document_count, + fallback_logging_active: runtime.fallback_active, + shell_phase: "pre.002-desktop-scaffold".to_owned(), + startup_diagnostic: runtime.startup_diagnostic.clone(), + }); + } + + /// Returns the resolved splash timings captured during application bootstrap. + #[must_use] + pub(crate) const fn splash_settings(&self) -> crate::SplashSettings { + return self.splash_settings; + } + + /// Marks the one-shot splash lifecycle as started and reports whether this caller won the transition. + pub(crate) fn begin_splash_sequence(&self) -> bool { + return self + .splash_sequence_started + .compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire) + .is_ok(); + } +} + +struct LoggingRuntimeState { + guard: ksp_logging_lib::LoggingGuard, + active_profile_id: std::option::Option, + fallback_active: bool, + startup_diagnostic: std::option::Option, +} diff --git a/crates/ksp-app-backfill-desk/src/bootstrap.rs b/crates/ksp-app-backfill-desk/src/bootstrap.rs new file mode 100644 index 0000000..3595cfb --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/bootstrap.rs @@ -0,0 +1,146 @@ +// file: crates/ksp-app-backfill-desk/src/bootstrap.rs +// version: 1 + +//! Config and Logging bootstrap for the Backfill Desk scaffold. + +/// Crate-internal Logging startup state shared by the application state. +pub(crate) struct LoggingStartup { + /// Guard that keeps the installed Logging runtime alive. + pub(crate) guard: ksp_logging_lib::LoggingGuard, + /// Active managed profile when Config resolved one successfully. + pub(crate) active_profile_id: std::option::Option, + /// 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, +} + +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 { + let bootstrap = ksp_config_lib::ConfigBootstrapOptions::from_args(arguments); + let bootstrap = match bootstrap { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let registry = ksp_config_lib::ConfigFileRegistry::from_args(arguments); + let registry = match registry { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let engine = ksp_config_lib::ConfigDocumentEngine::new(bootstrap, registry); + return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine)); +} + +/// Initializes Logging from the standard managed Config document, with the common Desk fallback policy. +pub(crate) fn initialize_logging( + management: &ksp_config_lib::ConfigManagement, + runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, +) -> ksp_core_lib::Result { + 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 Backfill 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 { + 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 { + 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 Backfill 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), + }); +} diff --git a/crates/ksp-app-backfill-desk/src/constants.rs b/crates/ksp-app-backfill-desk/src/constants.rs new file mode 100644 index 0000000..af2cc21 --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/constants.rs @@ -0,0 +1,21 @@ +// file: crates/ksp-app-backfill-desk/src/constants.rs +// version: 1 + +//! Application-owned tracing targets and domains. + +/// Structured domain used while bootstrapping Config and Logging. +pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "backfill.bootstrap"; +/// Structured domain used by technical frontend events. +pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend"; +/// Structured domain used by the Backfill Desk shell. +pub(crate) const TRACING_DOMAIN_SHELL: &str = "backfill.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 Backfill Desk. +pub(crate) const TRACING_TARGET: &str = "ksp-app-backfill-desk"; +/// Owning target for generic frontend events emitted through the KSP bridge. +pub(crate) const TRACING_TARGET_FRONTEND: &str = "ksp-app-backfill-desk.frontend"; +/// Owning target for main-window frontend events. +pub(crate) const TRACING_TARGET_FRONTEND_MAIN: &str = "ksp-app-backfill-desk.frontend.main"; +/// Owning target for splash-window frontend events. +pub(crate) const TRACING_TARGET_FRONTEND_SPLASH: &str = "ksp-app-backfill-desk.frontend.splash"; diff --git a/crates/ksp-app-backfill-desk/src/dto_common.rs b/crates/ksp-app-backfill-desk/src/dto_common.rs new file mode 100644 index 0000000..4cd95d3 --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/dto_common.rs @@ -0,0 +1,54 @@ +// file: crates/ksp-app-backfill-desk/src/dto_common.rs +// version: 1 + +//! Common Tauri DTOs shared by the Backfill Desk shell. + +use ts_rs::TS; // rust-rules: trait-import + +/// Safe command error projection that never serializes arbitrary KSP error context or source values. +#[derive(Clone, Debug, serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/CommandErrorDto.ts")] +pub(crate) struct CommandErrorDto { + /// Stable KSP error domain. + pub(crate) domain: String, + /// Stable KSP error code within the domain. + pub(crate) code: String, + /// Human-readable error message without arbitrary context fields. + pub(crate) message: String, +} + +impl crate::CommandErrorDto { + /// Builds a bounded safe projection from a KSP error. + #[must_use] + pub(crate) fn from_error(error: &ksp_core_lib::Error) -> Self { + return Self { + domain: error.code().domain().to_owned(), + code: error.code().code().to_owned(), + message: error.message().to_owned(), + }; + } +} + +/// Safe scaffold/runtime snapshot exposed to the Backfill Desk shell. +#[derive(Clone, Debug, serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts")] +pub(crate) struct ShellStatusDto { + /// Active configured Logging profile, or `None` while transient fallback Logging is active. + pub(crate) active_logging_profile: std::option::Option, + /// Cargo application version. + pub(crate) application_version: String, + /// Number of logical Config resources registered by the current runtime. + pub(crate) config_document_count: u32, + /// Whether Backfill Desk had to install its transient in-memory Logging fallback. + pub(crate) fallback_logging_active: bool, + /// Current implementation phase exposed by the shell. + pub(crate) shell_phase: String, + /// Safe startup diagnostic that caused fallback Logging, when applicable. + pub(crate) startup_diagnostic: std::option::Option, +} + +#[cfg(test)] +#[path = "../unit_tests/dto_common.rs"] +mod tests; diff --git a/crates/ksp-app-backfill-desk/src/errors.rs b/crates/ksp-app-backfill-desk/src/errors.rs new file mode 100644 index 0000000..70607ec --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/errors.rs @@ -0,0 +1,26 @@ +// file: crates/ksp-app-backfill-desk/src/errors.rs +// version: 1 + +//! Application-local error codes for Backfill Desk composition and desktop runtime surfaces. + +/// Shared Backfill Desk runtime state is internally inconsistent. +pub(crate) const ERROR_CODE_APP_STATE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "app_state_invalid"); +/// Shared Backfill 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("backfill_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("backfill_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("backfill_desk", "frontend_log_target_invalid"); +/// Backfill 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("backfill_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("backfill_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("backfill_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("backfill_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("backfill_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("backfill_desk", "tauri_window_operation_failed"); diff --git a/crates/ksp-app-backfill-desk/src/frontend_logging.rs b/crates/ksp-app-backfill-desk/src/frontend_logging.rs new file mode 100644 index 0000000..28448b4 --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/frontend_logging.rs @@ -0,0 +1,205 @@ +// file: crates/ksp-app-backfill-desk/src/frontend_logging.rs +// version: 1 + +//! KSP-owned bridge for technical log events emitted by Backfill Desk frontend scripts. + +use ts_rs::TS; // rust-rules: trait-import + +/// Log payload sent by Backfill Desk frontend scripts. +#[derive(Clone, Debug, serde::Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/frontend_logging/FrontendLogPayloadDto.ts")] +pub(crate) struct FrontendLogPayloadDto { + /// Lowercase KSP log level. + pub(crate) level: String, + /// Whitelisted logical frontend target identifier. + pub(crate) target_id: String, + /// Rendered technical frontend message. + pub(crate) message: String, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum FrontendLogLevel { + Trace, + Debug, + Info, + Warn, + Error, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum FrontendLogTarget { + Frontend, + Main, + Splash, +} + +/// Emits one validated frontend event through the KSP Logging facade. +pub(crate) fn emit_frontend_log_event(payload: FrontendLogPayloadDto) -> ksp_core_lib::Result<()> { + let level = parse_level(payload.level.as_str()); + let level = match level { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let target = parse_target(payload.target_id.as_str()); + let target = match target { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + emit_validated_frontend_log(level, target, payload.message.as_str()); + return std::result::Result::Ok(()); +} + +fn parse_level(level: &str) -> ksp_core_lib::Result { + return match level.trim().to_ascii_lowercase().as_str() { + "trace" => std::result::Result::Ok(FrontendLogLevel::Trace), + "debug" => std::result::Result::Ok(FrontendLogLevel::Debug), + "info" => std::result::Result::Ok(FrontendLogLevel::Info), + "warn" => std::result::Result::Ok(FrontendLogLevel::Warn), + "error" => std::result::Result::Ok(FrontendLogLevel::Error), + _ => std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID, "Frontend log level is not supported")), + }; +} + +fn parse_target(target_id: &str) -> ksp_core_lib::Result { + return match target_id.trim().to_ascii_lowercase().as_str() { + "frontend" => std::result::Result::Ok(FrontendLogTarget::Frontend), + "main" => std::result::Result::Ok(FrontendLogTarget::Main), + "splash" => std::result::Result::Ok(FrontendLogTarget::Splash), + _ => { + std::result::Result::Err(ksp_core_lib::Error::new(crate::ERROR_CODE_FRONTEND_LOG_TARGET_INVALID, "Frontend log target identifier is not supported")) + }, + }; +} + +fn emit_validated_frontend_log(level: FrontendLogLevel, target: FrontendLogTarget, message: &str) { + return match target { + FrontendLogTarget::Frontend => emit_frontend_target(level, message), + FrontendLogTarget::Main => emit_main_target(level, message), + FrontendLogTarget::Splash => emit_splash_target(level, message), + }; +} + +fn emit_frontend_target(level: FrontendLogLevel, message: &str) { + return match level { + FrontendLogLevel::Trace => ksp_logging_lib::trace!( + target: crate::TRACING_TARGET_FRONTEND, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "frontend", + "{message}" + ), + FrontendLogLevel::Debug => ksp_logging_lib::debug!( + target: crate::TRACING_TARGET_FRONTEND, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "frontend", + "{message}" + ), + FrontendLogLevel::Info => ksp_logging_lib::info!( + target: crate::TRACING_TARGET_FRONTEND, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "frontend", + "{message}" + ), + FrontendLogLevel::Warn => ksp_logging_lib::warn!( + target: crate::TRACING_TARGET_FRONTEND, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "frontend", + "{message}" + ), + FrontendLogLevel::Error => ksp_logging_lib::error!( + target: crate::TRACING_TARGET_FRONTEND, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "frontend", + "{message}" + ), + }; +} + +fn emit_main_target(level: FrontendLogLevel, message: &str) { + return match level { + FrontendLogLevel::Trace => ksp_logging_lib::trace!( + target: crate::TRACING_TARGET_FRONTEND_MAIN, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "main", + "{message}" + ), + FrontendLogLevel::Debug => ksp_logging_lib::debug!( + target: crate::TRACING_TARGET_FRONTEND_MAIN, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "main", + "{message}" + ), + FrontendLogLevel::Info => ksp_logging_lib::info!( + target: crate::TRACING_TARGET_FRONTEND_MAIN, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "main", + "{message}" + ), + FrontendLogLevel::Warn => ksp_logging_lib::warn!( + target: crate::TRACING_TARGET_FRONTEND_MAIN, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "main", + "{message}" + ), + FrontendLogLevel::Error => ksp_logging_lib::error!( + target: crate::TRACING_TARGET_FRONTEND_MAIN, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "main", + "{message}" + ), + }; +} + +fn emit_splash_target(level: FrontendLogLevel, message: &str) { + return match level { + FrontendLogLevel::Trace => ksp_logging_lib::trace!( + target: crate::TRACING_TARGET_FRONTEND_SPLASH, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "splash", + "{message}" + ), + FrontendLogLevel::Debug => ksp_logging_lib::debug!( + target: crate::TRACING_TARGET_FRONTEND_SPLASH, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "splash", + "{message}" + ), + FrontendLogLevel::Info => ksp_logging_lib::info!( + target: crate::TRACING_TARGET_FRONTEND_SPLASH, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "splash", + "{message}" + ), + FrontendLogLevel::Warn => ksp_logging_lib::warn!( + target: crate::TRACING_TARGET_FRONTEND_SPLASH, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "splash", + "{message}" + ), + FrontendLogLevel::Error => ksp_logging_lib::error!( + target: crate::TRACING_TARGET_FRONTEND_SPLASH, + domain = crate::TRACING_DOMAIN_FRONTEND, + action = "frontend_log", + target_id = "splash", + "{message}" + ), + }; +} + +#[cfg(test)] +#[path = "../unit_tests/frontend_logging.rs"] +mod tests; diff --git a/crates/ksp-app-backfill-desk/src/lib.rs b/crates/ksp-app-backfill-desk/src/lib.rs new file mode 100644 index 0000000..23d11b5 --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/lib.rs @@ -0,0 +1,90 @@ +// file: crates/ksp-app-backfill-desk/src/lib.rs +// version: 1 + +//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs. + +#![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 Backfill desktop application. +pub use self::tauri::run; + +/// Shared Backfill 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 scaffold Logging runtime from standard Config, with a bounded in-memory fallback. +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 Backfill 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 Backfill Desk. +pub(crate) use self::constants::TRACING_TARGET; +/// Owning target for generic frontend events emitted through the KSP bridge. +pub(crate) use self::constants::TRACING_TARGET_FRONTEND; +/// Owning target for main-window frontend events. +pub(crate) use self::constants::TRACING_TARGET_FRONTEND_MAIN; +/// Owning target for splash-window frontend events. +pub(crate) use self::constants::TRACING_TARGET_FRONTEND_SPLASH; +/// Safe command error projection exposed to Tauri commands. +pub(crate) use self::dto_common::CommandErrorDto; +/// Safe scaffold/runtime snapshot exposed to the shell. +pub(crate) use self::dto_common::ShellStatusDto; +/// Shared Backfill Desk runtime state is internally inconsistent. +pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID; +/// Shared Backfill 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; +/// Backfill 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 Backfill 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 Backfill 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 Backfill 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; diff --git a/crates/ksp-app-backfill-desk/src/logging_runtime.rs b/crates/ksp-app-backfill-desk/src/logging_runtime.rs new file mode 100644 index 0000000..6f47878 --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/logging_runtime.rs @@ -0,0 +1,10 @@ +// file: crates/ksp-app-backfill-desk/src/logging_runtime.rs +// version: 1 + +//! Backfill Desk Logging runtime identity helpers. + +/// Creates the stable runtime identity for this Backfill Desk process launch. +pub(crate) fn launch_identity() -> ksp_core_lib::Result { + 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); +} diff --git a/crates/ksp-app-backfill-desk/src/main.rs b/crates/ksp-app-backfill-desk/src/main.rs new file mode 100644 index 0000000..3a5c06c --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/main.rs @@ -0,0 +1,58 @@ +// file: crates/ksp-app-backfill-desk/src/main.rs +// version: 1 + +//! Binary entry point for the KSP Backfill 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 Backfill 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_backfill_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-backfill-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::>(); + let run_result = ksp_app_backfill_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(()); + } +} diff --git a/crates/ksp-app-backfill-desk/src/splash.rs b/crates/ksp-app-backfill-desk/src/splash.rs new file mode 100644 index 0000000..c3f162c --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/splash.rs @@ -0,0 +1,214 @@ +// file: crates/ksp-app-backfill-desk/src/splash.rs +// version: 1 + +//! Common splash settings and frontend event contracts for Backfill Desk. + +use ts_rs::TS; // rust-rules: trait-import + +const DEFAULT_SPLASH_FADE_IN_MS: u32 = 300; +const DEFAULT_SPLASH_FADE_OUT_MS: u32 = 300; +const DEFAULT_SPLASH_MINIMUM_MS: u64 = 1200; +const ENV_SPLASH_FADE_IN_MS: &str = "KSP_DESK_SPLASH_FADE_IN_MS"; +const ENV_SPLASH_FADE_OUT_MS: &str = "KSP_DESK_SPLASH_FADE_OUT_MS"; +const ENV_SPLASH_MINIMUM_MS: &str = "KSP_DESK_SPLASH_MINIMUM_MS"; +const MAX_SPLASH_FADE_MS: u32 = 10_000; +const MAX_SPLASH_MINIMUM_MS: u64 = 60_000; + +/// Runtime timings used by the common desk splash lifecycle. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct SplashSettings { + fade_in_ms: u32, + minimum_ms: u64, + fade_out_ms: u32, + fade_in_source: ksp_config_lib::ConfigEnvironmentSource, + minimum_source: ksp_config_lib::ConfigEnvironmentSource, + fade_out_source: ksp_config_lib::ConfigEnvironmentSource, +} + +impl SplashSettings { + /// Resolves splash timings through the Config-owned environment snapshot. + pub(crate) fn load() -> ksp_core_lib::Result { + let environment = ksp_config_lib::ConfigEnvironment::load(); + let environment = match environment { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let fade_in = environment.resolve_variable(ENV_SPLASH_FADE_IN_MS, std::option::Option::Some("300")); + let fade_in = match fade_in { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let minimum = environment.resolve_variable(ENV_SPLASH_MINIMUM_MS, std::option::Option::Some("1200")); + let minimum = match minimum { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let fade_out = environment.resolve_variable(ENV_SPLASH_FADE_OUT_MS, std::option::Option::Some("300")); + let fade_out = match fade_out { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let fade_in_ms = parse_u32_setting(ENV_SPLASH_FADE_IN_MS, fade_in.value(), MAX_SPLASH_FADE_MS); + let fade_in_ms = match fade_in_ms { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let minimum_ms = parse_u64_setting(ENV_SPLASH_MINIMUM_MS, minimum.value(), MAX_SPLASH_MINIMUM_MS); + let minimum_ms = match minimum_ms { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let fade_out_ms = parse_u32_setting(ENV_SPLASH_FADE_OUT_MS, fade_out.value(), MAX_SPLASH_FADE_MS); + let fade_out_ms = match fade_out_ms { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + return std::result::Result::Ok(Self { + fade_in_ms, + minimum_ms, + fade_out_ms, + fade_in_source: fade_in.source(), + minimum_source: minimum.source(), + fade_out_source: fade_out.source(), + }); + } + + /// Returns safe in-memory timings used when the managed environment cannot be resolved. + #[must_use] + pub(crate) const fn fallback() -> Self { + return Self { + fade_in_ms: DEFAULT_SPLASH_FADE_IN_MS, + minimum_ms: DEFAULT_SPLASH_MINIMUM_MS, + fade_out_ms: DEFAULT_SPLASH_FADE_OUT_MS, + fade_in_source: ksp_config_lib::ConfigEnvironmentSource::Fallback, + minimum_source: ksp_config_lib::ConfigEnvironmentSource::Fallback, + fade_out_source: ksp_config_lib::ConfigEnvironmentSource::Fallback, + }; + } + + /// Returns the configured fade-in duration. + #[must_use] + pub(crate) const fn fade_in_ms(self) -> u32 { + return self.fade_in_ms; + } + + /// Returns the minimum fully-visible splash duration. + #[must_use] + pub(crate) const fn minimum_ms(self) -> u64 { + return self.minimum_ms; + } + + /// Returns the configured fade-out duration. + #[must_use] + pub(crate) const fn fade_out_ms(self) -> u32 { + return self.fade_out_ms; + } + + /// Returns the safe provenance code for the fade-in duration. + #[must_use] + pub(crate) const fn fade_in_source(self) -> &'static str { + return environment_source_code(self.fade_in_source); + } + + /// Returns the safe provenance code for the minimum duration. + #[must_use] + pub(crate) const fn minimum_source(self) -> &'static str { + return environment_source_code(self.minimum_source); + } + + /// Returns the safe provenance code for the fade-out duration. + #[must_use] + pub(crate) const fn fade_out_source(self) -> &'static str { + return environment_source_code(self.fade_out_source); + } + + /// Returns the expected additive backend lifecycle from readiness through fade-out. + #[must_use] + pub(crate) fn expected_backend_lifecycle_ms(self) -> u64 { + return u64::from(self.fade_in_ms) + self.minimum_ms + u64::from(self.fade_out_ms); + } +} + +/// Command emitted by Rust to the splash frontend. +#[derive(Clone, Debug, serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/splash/SplashOrderDto.ts")] +pub(crate) struct SplashOrderDto { + /// Splash action name. + pub(crate) action: String, + /// Optional status text. + pub(crate) message: std::option::Option, + /// Optional message severity used by the bottom message feed. + pub(crate) status: std::option::Option, + /// Optional animation duration in milliseconds. + pub(crate) duration_ms: std::option::Option, +} + +impl SplashOrderDto { + /// Creates a new `SplashOrderDto` value. + #[must_use] + pub(crate) fn new(action: &str, message: std::option::Option<&str>, status: std::option::Option<&str>, duration_ms: std::option::Option) -> Self { + return Self { + action: action.to_owned(), + message: message.map(std::string::ToString::to_string), + status: status.map(std::string::ToString::to_string), + duration_ms, + }; + } +} + +const fn environment_source_code(source: ksp_config_lib::ConfigEnvironmentSource) -> &'static str { + return match source { + ksp_config_lib::ConfigEnvironmentSource::Process => "process", + ksp_config_lib::ConfigEnvironmentSource::DotEnv => "dotenv", + ksp_config_lib::ConfigEnvironmentSource::Fallback => "fallback", + }; +} + +fn parse_u64_setting(variable_name: &str, value: &str, maximum: u64) -> ksp_core_lib::Result { + let parsed = value.parse::(); + 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 { + let parsed = value.parse::(); + 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; diff --git a/crates/ksp-app-backfill-desk/src/tauri.rs b/crates/ksp-app-backfill-desk/src/tauri.rs new file mode 100644 index 0000000..28c63cb --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/tauri.rs @@ -0,0 +1,99 @@ +// file: crates/ksp-app-backfill-desk/src/tauri.rs +// version: 1 + +//! Tauri runtime assembly for the KSP Backfill desktop application. + +/// Runs the Backfill desktop 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 Backfill Desk Tauri runtime") + .with_context("tauri_error", error.to_string()), + ), + }; +} + +fn configure_state(builder: tauri::Builder, app_state: crate::AppState) -> tauri::Builder { + return builder.manage(app_state); +} + +fn configure_plugins(builder: tauri::Builder) -> tauri::Builder { + let tracing_plugin = tauri_plugin_tracing::Builder::new().build::(); + 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::Builder { + return builder.invoke_handler(tauri::generate_handler![emit_frontend_log, get_runtime_status, splash_frontend_ready]); +} + +fn configure_setup(builder: tauri::Builder) -> tauri::Builder { + 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, "Backfill Desk Tauri shell setup completed"); + return std::result::Result::Ok(()); + }); +} + +fn project_command_error(command: &'static str, domain: &'static str, error: &ksp_core_lib::Error) -> crate::CommandErrorDto { + ksp_logging_lib::warn!( + target: crate::TRACING_TARGET, + domain = domain, + command = command, + error_domain = error.code().domain(), + error_code = error.code().code(), + "Backfill Desk Tauri command failed" + ); + return crate::CommandErrorDto::from_error(error); +} + +#[tauri::command] +fn emit_frontend_log(payload: crate::FrontendLogPayloadDto) -> std::result::Result<(), crate::CommandErrorDto> { + let result = crate::emit_frontend_log_event(payload); + return match result { + std::result::Result::Ok(()) => std::result::Result::Ok(()), + std::result::Result::Err(error) => std::result::Result::Err(project_command_error("emit_frontend_log", crate::TRACING_DOMAIN_FRONTEND, &error)), + }; +} + +#[tauri::command] +fn get_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result { + let result = state.shell_status(); + return match result { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(project_command_error("get_runtime_status", crate::TRACING_DOMAIN_SHELL, &error)), + }; +} + +#[tauri::command] +async fn splash_frontend_ready( + app: tauri::AppHandle, + webview_window: tauri::WebviewWindow, + state: tauri::State<'_, crate::AppState>, +) -> std::result::Result<(), crate::CommandErrorDto> { + let result = crate::splash_frontend_ready_service(app, webview_window, &state).await; + return match result { + std::result::Result::Ok(()) => std::result::Result::Ok(()), + std::result::Result::Err(error) => std::result::Result::Err(project_command_error("splash_frontend_ready", crate::TRACING_DOMAIN_WINDOWS, &error)), + }; +} diff --git a/crates/ksp-app-backfill-desk/src/tw_main.rs b/crates/ksp-app-backfill-desk/src/tw_main.rs new file mode 100644 index 0000000..e874a70 --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/tw_main.rs @@ -0,0 +1,53 @@ +// file: crates/ksp-app-backfill-desk/src/tw_main.rs +// version: 1 + +//! Tauri-window helpers for the Backfill Desk main window. + +use tauri::Manager; // rust-rules: trait-import + +/// Crate-internal `WINDOW_LABEL_MAIN` constant. +pub(crate) const WINDOW_LABEL_MAIN: &str = "main"; + +/// Resolves the required main window or returns a typed error. +pub(crate) fn require_main_window(manager: &impl Manager) -> ksp_core_lib::Result { + 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, "Backfill Desk main window is missing") + .with_context("window_label", WINDOW_LABEL_MAIN), + ), + }; +} + +/// Shows main window. +pub(crate) fn show_main_window(app: &tauri::AppHandle) -> ksp_core_lib::Result<()> { + let window = crate::require_main_window(app); + let window = match window { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let shown = window.show(); + if let std::result::Result::Err(error) = shown { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot show Backfill 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 Backfill 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(()); +} diff --git a/crates/ksp-app-backfill-desk/src/tw_splash.rs b/crates/ksp-app-backfill-desk/src/tw_splash.rs new file mode 100644 index 0000000..dca0135 --- /dev/null +++ b/crates/ksp-app-backfill-desk/src/tw_splash.rs @@ -0,0 +1,173 @@ +// file: crates/ksp-app-backfill-desk/src/tw_splash.rs +// version: 1 + +//! Tauri-window lifecycle for the Backfill 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) -> ksp_core_lib::Result { + 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, "Backfill Desk splash window is missing") + .with_context("window_label", WINDOW_LABEL_SPLASH), + ), + }; +} + +/// Starts the one-shot splash lifecycle after the splash frontend reports readiness. +pub(crate) async fn splash_frontend_ready_service( + app: tauri::AppHandle, + invoking_window: tauri::WebviewWindow, + state: &crate::AppState, +) -> ksp_core_lib::Result<()> { + if invoking_window.label() != WINDOW_LABEL_SPLASH { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_SPLASH_ORIGIN_INVALID, "Splash readiness may only originate from the splash window") + .with_context("window_label", invoking_window.label()), + ); + } + if !state.begin_splash_sequence() { + ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, "duplicate splash frontend readiness ignored"); + return std::result::Result::Ok(()); + } + let settings = state.splash_settings(); + let lifecycle_started = std::time::Instant::now(); + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + domain = crate::TRACING_DOMAIN_WINDOWS, + window_label = WINDOW_LABEL_SPLASH, + fade_in_ms = settings.fade_in_ms(), + fade_in_source = settings.fade_in_source(), + minimum_ms = settings.minimum_ms(), + minimum_source = settings.minimum_source(), + fade_out_ms = settings.fade_out_ms(), + fade_out_source = settings.fade_out_source(), + expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(), + "splash frontend readiness accepted; starting splash to main lifecycle" + ); + let emitted = emit_order( + &invoking_window, + crate::SplashOrderDto::new("fade_in", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_in_ms())), + ); + if let std::result::Result::Err(error) = emitted { + return std::result::Result::Err(error); + } + let emitted = emit_message(&invoking_window, "Initialisation de Backfill Desk...", "info"); + if let std::result::Result::Err(error) = emitted { + return std::result::Result::Err(error); + } + let emitted = emit_debug(&invoking_window, "Listener frontend prêt; fade-in piloté par Rust."); + if let std::result::Result::Err(error) = emitted { + return std::result::Result::Err(error); + } + sleep_and_log(&invoking_window, "fade-in", u64::from(settings.fade_in_ms()), lifecycle_started).await; + let emitted = emit_message(&invoking_window, "Configuration KSP chargée.", "success"); + if let std::result::Result::Err(error) = emitted { + return std::result::Result::Err(error); + } + let emitted = emit_debug(&invoking_window, "Configuration et provenance des timings résolues via ksp-config-lib."); + if let std::result::Result::Err(error) = emitted { + return std::result::Result::Err(error); + } + let emitted = emit_message(&invoking_window, "Runtime Logging initialisé.", "success"); + if let std::result::Result::Err(error) = emitted { + return std::result::Result::Err(error); + } + let emitted = emit_message(&invoking_window, "Scaffold desktop prêt.", "success"); + if let std::result::Result::Err(error) = emitted { + return std::result::Result::Err(error); + } + sleep_and_log(&invoking_window, "minimum", settings.minimum_ms(), lifecycle_started).await; + let emitted = emit_message(&invoking_window, "Initialisation terminée.", "success"); + if let std::result::Result::Err(error) = emitted { + return std::result::Result::Err(error); + } + let emitted = emit_order( + &invoking_window, + crate::SplashOrderDto::new("fade_out", std::option::Option::None, std::option::Option::None, std::option::Option::Some(settings.fade_out_ms())), + ); + if let std::result::Result::Err(error) = emitted { + return std::result::Result::Err(error); + } + sleep_and_log(&invoking_window, "fade-out", u64::from(settings.fade_out_ms()), lifecycle_started).await; + let show_main = crate::show_main_window(&app); + if let std::result::Result::Err(error) = show_main { + return std::result::Result::Err(error); + } + let destroyed = invoking_window.destroy(); + if let std::result::Result::Err(error) = destroyed { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot destroy Backfill Desk splash window") + .with_context("window_label", WINDOW_LABEL_SPLASH) + .with_source(error), + ); + } + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + domain = crate::TRACING_DOMAIN_WINDOWS, + window_label = WINDOW_LABEL_SPLASH, + expected_backend_lifecycle_ms = settings.expected_backend_lifecycle_ms(), + actual_backend_lifecycle_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0, + "splash window destroyed after main activation" + ); + return std::result::Result::Ok(()); +} + +fn emit_message(window: &tauri::WebviewWindow, message: &str, status: &str) -> ksp_core_lib::Result<()> { + return emit_order( + window, + crate::SplashOrderDto::new("add_message", std::option::Option::Some(message), std::option::Option::Some(status), std::option::Option::None), + ); +} + +fn emit_debug(window: &tauri::WebviewWindow, message: &str) -> ksp_core_lib::Result<()> { + if !cfg!(debug_assertions) { + return std::result::Result::Ok(()); + } + return emit_order( + window, + crate::SplashOrderDto::new("add_debug", std::option::Option::Some(message), std::option::Option::None, std::option::Option::None), + ); +} + +async fn sleep_and_log(window: &tauri::WebviewWindow, phase: &str, duration_ms: u64, lifecycle_started: std::time::Instant) { + let wait_started = std::time::Instant::now(); + tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await; + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + domain = crate::TRACING_DOMAIN_WINDOWS, + window_label = window.label(), + phase, + configured_wait_ms = duration_ms, + actual_wait_ms = wait_started.elapsed().as_secs_f64() * 1000.0, + lifecycle_elapsed_ms = lifecycle_started.elapsed().as_secs_f64() * 1000.0, + "splash lifecycle wait completed" + ); + return; +} + +fn emit_order(window: &tauri::WebviewWindow, order: crate::SplashOrderDto) -> ksp_core_lib::Result<()> { + let action = order.action.clone(); + let emitted = window.emit(SPLASH_EVENT_NAME, order); + return match emitted { + std::result::Result::Ok(()) => { + ksp_logging_lib::trace!(target: crate::TRACING_TARGET, domain = crate::TRACING_DOMAIN_WINDOWS, window_label = WINDOW_LABEL_SPLASH, action = action.as_str(), "splash order emitted"); + std::result::Result::Ok(()) + }, + std::result::Result::Err(error) => std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_WINDOW_OPERATION_FAILED, "Cannot emit Backfill Desk splash order") + .with_context("window_label", WINDOW_LABEL_SPLASH) + .with_context("action", action) + .with_source(error), + ), + }; +} diff --git a/crates/ksp-app-backfill-desk/tauri.conf.json b/crates/ksp-app-backfill-desk/tauri.conf.json new file mode 100644 index 0000000..d58ae8f --- /dev/null +++ b/crates/ksp-app-backfill-desk/tauri.conf.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "KSP Backfill Desk", + "identifier": "com.sasedev.ksp-app-backfill-desk", + "build": { + "beforeDevCommand": { + "script": "npm run dev", + "cwd": "." + }, + "devUrl": "http://localhost:1436", + "beforeBuildCommand": { + "script": "npm run build", + "cwd": "." + }, + "frontendDist": "../../../builds/khadhroony-solana-project/ksp-app-backfill-desk/dist" + }, + "app": { + "windows": [ + { + "label": "splash", + "url": "splash.html", + "title": "Chargement — KSP Backfill Desk", + "width": 960, + "height": 637, + "resizable": false, + "decorations": false, + "transparent": true, + "center": true, + "alwaysOnTop": true + }, + { + "label": "main", + "url": "main.html", + "title": "KSP Backfill 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" + ] + } +} diff --git a/crates/ksp-app-backfill-desk/tests/desktop_contract.rs b/crates/ksp-app-backfill-desk/tests/desktop_contract.rs new file mode 100644 index 0000000..d5c768d --- /dev/null +++ b/crates/ksp-app-backfill-desk/tests/desktop_contract.rs @@ -0,0 +1,121 @@ +// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs +// version: 1 + +//! Structural desktop contract checks for the Backfill Desk scaffold. + +#![forbid(unsafe_code)] +#![deny(unreachable_pub)] +#![warn(missing_docs)] + +fn app_root() -> std::path::PathBuf { + return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); +} + +fn workspace_root() -> std::path::PathBuf { + return app_root().join("../.."); +} + +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_bytes(path: &std::path::Path) -> std::vec::Vec { + let source = std::fs::read(path); + assert!(source.is_ok(), "unable to read {}", path.display()); + return match source { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => std::vec::Vec::new(), + }; +} + +#[test] +fn pre_002_rust_package_is_mixed_lib_bin_and_workspace_member() { + let root = app_root(); + let manifest = read_text(root.join("Cargo.toml").as_path()); + assert!(manifest.contains("name = \"ksp-app-backfill-desk\"")); + assert!(manifest.contains("name = \"ksp_app_backfill_desk_lib\"")); + assert!(manifest.contains("[[bin]]")); + let workspace = read_text(workspace_root().join("Cargo.toml").as_path()); + assert!(workspace.contains("crates/ksp-app-backfill-desk")); + let launcher = read_text(root.join("src/main.rs").as_path()); + assert!(launcher.contains("ksp_app_backfill_desk_lib::run")); + assert!(launcher.contains("try_lock_exclusive")); +} + +#[test] +fn pre_002_vite_and_tauri_identity_use_reserved_ports_and_crate_local_hooks() { + let root = app_root(); + let vite = read_text(root.join("vite.config.ts").as_path()); + assert!(vite.contains("port: 1436")); + assert!(vite.contains("port: 1437")); + assert!(vite.contains("strictPort: true")); + let tauri = read_text(root.join("tauri.conf.json").as_path()); + assert!(tauri.contains("KSP Backfill Desk")); + assert!(tauri.contains("com.sasedev.ksp-app-backfill-desk")); + assert!(tauri.contains("http://localhost:1436")); + assert!(tauri.contains("npm run dev")); + assert!(tauri.contains("npm run build")); + assert!(tauri.contains("../../../builds/khadhroony-solana-project/ksp-app-backfill-desk/dist")); +} + +#[test] +fn pre_002_package_dependencies_match_desk_baseline() { + let package = read_text(app_root().join("package.json").as_path()); + for required in [ + "@fltsci/tauri-plugin-tracing", + "@fortawesome/fontawesome-free", + "@tauri-apps/api", + "bootstrap", + "resize-observer-polyfill", + "simplebar", + "@tauri-apps/cli", + "@types/bootstrap", + "@types/node", + "sass-embedded", + "typescript", + "vite", + ] { + assert!(package.contains(required), "missing Desk package baseline dependency {required}"); + } + assert!(!package.contains("package-lock")); +} + +#[test] +fn pre_002_splash_assets_and_styles_match_existing_desk_template() { + let root = app_root(); + let reference = workspace_root().join("crates/ksp-app-solprices-desk"); + for relative in ["frontend/fonts/DOS_Amazigh.ttf", "frontend/imgs/logo.png", "frontend/imgs/splash.png", "icons/favicon.png", "icons/favicon.ico"] { + assert_eq!(read_bytes(root.join(relative).as_path()), read_bytes(reference.join(relative).as_path()), "Desk asset differs: {relative}"); + } + let splash = read_text(root.join("frontend/splash.html").as_path()); + assert!(splash.contains("imgs/splash.png")); + assert!(splash.contains("Backfill Desk")); + let splash_style = read_text(root.join("frontend/sass/splash.scss").as_path()); + let reference_style = read_text(reference.join("frontend/sass/splash.scss").as_path()); + let normalized = splash_style.replace("crates/ksp-app-backfill-desk", "crates/ksp-app-solprices-desk").replace("// version: 1", "// version: 2"); + assert_eq!(normalized, reference_style); +} + +#[test] +fn pre_002_shell_and_splash_use_shared_frontend_tooling() { + let root = app_root(); + let shell = read_text(root.join("frontend/main.html").as_path()); + for required in ["app-header", "app-sidebar", "app-sidebar-scroll", "app-content", "app-footer", "data-simplebar"] { + assert!(shell.contains(required), "Backfill Desk shell is missing template marker {required}"); + } + let main = read_text(root.join("frontend/ts/main.ts").as_path()); + assert!(main.contains("resize-observer-polyfill")); + assert!(main.contains("simplebar")); + assert!(main.contains("bootstrap")); + assert!(main.contains("Backfill Desk frontend control clicked")); + assert!(main.contains("Backfill Desk navigation tab clicked")); + assert!(main.contains("Backfill Desk runtime status refresh button clicked")); + let splash = read_text(root.join("frontend/ts/splash.ts").as_path()); + assert!(splash.contains("Splash opacity animation started")); + assert!(splash.contains("Backfill Desk splash frontend loaded")); +} diff --git a/crates/ksp-app-backfill-desk/tests/desktop_security.rs b/crates/ksp-app-backfill-desk/tests/desktop_security.rs new file mode 100644 index 0000000..e13821c --- /dev/null +++ b/crates/ksp-app-backfill-desk/tests/desktop_security.rs @@ -0,0 +1,115 @@ +// file: crates/ksp-app-backfill-desk/tests/desktop_security.rs +// version: 1 + +//! Security and dependency-boundary checks for the Backfill Desk scaffold. + +#![forbid(unsafe_code)] +#![deny(unreachable_pub)] +#![warn(missing_docs)] + +fn app_root() -> std::path::PathBuf { + return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); +} + +fn collect_files(directory: &std::path::Path, extension: &str, files: &mut std::vec::Vec) { + let entries = std::fs::read_dir(directory); + let entries = match entries { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + for entry in entries { + let entry = match entry { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => continue, + }; + let path = entry.path(); + if path.is_dir() { + collect_files(path.as_path(), extension, files); + continue; + } + if path.extension().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some(extension) { + files.push(path); + } + } +} + +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 pre_002_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\"")); + for forbidden in ["dialog:", "fs:", "http:", "shell:"] { + assert!(!capability.contains(forbidden)); + } + let manifest = read_text(root.join("Cargo.toml").as_path()); + assert!(manifest.contains("tauri-plugin-tracing.workspace = true")); + for forbidden in + ["ksp-job-backfill-lib", "ksp-onchain-transport-lib", "ksp-store-lib", "ksp-store-api", "ksp-store-postgres-lib", "reqwest", "tokio-postgres"] + { + assert!(!manifest.contains(forbidden), "pre.002 opens a later-layer dependency: {forbidden}"); + } +} + +#[test] +fn pre_002_frontend_has_no_network_persistence_or_native_dialog_surface() { + let root = app_root(); + let mut typescript = std::vec::Vec::new(); + collect_files(root.join("frontend/ts").as_path(), "ts", &mut typescript); + assert!(!typescript.is_empty()); + for path in typescript { + let source = read_text(path.as_path()); + for forbidden in ["fetch(", "XMLHttpRequest", "localStorage", "sessionStorage", "window.alert(", "window.confirm(", "window.prompt("] { + assert!(!source.contains(forbidden), "{} contains forbidden scaffold surface {forbidden}", path.display()); + } + } +} + +#[test] +fn pre_002_tauri_commands_remain_centralized() { + let root = app_root(); + let mut rust_files = std::vec::Vec::new(); + collect_files(root.join("src").as_path(), "rs", &mut rust_files); + let mut command_count = 0usize; + for path in rust_files { + let source = read_text(path.as_path()); + let count = source.matches("#[tauri::command]").count(); + if path.file_name().and_then(std::ffi::OsStr::to_str) == std::option::Option::Some("tauri.rs") { + command_count += count; + } else { + assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display()); + } + } + assert_eq!(command_count, 3); +} + +#[test] +fn pre_002_frontend_instrumentation_avoids_business_or_secret_payloads() { + let root = app_root(); + let main = read_text(root.join("frontend/ts/main.ts").as_path()); + for required in [ + "Backfill Desk frontend control clicked", + "Backfill Desk navigation tab clicked", + "Backfill Desk runtime status refresh button clicked", + "Frontend IPC command requested", + ] { + if required == "Frontend IPC command requested" { + let invoke = read_text(root.join("frontend/ts/invoke.ts").as_path()); + assert!(invoke.contains(required)); + } else { + assert!(main.contains(required)); + } + } + for forbidden in ["JSON.stringify(status)", "signature", "address", "apiKey", "authorization", "database_url"] { + assert!(!main.contains(forbidden)); + } +} diff --git a/crates/ksp-app-backfill-desk/tests/public_api.rs b/crates/ksp-app-backfill-desk/tests/public_api.rs new file mode 100644 index 0000000..9e7ccee --- /dev/null +++ b/crates/ksp-app-backfill-desk/tests/public_api.rs @@ -0,0 +1,14 @@ +// file: crates/ksp-app-backfill-desk/tests/public_api.rs +// version: 1 + +//! Public API checks for the KSP Backfill 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_backfill_desk_lib::run; + assert_eq!(std::mem::size_of_val(&run_entry_point), std::mem::size_of:: ksp_core_lib::Result<()>>()); +} diff --git a/crates/ksp-app-backfill-desk/tsconfig.json b/crates/ksp-app-backfill-desk/tsconfig.json new file mode 100644 index 0000000..b5e3e21 --- /dev/null +++ b/crates/ksp-app-backfill-desk/tsconfig.json @@ -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" + ] +} diff --git a/crates/ksp-app-backfill-desk/unit_tests/dto_common.rs b/crates/ksp-app-backfill-desk/unit_tests/dto_common.rs new file mode 100644 index 0000000..2eb85b7 --- /dev/null +++ b/crates/ksp-app-backfill-desk/unit_tests/dto_common.rs @@ -0,0 +1,14 @@ +// file: crates/ksp-app-backfill-desk/unit_tests/dto_common.rs +// version: 1 + +#[test] +fn command_error_projection_excludes_arbitrary_context_and_source_values() { + let source = std::io::Error::other("secret-source"); + let error = ksp_core_lib::Error::new(ksp_core_lib::ErrorCode::new("test", "failed"), "safe message") + .with_context("secret", "hidden") + .with_source(source); + let dto = crate::CommandErrorDto::from_error(&error); + assert_eq!(dto.domain, "test"); + assert_eq!(dto.code, "failed"); + assert_eq!(dto.message, "safe message"); +} diff --git a/crates/ksp-app-backfill-desk/unit_tests/frontend_logging.rs b/crates/ksp-app-backfill-desk/unit_tests/frontend_logging.rs new file mode 100644 index 0000000..f1bc735 --- /dev/null +++ b/crates/ksp-app-backfill-desk/unit_tests/frontend_logging.rs @@ -0,0 +1,40 @@ +// file: crates/ksp-app-backfill-desk/unit_tests/frontend_logging.rs +// version: 1 + +#[test] +fn frontend_log_levels_are_explicit_and_case_insensitive() { + assert!(matches!(super::parse_level(" TRACE "), std::result::Result::Ok(super::FrontendLogLevel::Trace))); + assert!(matches!(super::parse_level("debug"), std::result::Result::Ok(super::FrontendLogLevel::Debug))); + assert!(matches!(super::parse_level("Info"), std::result::Result::Ok(super::FrontendLogLevel::Info))); + assert!(matches!(super::parse_level("warn"), std::result::Result::Ok(super::FrontendLogLevel::Warn))); + assert!(matches!(super::parse_level("ERROR"), std::result::Result::Ok(super::FrontendLogLevel::Error))); +} + +#[test] +fn unsupported_frontend_log_level_is_rejected() { + let result = super::parse_level("notice"); + assert!(result.is_err()); + let error = match result { + std::result::Result::Ok(_) => return, + std::result::Result::Err(error) => error, + }; + assert_eq!(error.code(), crate::ERROR_CODE_FRONTEND_LOG_LEVEL_INVALID); +} + +#[test] +fn frontend_log_targets_are_whitelisted() { + assert!(matches!(super::parse_target("frontend"), std::result::Result::Ok(super::FrontendLogTarget::Frontend))); + assert!(matches!(super::parse_target(" MAIN "), std::result::Result::Ok(super::FrontendLogTarget::Main))); + assert!(matches!(super::parse_target("Splash"), std::result::Result::Ok(super::FrontendLogTarget::Splash))); +} + +#[test] +fn arbitrary_frontend_log_target_is_rejected() { + let result = super::parse_target("ksp-app-backfill-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); +} diff --git a/crates/ksp-app-backfill-desk/unit_tests/splash.rs b/crates/ksp-app-backfill-desk/unit_tests/splash.rs new file mode 100644 index 0000000..2cf77b7 --- /dev/null +++ b/crates/ksp-app-backfill-desk/unit_tests/splash.rs @@ -0,0 +1,36 @@ +// file: crates/ksp-app-backfill-desk/unit_tests/splash.rs +// version: 1 + +#[test] +fn fallback_splash_settings_are_short_and_ordered() { + let settings = crate::SplashSettings::fallback(); + assert_eq!(settings.fade_in_ms(), 300); + assert_eq!(settings.minimum_ms(), 1200); + assert_eq!(settings.fade_out_ms(), 300); + assert_eq!(settings.expected_backend_lifecycle_ms(), 1800); + assert_eq!(settings.fade_in_source(), "fallback"); + assert_eq!(settings.minimum_source(), "fallback"); + assert_eq!(settings.fade_out_source(), "fallback"); +} + +#[test] +fn splash_order_contract_keeps_action_message_status_and_duration() { + let order = + crate::SplashOrderDto::new("add_message", std::option::Option::Some("ready"), std::option::Option::Some("success"), std::option::Option::Some(300)); + assert_eq!(order.action, "add_message"); + assert_eq!(order.message.as_deref(), std::option::Option::Some("ready")); + assert_eq!(order.status.as_deref(), std::option::Option::Some("success")); + assert_eq!(order.duration_ms, std::option::Option::Some(300)); +} + +#[test] +fn splash_duration_parser_rejects_invalid_or_unbounded_values() { + let malformed = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "invalid", 60_000); + let oversized_minimum = super::parse_u64_setting("KSP_DESK_SPLASH_MINIMUM_MS", "60001", 60_000); + let oversized_fade_in = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_IN_MS", "10001", 10_000); + let oversized_fade_out = super::parse_u32_setting("KSP_DESK_SPLASH_FADE_OUT_MS", "10001", 10_000); + assert!(malformed.is_err()); + assert!(oversized_minimum.is_err()); + assert!(oversized_fade_in.is_err()); + assert!(oversized_fade_out.is_err()); +} diff --git a/crates/ksp-app-backfill-desk/vite.config.ts b/crates/ksp-app-backfill-desk/vite.config.ts new file mode 100644 index 0000000..cdf287b --- /dev/null +++ b/crates/ksp-app-backfill-desk/vite.config.ts @@ -0,0 +1,72 @@ +// file: crates/ksp-app-backfill-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-backfill-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: 1436, + strictPort: true, + host: devHost || false, + ws: { + protocol: "ws", + host: devHost || "localhost", + port: 1437, + }, + watch: { + ignored: ["**/src/**"], + }, + }, +}); diff --git a/deltas/0.3.7/pre.002.md b/deltas/0.3.7/pre.002.md new file mode 100644 index 0000000..5820260 --- /dev/null +++ b/deltas/0.3.7/pre.002.md @@ -0,0 +1,161 @@ + + + +# Delta `0.3.7-pre.002` — scaffold `ksp-app-backfill-desk` + +## 1. Base requise + +```text +0.3.7-pre.001 appliquée +workspace.package.version = 0.3.7-pre.1 +``` + +La version workspace passe à `0.3.7-pre.2`. + +## 2. Objectif + +Créer uniquement le gabarit desktop Tauri de Backfill Desk avant toute composition Transport/Store/Backfill fonctionnelle. + +La tranche fournit : package Rust mixte lib/bin, launcher single-instance, Config/Logging standard transitoire, splash commun KSP, shell `Backfill / Diagnostics`, bridge Logging frontend, ports Vite/HMR 1436/1437, capabilities minimales et canaris desktop/security/public API. + +## 3. Gabarit Desk réutilisé + +Le scaffold suit `KSP-APP-008/015/023/025/027` et les contraintes opérateur confirmées pour cette release : + +```text +même splash.png +même logo/favicon +même font DOS_Amazigh.ttf +même splash.scss, hors header de propriété +même baseline Bootstrap / Bootswatch / Font Awesome / SimpleBar +mêmes scripts npm dev/build +mêmes devDependencies que les Desk existantes +``` + +Le splash change uniquement son identité textuelle vers `Backfill Desk`; sa géométrie, son image, sa font et son lifecycle restent ceux du gabarit. + +`package.json` contient au minimum : + +```text +@fltsci/tauri-plugin-tracing ^0.3 +@fortawesome/fontawesome-free ^7.3 +@tauri-apps/api ^2.11 +bootstrap ^5.3 +resize-observer-polyfill ^1.5 +simplebar ^6.3 +``` + +avec : + +```text +@tauri-apps/cli ^2.11 +@types/bootstrap ^5.2 +@types/node ^26.1 +sass-embedded ^1.102 +typescript ^7.0 +vite ^8.2 +``` + +## 4. Frontend et instrumentation + +Le shell reste volontairement non métier : la vue Backfill est un placeholder et Diagnostics expose seulement version, phase, nombre de documents Config, profil Logging et état fallback. + +Le frontend trace via le bridge KSP : chargement main/splash, clics génériques sur les contrôles, clics tabs, transitions de vue, clic Refresh diagnostics, début/fin/échec IPC, rendu du status et ordres/animations splash. Aucun payload Backfill, adresse, signature, endpoint, URI Store ou secret n'est journalisé. + +## 5. Frontières préservées + +`pre.002` n'ajoute volontairement aucune dépendance applicative vers : + +```text +ksp-job-backfill-lib +ksp-onchain-transport-lib +ksp-store-lib +ksp-store-api +ksp-store-postgres-lib +reqwest +tokio-postgres +``` + +Aucun RPC, Store, job, checkpoint, scope, provider, endpoint, scheduler, retry ou payload RAW n'est ouvert dans le frontend ou le runtime applicatif. Le composite Backfill Desk et le packaging Config restent à `pre.003`. + +## 6. Fichiers ajoutés + +```text +crates/ksp-app-backfill-desk/** +deltas/0.3.7/pre.002.md +``` + +Les bindings TS-RS et autres artefacts générés ne sont pas versionnés. + +## 7. Fichiers modifiés + +```text +Cargo.toml +docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md +docs/validation/024-V0_3_7_BACKFILL_DESK.md +``` + +## 8. Fichiers supprimés + +Aucun. + +## 9. Validations exécutées + +```text +python3 scripts/audit_rust_workspace_rules.py +General Rust rule audit: clean +Rust export completeness audit: 0 candidate(s) +KSP workspace Rust rule audit: clean + +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.7 +Markdown table audit: clean (270 table(s), 149 file(s)) + +Manifest parse audit: clean (4 JSON, 2 TOML) +Desk package baseline audit: clean (6 mandatory dependencies; devDependencies exact across 3 references) +Desk binary asset audit: clean (5 assets byte-identical across 3 references) +Desk shared SASS audit: clean (5 shared files body-identical to SOL Prices Desk) +Desk splash template audit: clean (HTML/SCSS identical modulo Backfill identity and file metadata) +Desk shell skeleton audit: clean (header/sidebar/content/footer + SimpleBar) +Frontend interaction/security audit: clean +pre.002 dependency/capability boundary audit: clean +Desk port audit: clean (1436/1437 strict) +TypeScript syntax audit: clean (5 file(s)) +Delta scope audit: clean (46 added, 3 modified, 0 deleted; 45 app files + delta) +``` + +Le contrôle TypeScript est un contrôle syntaxique par transpilation avec le compilateur global disponible (`tsc 5.8.3`) ; il ne remplace ni le typecheck complet avec les dépendances npm installées et bindings TS-RS générés, ni le build Tauri. + +## 10. Validations non exécutées dans le sandbox + +Le sandbox ne fournit ni `cargo` ni `rustfmt`; les commandes Rust obligatoires ne sont donc jamais déclarées PASS localement. + +Gate opérateur attendu : + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.7 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-app-backfill-desk +cargo tree -p ksp-app-backfill-desk --edges normal +cargo tree -p ksp-app-backfill-desk -e features +``` + +Une correction découverte par ce gate reste un `pre.002-fix.NNN`. + +## 11. Décisions prises + +- `ksp-app-backfill-desk` est le nom fixé de l'application `0.3.7`. +- 1436/1437 sont les ports exclusifs du Desk. +- le gabarit visuel et frontend des Desk existantes est repris comme contrat, sans reprendre de logique métier kbot3 ; +- le Logging standard est utilisé transitoirement pour que l'instrumentation frontend soit effective dès le scaffold ; `pre.003` introduira le composite Backfill Desk ; +- les capabilities restent `core:default + tracing:default`. + +## 12. Questions ouvertes + +Aucune question bloquante pour `pre.002`. + +## 13. Suite + +`0.3.7-pre.003` reste dédié au composite Config Backfill Desk, aux resources bundle minimales et au bootstrap composite Logging/Transport/Store, sans encore construire le runtime Transport réel. diff --git a/docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md b/docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md index 5c9e278..655587e 100644 --- a/docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md +++ b/docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md @@ -1,5 +1,5 @@ - + # Plan v0.3.7 — Backfill Desk @@ -231,7 +231,7 @@ Archives/règles/surfaces, matrice kbot3, screen map, DTO/commands, Config/compo ### pre.002 — scaffold desktop minimal -Créer package lib+bin, shell/splash/tracing/capability, frontend minimal, ports Vite/HMR 1436/1437 stricts et tests desktop de structure. +Créer package lib+bin, shell/splash/tracing/capability, frontend minimal, ports Vite/HMR 1436/1437 stricts et tests desktop de structure. Le scaffold réutilise strictement le gabarit des Desk existantes : mêmes assets splash/logo/font, mêmes familles SASS/Bootstrap/SimpleBar, mêmes dépendances frontend minimales et mêmes `devDependencies`. Le bridge Logging frontend trace dès cette tranche les clics/onglets/transitions significatives ; le bootstrap Logging standard reste transitoire jusqu'au composite Backfill Desk de `pre.003`. ### pre.003 — Config composite et packaging diff --git a/docs/validation/024-V0_3_7_BACKFILL_DESK.md b/docs/validation/024-V0_3_7_BACKFILL_DESK.md index 6395e6f..b6a3542 100644 --- a/docs/validation/024-V0_3_7_BACKFILL_DESK.md +++ b/docs/validation/024-V0_3_7_BACKFILL_DESK.md @@ -1,5 +1,5 @@ - + # Validation v0.3.7 — Backfill Desk @@ -109,3 +109,29 @@ Le journal opérateur fourni rapporte `cargo clean`, `cargo fmt`, audits Rust/Ma - [ ] Smoke live opt-in exécuté si environnement sûr disponible, sinon explicitement non requis. - [ ] Réconciliation README/USAGE/plan/validation dans sa tranche dédiée. - [ ] Prompt 0.3.8 + CHANGELOG + ROADMAP uniquement dans la lane de publication. + +## 9. pre.002 — scaffold desktop + +Le scaffold `ksp-app-backfill-desk` matérialise le gabarit Desk avant toute ouverture Transport/Store/Backfill : lib+bin, verrou single-instance, Config/Logging standard transitoire, splash commun, bridge Logging frontend, shell `Backfill / Diagnostics`, ports 1436/1437 et capabilities `core:default + tracing:default`. + +Contraintes de gabarit vérifiées statiquement : mêmes assets splash/logo/DOS Amazigh que les trois Desk existantes, corps des cinq fichiers SASS partagés identique à SOL Prices Desk, splash HTML/SCSS identique hors identité Backfill et métadonnées de fichier, shell commun `header + sidebar + content + footer`, dépendances frontend minimales `@fltsci/tauri-plugin-tracing`, Font Awesome, Tauri API, Bootstrap, ResizeObserver polyfill et SimpleBar, plus les mêmes `devDependencies` que les trois Desk. Les clics, tabs, transitions de vue, refresh diagnostic et IPC sont instrumentés sans payload Backfill, adresse, signature, endpoint ou secret. + +### Gate statique local + +- [X] audit Rust workspace propre, zéro candidat d'export ; +- [X] audit Markdown propre : 270 tableaux / 149 fichiers ; +- [X] 4 manifests JSON et 2 manifests TOML parsés ; +- [X] 6 dépendances frontend minimales aux versions du gabarit ; +- [X] `devDependencies` strictement identiques dans Config Desk, Wallet Desk, SOL Prices Desk et Backfill Desk ; +- [X] 5 assets binaires strictement identiques dans les quatre Desk ; +- [X] 5 fichiers SASS partagés identiques à SOL Prices Desk hors header de propriété ; +- [X] splash commun et shell sidebar contrôlés ; +- [X] capabilities `core:default + tracing:default` seulement ; +- [X] aucune dépendance Transport/Store/Backfill ouverte en `pre.002` ; +- [X] audit frontend interaction/security propre ; +- [X] audit syntaxique TypeScript propre sur 5 fichiers ; +- [X] diff `pre.001 -> pre.002` borné à 46 ajouts, 3 modifications et aucune suppression ; +- [ ] `cargo fmt/check/clippy/test` : non exécutés, `cargo`/`rustfmt` absents du sandbox ; +- [ ] typecheck npm complet / build Tauri : non exécutés dans le sandbox. + +Le contrôle TypeScript local est syntaxique avec le compilateur global disponible ; il ne remplace pas le typecheck complet avec dépendances npm et bindings TS-RS générés. Toute correction révélée par le gate opérateur reste dans la lane `pre.002-fix.NNN`.