v0.3.7-pre.008

This commit is contained in:
2026-09-02 16:56:11 +02:00
parent 6e406a6b84
commit 48d26b43f9
19 changed files with 693 additions and 30 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-backfill-desk/frontend/main.html -->
<!-- version: 3 -->
<!-- version: 4 -->
<!DOCTYPE html>
<html lang="fr">
@@ -121,10 +121,13 @@
</div>
</div>
<div class="d-flex align-items-center gap-3 flex-wrap mt-4">
<button id="validateBackfillRequest" class="btn btn-primary" type="submit" disabled>
<button id="validateBackfillRequest" class="btn btn-outline-primary" type="submit" disabled>
<i class="fa-solid fa-shield-halved me-2" aria-hidden="true"></i>Valider la requête
</button>
<span class="text-body-secondary small">Cette tranche ne démarre aucun job.</span>
<button id="startBackfillRequest" class="btn btn-primary" type="button" disabled>
<i class="fa-solid fa-play me-2" aria-hidden="true"></i>Démarrer le backfill
</button>
<span class="text-body-secondary small">Un seul run peut être actif ; monitoring détaillé en pre.009.</span>
</div>
</form>
<div id="backfillRequestValidation" class="alert alert-secondary mt-4 mb-0" role="status" aria-live="polite" hidden></div>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
// version: 3
// version: 4
import "bootstrap";
import ResizeObserver from "resize-observer-polyfill";
@@ -7,6 +7,7 @@ import "simplebar";
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { BackfillRequestPreviewDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillRequestPreviewDto.ts";
import type { BackfillStartRequestDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartRequestDto.ts";
import type { BackfillStartResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartResponseDto.ts";
import type { BackfillDeskOptionsDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillDeskOptionsDto.ts";
import type { BackfillHttpRouteOptionDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillHttpRouteOptionDto.ts";
import type { ShellStatusDto } from "./bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts";
@@ -35,6 +36,9 @@ const scopeLabels: Record<string, string> = {
explicit_signatures: "Signatures explicites",
};
let compositionReadyForStart = false;
let runStartAccepted = false;
function isViewId(value: string | undefined): value is ViewId {
return value === "backfill" || value === "diagnostics";
@@ -125,6 +129,13 @@ function applyNumberInputContract(inputId: string, maximum: number, defaultValue
}
}
function refreshStartButton(): void {
const startButton = document.querySelector<HTMLButtonElement>("#startBackfillRequest");
if (startButton) {
startButton.disabled = !compositionReadyForStart || runStartAccepted;
}
}
function renderCampaignContract(options: BackfillDeskOptionsDto): void {
populateCodeSelect("backfillScopeKind", options.scopeKinds, scopeLabels);
populateCodeSelect("backfillCommitment", options.commitments, commitmentLabels);
@@ -293,6 +304,31 @@ function renderRequestValidationError(caughtError: unknown): void {
status.textContent = message;
}
async function startBackfillRequest(): Promise<void> {
const request = buildBackfillStartRequest();
frontendDebug("main", "Backfill Desk campaign Start requested", {
commitment: request.commitment,
httpRole: request.httpRole,
scopeKind: request.scopeKind,
});
try {
const started = await invokeKsp<BackfillStartResponseDto>("main", "backfill_start", { request });
runStartAccepted = true;
refreshStartButton();
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
if (status) {
status.hidden = false;
status.classList.remove("alert-secondary", "alert-danger");
status.classList.add("alert-success");
status.textContent = `Job ${started.jobId} démarré (${started.state}). Le monitoring détaillé arrive en pre.009.`;
}
frontendInfo("main", "Backfill Desk campaign Start accepted", { jobId: started.jobId, state: started.state });
} catch (caughtError) {
renderRequestValidationError(caughtError);
frontendWarn("main", "Backfill Desk campaign Start failed");
}
}
async function validateBackfillRequest(): Promise<void> {
const request = buildBackfillStartRequest();
frontendDebug("main", "Backfill Desk campaign validation requested", {
@@ -331,6 +367,12 @@ function bindCampaignForm(): void {
void validateBackfillRequest();
});
}
const startButton = document.querySelector<HTMLButtonElement>("#startBackfillRequest");
if (startButton) {
startButton.addEventListener("click", () => {
void startBackfillRequest();
});
}
frontendTrace("main", "Backfill Desk campaign form handlers installed");
}
@@ -342,7 +384,9 @@ function routeLabel(route: BackfillHttpRouteOptionDto): string {
}
function renderBackfillOptions(options: BackfillDeskOptionsDto): void {
compositionReadyForStart = options.compositionReady;
renderCampaignContract(options);
refreshStartButton();
const select = document.querySelector<HTMLSelectElement>("#backfillHttpRoute");
if (select) {
const previous = select.value;

View File

@@ -1,10 +1,11 @@
// file: crates/ksp-app-backfill-desk/src/app_state.rs
// version: 5
// version: 6
//! Shared backend state owned by the Backfill Desk Tauri application.
/// Shared Backfill Desk application state managed by Tauri.
pub(crate) struct AppState {
backfill_runs: crate::BackfillRunState,
config_management: ksp_config_lib::ConfigManagement,
logging_runtime: std::sync::Mutex<LoggingRuntimeState>,
shutdown_started: std::sync::atomic::AtomicBool,
@@ -76,6 +77,7 @@ impl crate::AppState {
"resolved Backfill Desk splash timings"
);
return std::result::Result::Ok(Self {
backfill_runs: crate::BackfillRunState::new(),
config_management,
logging_runtime: std::sync::Mutex::new(LoggingRuntimeState {
guard: logging_startup.guard,
@@ -121,7 +123,7 @@ impl crate::AppState {
application_version: env!("CARGO_PKG_VERSION").to_owned(),
config_document_count: document_count,
fallback_logging_active: runtime.fallback_active,
shell_phase: "pre.007-request-mapping".to_owned(),
shell_phase: "pre.008-runtime-start".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(),
});
}
@@ -194,6 +196,103 @@ impl crate::AppState {
return std::result::Result::Ok(preview);
}
/// Prepares one concrete Backfill runtime, installs its control handle atomically and lends execution resources before spawn.
pub(crate) fn prepare_backfill_start(&self, request: crate::BackfillStartRequestDto) -> ksp_core_lib::Result<crate::BackfillRunLaunch> {
if self.shutdown_started.load(std::sync::atomic::Ordering::Acquire) {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
"Backfill Desk cannot start a Backfill run while application shutdown is in progress",
));
}
let options = self.backfill_options();
let options = match options {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let job_id = self.backfill_runs.next_job_id();
let job_id = match job_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mapped = crate::map_backfill_request(request, &options, job_id.clone());
let mapped = match mapped {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let runtime = ksp_job_backfill_lib::BackfillJobRuntime::new(mapped);
let runtime = match runtime {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let handle = runtime.handle();
let installed = self.backfill_runs.install(job_id.clone(), handle);
if let std::result::Result::Err(error) = installed {
return std::result::Result::Err(error);
}
let transport = self.transport_runtime.as_ref();
let transport = match transport {
std::option::Option::Some(value) => value.pool(),
std::option::Option::None => {
let rollback = self.backfill_runs.rollback(&job_id);
if let std::result::Result::Err(error) = rollback {
return std::result::Result::Err(error);
}
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
"Backfill Desk cannot start a Backfill run without a ready HTTP Transport pool",
));
},
};
let store = self.store_startup.take_for_run();
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
let rollback = self.backfill_runs.rollback(&job_id);
if let std::result::Result::Err(rollback_error) = rollback {
return std::result::Result::Err(rollback_error);
}
return std::result::Result::Err(error);
},
};
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_RUN,
job_id = job_id.as_str(),
"admitted Backfill Desk run and installed control handle before async spawn"
);
return std::result::Result::Ok(crate::BackfillRunLaunch { job_id, runtime, store, transport });
}
/// Executes one already-admitted Backfill launch and restores application resources after its terminal result.
pub(crate) async fn execute_backfill_run(&self, launch: crate::BackfillRunLaunch) -> ksp_core_lib::Result<()> {
let job_id = launch.job_id.clone();
let result = launch.runtime.run(&launch.transport, &launch.store).await;
let restore = self.store_startup.restore_after_run(launch.store);
let finished = self.backfill_runs.finish(&job_id);
let cancellation_requested = match finished {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = restore {
return std::result::Result::Err(error);
}
return match result {
std::result::Result::Ok(snapshot) => {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_RUN,
cancellation_requested = cancellation_requested,
contiguous_completed = snapshot.contiguous_completed(),
job_id = job_id.as_str(),
phase = snapshot.phase().code(),
"Backfill Desk run reached a terminal snapshot and released the single-run slot"
);
std::result::Result::Ok(())
},
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Marks graceful application shutdown as started and reports whether this caller won the one-shot transition.
pub(crate) fn begin_shutdown(&self) -> bool {
return self.shutdown_started.compare_exchange(false, true, std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire).is_ok();

View File

@@ -0,0 +1,131 @@
// file: crates/ksp-app-backfill-desk/src/backfill_run.rs
// version: 1
//! Single-active-run admission and launch ownership for Backfill Desk.
/// Fully prepared Backfill execution moved into the Tauri async runtime after admission.
pub(crate) struct BackfillRunLaunch {
/// Backend-generated logical Job identity.
pub(crate) job_id: ksp_job_api::JobId,
/// Concrete Backfill runtime owning discovery, hydration, persistence and latest-value publication.
pub(crate) runtime: ksp_job_backfill_lib::BackfillJobRuntime,
/// Store facade temporarily borrowed from the application Store runtime for the duration of this run.
pub(crate) store: ksp_store_lib::Store,
/// Shareable HTTP Transport pool selected by the active composite profile.
pub(crate) transport: ksp_onchain_transport_lib::HttpTransportPool,
}
impl crate::BackfillRunLaunch {
/// Builds the safe acknowledgement returned immediately after the non-blocking Start admission.
#[must_use]
pub(crate) fn response(&self) -> crate::BackfillStartResponseDto {
return crate::BackfillStartResponseDto { job_id: self.job_id.as_str().to_owned(), state: ksp_job_api::JobState::Created.code().to_owned() };
}
}
/// Single-run control slot retained by the application while one Backfill runtime is active.
pub(crate) struct BackfillRunState {
active: std::sync::Mutex<std::option::Option<ActiveBackfillRun>>,
next_sequence: std::sync::atomic::AtomicU64,
}
impl crate::BackfillRunState {
/// Creates an empty single-run state for one desktop application session.
#[must_use]
pub(crate) const fn new() -> Self {
return Self {
active: std::sync::Mutex::new(std::option::Option::None),
next_sequence: std::sync::atomic::AtomicU64::new(1),
};
}
/// Allocates one bounded backend-owned Job identity without accepting caller-supplied identity material.
pub(crate) fn next_job_id(&self) -> ksp_core_lib::Result<ksp_job_api::JobId> {
let sequence = self
.next_sequence
.fetch_update(std::sync::atomic::Ordering::AcqRel, std::sync::atomic::Ordering::Acquire, |current| return current.checked_add(1));
let sequence = match sequence {
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_INVALID,
"Backfill Desk Job identity sequence is exhausted",
));
},
};
return ksp_job_api::JobId::new(format!("backfill-desk-{sequence}"));
}
/// Installs one control handle atomically and rejects concurrent Starts while another run owns the slot.
pub(crate) fn install(&self, job_id: ksp_job_api::JobId, handle: ksp_job_backfill_lib::BackfillJobHandle) -> ksp_core_lib::Result<()> {
let active = self.active.lock();
let mut active = match active {
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 active-run state lock is poisoned",
));
},
};
if active.is_some() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_RUN_ACTIVE,
"Backfill Desk already has an active Backfill run",
));
}
*active = std::option::Option::Some(ActiveBackfillRun { handle, job_id });
return std::result::Result::Ok(());
}
/// Clears the matching terminal run and reports whether cancellation had been requested on its retained handle.
pub(crate) fn finish(&self, job_id: &ksp_job_api::JobId) -> ksp_core_lib::Result<bool> {
let active = self.active.lock();
let mut active = match active {
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 active-run state lock is poisoned",
));
},
};
let current = active.as_ref();
let current = match current {
std::option::Option::Some(value) if &value.job_id == job_id => value,
std::option::Option::Some(_) => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Backfill Desk terminal run does not match the active Job identity",
));
},
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Backfill Desk terminal run has no active admission slot",
));
},
};
let cancellation_requested = current.handle.is_cancellation_requested();
*active = std::option::Option::None;
return std::result::Result::Ok(cancellation_requested);
}
/// Rolls back a just-installed admission when execution resources cannot be acquired before spawn.
pub(crate) fn rollback(&self, job_id: &ksp_job_api::JobId) -> ksp_core_lib::Result<()> {
let finished = self.finish(job_id);
return match finished {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
struct ActiveBackfillRun {
handle: ksp_job_backfill_lib::BackfillJobHandle,
job_id: ksp_job_api::JobId,
}
#[cfg(test)]
#[path = "../unit_tests/backfill_run.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/constants.rs
// version: 5
// version: 6
//! Application-owned tracing targets and domains.
@@ -23,6 +23,8 @@ pub(crate) const TRACING_DOMAIN_BOOTSTRAP: &str = "backfill.bootstrap";
pub(crate) const TRACING_DOMAIN_FRONTEND: &str = "frontend";
/// Structured domain used by Backfill campaign admission and request mapping.
pub(crate) const TRACING_DOMAIN_REQUEST: &str = "backfill.request";
/// Structured domain used by concrete Backfill run admission and execution.
pub(crate) const TRACING_DOMAIN_RUN: &str = "backfill.run";
/// Structured domain used by the Backfill Desk shell.
pub(crate) const TRACING_DOMAIN_SHELL: &str = "backfill.shell";
/// Structured domain used by Store readiness and shutdown operations.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/dto_backfill.rs
// version: 1
// version: 2
//! Application-owned Backfill campaign DTOs and backend-derived request limits.
@@ -65,6 +65,17 @@ pub(crate) struct BackfillStartRequestDto {
pub(crate) scope_kind: String,
}
/// Safe immediate acknowledgement returned after one Backfill run is admitted and spawned.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartResponseDto.ts")]
pub(crate) struct BackfillStartResponseDto {
/// Backend-generated bounded Job identifier for this in-session run.
pub(crate) job_id: String,
/// Initial lifecycle state at the time Start returns to the frontend.
pub(crate) state: String,
}
/// Safe projection proving that one app request mapped to the KSP Backfill contract.
///
/// Address and signature values are intentionally reduced to presence/count metadata. This DTO is

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/errors.rs
// version: 6
// version: 7
//! Application-local error codes for Backfill Desk composition and desktop runtime surfaces.
@@ -12,6 +12,8 @@ pub(crate) const ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY: ksp_core_lib::ErrorC
ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_composition_not_ready");
/// Backfill Desk received a campaign field or logical route that cannot map to the Backfill contract.
pub(crate) const ERROR_CODE_BACKFILL_REQUEST_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_request_invalid");
/// Backfill Desk refuses a concurrent Start while another Backfill run owns the single-run slot.
pub(crate) const ERROR_CODE_BACKFILL_RUN_ACTIVE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "backfill_run_active");
/// Backfill Desk composite configuration is missing or references an unexpected document.
pub(crate) const ERROR_CODE_CONFIG_COMPOSITE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("backfill_desk", "config_composite_invalid");
/// Frontend logging requested an unsupported level.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/lib.rs
// version: 6
// version: 7
//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs.
@@ -9,6 +9,7 @@
mod app_state;
mod backfill_request;
mod backfill_run;
mod bootstrap;
mod constants;
mod dto_backfill;
@@ -32,6 +33,10 @@ pub(crate) use self::app_state::AppState;
pub(crate) use self::backfill_request::map_backfill_request;
/// Projects one validated Backfill request without returning address or signature values.
pub(crate) use self::backfill_request::project_backfill_request;
/// Fully prepared Backfill launch moved into the async runtime after single-run admission.
pub(crate) use self::backfill_run::BackfillRunLaunch;
/// Single-active-run admission state retained by the application.
pub(crate) use self::backfill_run::BackfillRunState;
/// 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.
@@ -62,6 +67,8 @@ pub(crate) use self::constants::TRACING_DOMAIN_BOOTSTRAP;
pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND;
/// Structured domain used by Backfill campaign admission and request mapping.
pub(crate) use self::constants::TRACING_DOMAIN_REQUEST;
/// Structured domain used by concrete Backfill run admission and execution.
pub(crate) use self::constants::TRACING_DOMAIN_RUN;
/// Structured domain used by the Backfill Desk shell.
pub(crate) use self::constants::TRACING_DOMAIN_SHELL;
/// Structured domain used by Store readiness and shutdown operations.
@@ -84,6 +91,8 @@ pub(crate) use self::dto_backfill::BackfillRequestLimitsDto;
pub(crate) use self::dto_backfill::BackfillRequestPreviewDto;
/// App-owned campaign request received from the frontend.
pub(crate) use self::dto_backfill::BackfillStartRequestDto;
/// Safe immediate acknowledgement returned after one Backfill run is admitted and spawned.
pub(crate) use self::dto_backfill::BackfillStartResponseDto;
/// Returns commitment codes admitted by the current Backfill contract.
pub(crate) use self::dto_backfill::backfill_commitment_codes;
/// Builds frontend-safe campaign limits from Job-owned public constants.
@@ -106,6 +115,8 @@ pub(crate) use self::errors::ERROR_CODE_APP_STATE_LOCK_FAILED;
pub(crate) use self::errors::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY;
/// Backfill Desk received a campaign field that cannot map to the Backfill contract.
pub(crate) use self::errors::ERROR_CODE_BACKFILL_REQUEST_INVALID;
/// Backfill Desk refuses a concurrent Start while another Backfill run owns the single-run slot.
pub(crate) use self::errors::ERROR_CODE_BACKFILL_RUN_ACTIVE;
/// Backfill Desk composite configuration is missing or references an unexpected document.
pub(crate) use self::errors::ERROR_CODE_CONFIG_COMPOSITE_INVALID;
/// Frontend logging requested an unsupported level.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/store_runtime.rs
// version: 1
// version: 2
//! Composite-selected Store readiness and shutdown lifecycle owned by Backfill Desk.
@@ -21,6 +21,36 @@ impl StoreStartup {
options.composition_ready = options.transport_ready && options.network_coherent && options.store_ready;
}
/// Temporarily lends the ready Store facade to one admitted Backfill execution.
pub(crate) fn take_for_run(&self) -> ksp_core_lib::Result<ksp_store_lib::Store> {
let runtime = self.runtime.as_ref();
let runtime = match runtime {
std::option::Option::Some(value) if value.health_ready() => value,
_ => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_BACKFILL_COMPOSITION_NOT_READY,
"Backfill Desk cannot start a Backfill run without a ready Store",
));
},
};
return runtime.take_for_run();
}
/// Restores the Store facade after one Backfill execution reaches its terminal result.
pub(crate) fn restore_after_run(&self, store: ksp_store_lib::Store) -> ksp_core_lib::Result<()> {
let runtime = self.runtime.as_ref();
let runtime = match runtime {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Backfill Desk cannot restore Store after runtime ownership disappeared",
));
},
};
return runtime.restore_after_run(store);
}
/// Closes the retained Store runtime if startup reached the physical Store-open phase.
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
let runtime = self.runtime.as_ref();
@@ -46,6 +76,44 @@ impl StoreRuntime {
return self.health_ready;
}
/// Temporarily removes the Store from the application slot for one single-active Backfill execution.
pub(crate) fn take_for_run(&self) -> ksp_core_lib::Result<ksp_store_lib::Store> {
let store = take_store(&self.store);
let store = match store {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return match store {
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_BACKFILL_RUN_ACTIVE,
"Backfill Desk Store is already owned by an active Backfill run",
)),
};
}
/// Restores the Store after one terminal Backfill execution so a later campaign can be admitted.
pub(crate) fn restore_after_run(&self, store: ksp_store_lib::Store) -> ksp_core_lib::Result<()> {
let locked = self.store.lock();
let mut locked = match locked {
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 Store runtime state lock is poisoned",
));
},
};
if locked.is_some() {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_APP_STATE_INVALID,
"Backfill Desk cannot restore Store into an occupied runtime slot",
));
}
*locked = std::option::Option::Some(store);
return std::result::Result::Ok(());
}
/// Explicitly closes the Store exactly once through its backend-neutral facade.
pub(crate) async fn close(&self) -> ksp_core_lib::Result<()> {
let store = take_store(&self.store);

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/tauri.rs
// version: 5
// version: 6
//! Tauri runtime assembly for the KSP Backfill desktop application.
@@ -77,6 +77,7 @@ fn configure_plugins(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<taur
fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tauri::Wry> {
return builder.invoke_handler(tauri::generate_handler![
backfill_options,
backfill_start,
backfill_validate_request,
emit_frontend_log,
get_runtime_status,
@@ -164,6 +165,36 @@ fn backfill_options(state: tauri::State<'_, crate::AppState>) -> std::result::Re
};
}
#[tauri::command]
fn backfill_start(
app: tauri::AppHandle,
request: crate::BackfillStartRequestDto,
state: tauri::State<'_, crate::AppState>,
) -> std::result::Result<crate::BackfillStartResponseDto, crate::CommandErrorDto> {
let launch = state.prepare_backfill_start(request);
let launch = match launch {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(project_command_error("backfill_start", crate::TRACING_DOMAIN_RUN, &error));
},
};
let response = launch.response();
let _run_task = tauri::async_runtime::spawn(async move {
let state = app.state::<crate::AppState>();
let executed = state.execute_backfill_run(launch).await;
if let std::result::Result::Err(error) = executed {
ksp_logging_lib::warn!(
target: crate::TRACING_TARGET,
domain = crate::TRACING_DOMAIN_RUN,
error_domain = error.code().domain(),
error_code = error.code().code(),
"Backfill Desk async run terminated with an error"
);
}
});
return std::result::Result::Ok(response);
}
#[tauri::command]
fn backfill_validate_request(
request: crate::BackfillStartRequestDto,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/transport_runtime.rs
// version: 4
// version: 5
//! Composite-selected HTTP Transport readiness and route inventory owned by Backfill Desk.
@@ -10,6 +10,12 @@ pub(crate) struct TransportRuntime {
}
impl TransportRuntime {
/// Returns a shareable clone of the Transport-owned HTTP pool for one admitted Backfill execution.
#[must_use]
pub(crate) fn pool(&self) -> ksp_onchain_transport_lib::HttpTransportPool {
return self.pool.clone();
}
/// Returns the composite-selected Transport profile identifier.
#[must_use]
pub(crate) fn profile_id(&self) -> &str {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
// version: 7
// version: 8
//! Structural desktop contract checks for the Backfill Desk scaffold.
@@ -245,13 +245,13 @@ fn pre_006_mainnet_default_and_http_route_selector_are_explicit_without_start_ru
assert!(frontend.contains("backfill_options"));
let html = read_text(root.join("frontend/main.html").as_path());
assert!(html.contains("backfillHttpRoute"));
for forbidden in ["backfill_start", "BackfillJobRuntime", "grpc", "websocket"] {
for forbidden in ["grpc", "websocket"] {
assert!(!frontend.contains(forbidden), "pre.006 frontend opens deferred runtime marker {forbidden}");
}
}
#[test]
fn pre_007_campaign_dto_and_backend_mapping_exist_without_start_runtime() {
fn pre_007_campaign_dto_and_backend_mapping_remain_the_authoritative_start_input() {
let root = app_root();
let manifest = read_text(root.join("Cargo.toml").as_path());
assert!(manifest.contains("ksp-job-api = { path = \"../ksp-job-api\" }"));
@@ -291,8 +291,38 @@ fn pre_007_campaign_dto_and_backend_mapping_exist_without_start_runtime() {
assert!(frontend.contains("backfill_validate_request"));
assert!(frontend.contains("Backfill Desk campaign validation requested"));
assert!(frontend.contains("Aucun job n'a été démarré"));
for forbidden in ["backfill_start", "BackfillJobRuntime", "BackfillJobHandle"] {
assert!(!frontend.contains(forbidden), "pre.007 frontend opens deferred Start/runtime marker {forbidden}");
assert!(!tauri.contains(forbidden), "pre.007 Tauri surface opens deferred Start/runtime marker {forbidden}");
}
assert!(frontend.contains("backfill_validate_request"));
assert!(tauri.contains("backfill_validate_request"));
}
#[test]
fn pre_008_start_installs_handle_before_non_blocking_spawn_and_keeps_single_run_backend_owned() {
let root = app_root();
let state = read_text(root.join("src/app_state.rs").as_path());
let run = read_text(root.join("src/backfill_run.rs").as_path());
let tauri = read_text(root.join("src/tauri.rs").as_path());
let html = read_text(root.join("frontend/main.html").as_path());
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
assert!(state.contains("BackfillJobRuntime::new(mapped)"));
let handle_index = state.find("let handle = runtime.handle();");
let install_index = state.find("self.backfill_runs.install(job_id.clone(), handle)");
assert!(handle_index.is_some());
assert!(install_index.is_some());
if let (std::option::Option::Some(handle_index), std::option::Option::Some(install_index)) = (handle_index, install_index) {
assert!(handle_index < install_index);
}
assert!(run.contains("ERROR_CODE_BACKFILL_RUN_ACTIVE"));
assert!(run.contains("std::sync::Mutex<std::option::Option<ActiveBackfillRun>>"));
assert!(tauri.contains("backfill_start"));
let prepare_index = tauri.find("state.prepare_backfill_start(request)");
let spawn_index = tauri.find("tauri::async_runtime::spawn(async move");
assert!(prepare_index.is_some());
assert!(spawn_index.is_some());
if let (std::option::Option::Some(prepare_index), std::option::Option::Some(spawn_index)) = (prepare_index, spawn_index) {
assert!(prepare_index < spawn_index);
}
assert!(html.contains("startBackfillRequest"));
assert!(html.contains("Démarrer le backfill"));
assert!(frontend.contains("Backfill Desk campaign Start requested"));
assert!(frontend.contains("backfill_start"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_security.rs
// version: 7
// version: 8
//! Security and dependency-boundary checks for the Backfill Desk scaffold.
@@ -91,7 +91,7 @@ fn pre_002_tauri_commands_remain_centralized() {
assert_eq!(count, 0, "{} declares a Tauri command outside tauri.rs", path.display());
}
}
assert_eq!(command_count, 5);
assert_eq!(command_count, 6);
}
#[test]
@@ -204,3 +204,37 @@ fn pre_007_request_mapping_keeps_network_endpoint_and_payload_secrets_backend_ow
assert!(!state.contains(forbidden), "request validation logging includes forbidden payload marker {forbidden}");
}
}
#[test]
fn pre_008_start_surface_keeps_payload_and_physical_resources_backend_owned() {
let root = app_root();
let dto = read_text(root.join("src/dto_backfill.rs").as_path());
let response_start = dto.find("pub(crate) struct BackfillStartResponseDto");
let preview_start = dto.find("pub(crate) struct BackfillRequestPreviewDto");
assert!(response_start.is_some());
assert!(preview_start.is_some());
let response_source = match (response_start, preview_start) {
(std::option::Option::Some(start), std::option::Option::Some(end)) if start < end => &dto[start..end],
_ => "",
};
assert!(response_source.contains("pub(crate) job_id: String"));
assert!(response_source.contains("pub(crate) state: String"));
for forbidden in ["address", "signature", "provider", "endpoint", "url", "credential", "token", "network"] {
assert!(!response_source.contains(forbidden), "Start acknowledgement leaks forbidden marker {forbidden}");
}
let run = read_text(root.join("src/backfill_run.rs").as_path());
assert!(run.contains("BackfillJobHandle"));
assert!(run.contains("ERROR_CODE_BACKFILL_RUN_ACTIVE"));
assert!(!run.contains("reqwest"));
assert!(!run.contains("tokio_postgres"));
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
let start_marker = frontend.find("Backfill Desk campaign Start requested");
assert!(start_marker.is_some());
let start_source = match start_marker {
std::option::Option::Some(index) => &frontend[index..std::cmp::min(index + 420, frontend.len())],
std::option::Option::None => "",
};
for forbidden in ["address:", "anchorSignature:", "explicitSignatures:", "minContextSlot:"] {
assert!(!start_source.contains(forbidden), "Start tracing includes forbidden request payload marker {forbidden}");
}
}

View File

@@ -0,0 +1,24 @@
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
// version: 1
#[test]
fn generated_run_ids_are_backend_owned_bounded_and_unique_in_session() {
let state = crate::BackfillRunState::new();
let first = state.next_job_id();
let second = state.next_job_id();
assert!(first.is_ok());
assert!(second.is_ok());
let first = match first {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let second = match second {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_ne!(first, second);
assert!(first.as_str().starts_with("backfill-desk-"));
assert!(second.as_str().starts_with("backfill-desk-"));
assert!(first.as_str().len() <= ksp_job_api::MAX_JOB_ID_BYTES);
assert!(second.as_str().len() <= ksp_job_api::MAX_JOB_ID_BYTES);
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
// version: 1
// version: 2
#[test]
fn request_limits_are_derived_from_job_owned_public_bounds() {
@@ -29,3 +29,17 @@ fn request_options_expose_exact_current_commitments_and_http_scopes() {
vec!["latest_address".to_owned(), "before_address".to_owned(), "after_address".to_owned(), "explicit_signatures".to_owned()]
);
}
#[test]
fn start_acknowledgement_contains_only_backend_job_identity_and_initial_state() {
let response = crate::BackfillStartResponseDto { job_id: "backfill-desk-1".to_owned(), state: "created".to_owned() };
let serialized = serde_json::to_string(&response);
assert!(serialized.is_ok());
if let std::result::Result::Ok(serialized) = serialized {
assert!(serialized.contains("backfill-desk-1"));
assert!(serialized.contains("created"));
for forbidden in ["address", "signature", "provider", "endpoint", "credential", "token"] {
assert!(!serialized.contains(forbidden));
}
}
}