v0.3.7-pre.008
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 426
|
||||
# version: 427
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.7-pre.7.fix.1"
|
||||
version = "0.3.7-pre.8"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
131
crates/ksp-app-backfill-desk/src/backfill_run.rs
Normal file
131
crates/ksp-app-backfill-desk/src/backfill_run.rs
Normal 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;
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
24
crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
Normal file
24
crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
Normal 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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
130
deltas/0.3.7/pre.008.md
Normal file
130
deltas/0.3.7/pre.008.md
Normal file
@@ -0,0 +1,130 @@
|
||||
<!-- file: deltas/0.3.7/pre.008.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.7-pre.008` — runtime et Start réel Backfill
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
```text
|
||||
0.3.7-pre.007-fix.001
|
||||
workspace.package.version = 0.3.7-pre.7.fix.1
|
||||
```
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Ouvrir l'exécution réelle d'une campagne Backfill HTTP à partir du contrat strictement validé en `pre.007`, avec un seul run actif, handle installé avant spawn et réponse Start minimale, sans encore ouvrir le monitoring latest-value, Cancel ou Resume.
|
||||
|
||||
## 3. Admission single-run
|
||||
|
||||
Le `JobId` est généré exclusivement côté backend sous la forme bornée `backfill-desk-<sequence>`. Le frontend ne fournit aucune identité Job.
|
||||
|
||||
`BackfillRunState` retient un unique `BackfillJobHandle` et refuse tout second Start via l'erreur stable `backfill_run_active`. `BackfillJobRuntime::handle()` est obtenu puis installé dans ce slot avant que le command handler ne lance `tauri::async_runtime::spawn`.
|
||||
|
||||
Le Start réutilise `map_backfill_request()` sans divergence : réseau dérivé du Store, rôle HTTP revalidé et bornes/scope/commitment validés par `BackfillRequest::new` restent les seules entrées autoritaires.
|
||||
|
||||
## 4. Ressources d'exécution
|
||||
|
||||
`HttpTransportPool` est cloné depuis `TransportRuntime` et conserve toute la topologie provider/URL/retry dans `ksp-onchain-transport-lib`.
|
||||
|
||||
Le `Store` backend-neutre n'est pas clonable. Backfill Desk le retire temporairement de son slot Rust, le déplace dans le task du run et le restaure après le résultat terminal avant de libérer le slot single-run. Aucune dépendance directe `ksp-store-api`, PostgreSQL, `reqwest`, `tonic` ou Yellowstone n'est ajoutée.
|
||||
|
||||
La fermeture de l'application pendant un run actif reste une limite transitoire : le couloir `pre.010` ajoutera la demande d'annulation coopérative et traitera explicitement les races shutdown/Cancel/terminal.
|
||||
|
||||
## 5. IPC et frontend
|
||||
|
||||
La commande Tauri `backfill_start` retourne uniquement :
|
||||
|
||||
```text
|
||||
job_id
|
||||
state = created
|
||||
```
|
||||
|
||||
Le formulaire conserve **Valider la requête** et ajoute **Démarrer le backfill**. Start est activé seulement lorsque `composition_ready = true`. Après admission, le bouton Start est verrouillé côté frontend jusqu'à l'arrivée du monitoring détaillé en `pre.009`.
|
||||
|
||||
Les logs Start contiennent seulement JobId backend, rôle logique/scope/commitment ou états techniques sûrs. Adresse, signature, ancre et valeur `min_context_slot` ne sont jamais journalisées.
|
||||
|
||||
## 6. Autocomplete Program IDs — exigence reportée au polish
|
||||
|
||||
Le besoin UX enregistré pendant cette tranche est réservé à `pre.012` : l'input adresse restera libre mais proposera un `datalist` HTML alimenté depuis le registre canonique `ksp-core-lib::entries()` et `ProgramIdEntry`. Aucune liste Program ID ne sera copiée/hardcodée dans le frontend. Le comportement recherché reprend uniquement le principe fonctionnel de l'ancien dataset `ks-program-ids` de kbot3, sans reprise de code.
|
||||
|
||||
## 7. Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.7-pre.8
|
||||
label = 0.3.7-pre.008
|
||||
```
|
||||
|
||||
## 8. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-app-backfill-desk/src/backfill_run.rs
|
||||
crates/ksp-app-backfill-desk/unit_tests/backfill_run.rs
|
||||
deltas/0.3.7/pre.008.md
|
||||
```
|
||||
|
||||
## 9. Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-app-backfill-desk/frontend/main.html
|
||||
crates/ksp-app-backfill-desk/frontend/ts/main.ts
|
||||
crates/ksp-app-backfill-desk/src/app_state.rs
|
||||
crates/ksp-app-backfill-desk/src/constants.rs
|
||||
crates/ksp-app-backfill-desk/src/dto_backfill.rs
|
||||
crates/ksp-app-backfill-desk/src/errors.rs
|
||||
crates/ksp-app-backfill-desk/src/lib.rs
|
||||
crates/ksp-app-backfill-desk/src/store_runtime.rs
|
||||
crates/ksp-app-backfill-desk/src/tauri.rs
|
||||
crates/ksp-app-backfill-desk/src/transport_runtime.rs
|
||||
crates/ksp-app-backfill-desk/tests/desktop_contract.rs
|
||||
crates/ksp-app-backfill-desk/tests/desktop_security.rs
|
||||
crates/ksp-app-backfill-desk/unit_tests/dto_backfill.rs
|
||||
docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md
|
||||
docs/validation/024-V0_3_7_BACKFILL_DESK.md
|
||||
```
|
||||
|
||||
## 10. Fichiers supprimés
|
||||
|
||||
Aucun.
|
||||
|
||||
## 11. Gate précédent acquis
|
||||
|
||||
Le replay opérateur de `0.3.7-pre.007-fix.001` est propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, toutes les suites `ksp-job-backfill-lib`, toutes les suites Backfill Desk et arbres Cargo passent. Le smoke `cargo tauri dev` confirme aussi le profil Mainnet, les trois routes HTTP logiques et le tracing frontend ; le Store non joignable laisse correctement le shell disponible en mode diagnostic.
|
||||
|
||||
## 12. Validations exécutées dans l'environnement d'assemblage
|
||||
|
||||
```text
|
||||
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
|
||||
```
|
||||
|
||||
Contrôles ciblés :
|
||||
|
||||
- syntaxe TypeScript par transpilation locale ;
|
||||
- handle construit et installé avant le spawn Start ;
|
||||
- admission single-run et JobId backend-only ;
|
||||
- absence de payload métier dans la réponse Start et les logs Start ;
|
||||
- dépendances/capabilities inchangées hors surfaces déjà autorisées ;
|
||||
- prêt/restauration du Store backend-neutre ;
|
||||
- absence de gRPC/WS dans le contrat Backfill V1 ;
|
||||
- contrôle différentiel strict et reconstruction par application du delta.
|
||||
|
||||
## 13. Validations non exécutées dans l'environnement d'assemblage
|
||||
|
||||
`cargo` et `rustfmt` ne sont pas disponibles dans l'environnement d'assemblage. Gate opérateur obligatoire avant `pre.009` :
|
||||
|
||||
```text
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.7
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-job-backfill-lib
|
||||
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
|
||||
```
|
||||
|
||||
## 14. Suite
|
||||
|
||||
`pre.009` ajoutera le bridge latest-value : source de snapshots, DTO de statut complet, resynchronisation par commande et événements coalescés. Cancel et races terminales restent en `pre.010`.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# Plan v0.3.7 — Backfill Desk
|
||||
|
||||
@@ -265,7 +265,7 @@ Le mapping Rust dérive le réseau du Store configuré, revalide `http_role` con
|
||||
|
||||
### pre.008 — runtime + Start
|
||||
|
||||
Single-active-run state, construction `BackfillJobRuntime`, spawn non bloquant, handle installé avant exécution, admission atomique et snapshot initial/terminal.
|
||||
Ouvrir le Start réel en réutilisant strictement le mapping `pre.007` : JobId généré côté backend, état single-active-run, construction `BackfillJobRuntime`, handle installé atomiquement avant tout spawn, puis exécution non bloquante sur le pool HTTP courant et le Store déjà ouvert. Le Store est temporairement prêté au run puis remis dans le slot applicatif après terminal ; aucune dépendance backend Store supplémentaire n'est ouverte. La réponse Start reste minimale (`job_id`, état initial) et aucun snapshot détaillé n'est encore projeté. La fermeture pendant un run reste une limite transitoire jusqu'au couloir Cancel/races de `pre.010`.
|
||||
|
||||
### pre.009 — monitoring latest-value
|
||||
|
||||
@@ -281,7 +281,7 @@ Projection sûre, conservation Rust-only du checkpoint, reprise in-session et in
|
||||
|
||||
### pre.012 — frontend fonctionnel et polish
|
||||
|
||||
Formulaire complet, états responsive, instrumentation sûre, summary terminal, erreurs, boutons contextuels, aucun secret/browser storage.
|
||||
Formulaire complet, états responsive, instrumentation sûre, summary terminal, erreurs, boutons contextuels, aucun secret/browser storage. L'adresse reste une saisie libre, mais reçoit un autocomplete HTML via `datalist` alimenté depuis le registre canonique `ksp-core-lib::entries()`/`ProgramIdEntry` : aucune copie locale de Program IDs, et une valeur arbitraire valide reste toujours acceptée. Le comportement fonctionnel recherché est celui de l'ancien dataset `ks-program-ids` de kbot3, sans reprise de son code.
|
||||
|
||||
### pre.013 — hardening et complétude
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/024-V0_3_7_BACKFILL_DESK.md -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 11 -->
|
||||
|
||||
# Validation v0.3.7 — Backfill Desk
|
||||
|
||||
@@ -340,4 +340,27 @@ Le correctif renomme uniquement cette variable locale en `request_job_id`. Aucun
|
||||
- [X] aucun fichier fonctionnel de `pre.007` modifié ;
|
||||
- [X] version Cargo synchronisée en `0.3.7-pre.7.fix.1` conformément à `VER-ID-007` et `VER-ID-010` ;
|
||||
- [X] audits statiques Rust/Markdown rejoués dans l'environnement d'assemblage ;
|
||||
- [ ] replay opérateur `cargo fmt/check/clippy/test` du fix à exécuter avant `pre.008`.
|
||||
- [X] replay opérateur `cargo fmt/check/clippy/test` du fix propre : audits, check, Clippy, `ksp-job-backfill-lib` et toutes les suites Backfill Desk passent.
|
||||
|
||||
## 17. `pre.008` — runtime et Start réel
|
||||
|
||||
Le replay opérateur de `pre.007-fix.001` est acquis. `pre.008` ouvre l'exécution réelle sans modifier le contrat de requête : `backfill_start` réutilise `map_backfill_request`, génère le `JobId` côté backend, construit `BackfillJobRuntime`, capture son `BackfillJobHandle` puis installe ce handle dans un slot single-active-run avant le spawn Tauri. Un second Start est rejeté par `backfill_run_active`.
|
||||
|
||||
Le pool HTTP est clonable et reste possédé par Transport. Le Store backend-neutre, non clonable, est temporairement retiré de son slot applicatif pour être déplacé dans le task du run ; après terminal, il est restauré avant libération du slot actif. La réponse IPC Start contient uniquement `job_id` et l'état initial `created`. Le monitoring détaillé, les événements latest-value et la projection terminale restent réservés à `pre.009`. Cancel et la fermeture coopérative d'un run actif restent réservés à `pre.010`.
|
||||
|
||||
La note UX ajoutée pendant cette tranche est reportée à `pre.012` : l'input adresse restera libre mais utilisera un `datalist` HTML construit depuis le registre canonique `ksp-core-lib::entries()`/`ProgramIdEntry`, sur le principe fonctionnel de l'ancien `ks-program-ids` de kbot3 sans en reprendre le code.
|
||||
|
||||
### Gate statique local `pre.008`
|
||||
|
||||
- [X] `BackfillJobRuntime::new` est appelé uniquement après le mapping autoritaire `pre.007` ;
|
||||
- [X] `BackfillJobHandle` est obtenu et installé dans le slot actif avant `tauri::async_runtime::spawn` ;
|
||||
- [X] admission single-run atomique avec erreur stable `backfill_run_active` ;
|
||||
- [X] JobId généré côté backend, jamais reçu du frontend ;
|
||||
- [X] pool HTTP cloné depuis `TransportRuntime`, sans URL/provider physique supplémentaire ;
|
||||
- [X] Store temporairement prêté au run puis restauré après terminal ;
|
||||
- [X] `BackfillStartResponseDto` limité à `job_id` + état initial ;
|
||||
- [X] frontend conserve le bouton de validation et ajoute un bouton Start distinct, activé seulement si `composition_ready` ;
|
||||
- [X] aucune adresse, signature, ancre ou `min_context_slot` ajoutée aux logs Start ;
|
||||
- [X] capabilities Tauri inchangées : `core:default + tracing:default` ;
|
||||
- [X] audits statiques Rust/Markdown propres dans l'environnement d'assemblage ;
|
||||
- [ ] `cargo fmt/check/clippy/test` de `pre.008` à rejouer par l'opérateur ; `cargo`/`rustfmt` restent absents du sandbox.
|
||||
|
||||
Reference in New Issue
Block a user