v0.3.7-pre.009
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 430
|
||||
# version: 431
|
||||
|
||||
[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.8.fix.3"
|
||||
version = "0.3.7-pre.9"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"recycle_timeout_ms": 5000
|
||||
},
|
||||
"tls": {
|
||||
"mode": "verify_full"
|
||||
"mode": "disabled"
|
||||
},
|
||||
"bootstrap": {
|
||||
"schema_autocreate": true,
|
||||
@@ -41,7 +41,7 @@
|
||||
"recycle_timeout_ms": 5000
|
||||
},
|
||||
"tls": {
|
||||
"mode": "verify_full"
|
||||
"mode": "disabled"
|
||||
},
|
||||
"bootstrap": {
|
||||
"schema_autocreate": true,
|
||||
@@ -66,7 +66,7 @@
|
||||
"recycle_timeout_ms": 5000
|
||||
},
|
||||
"tls": {
|
||||
"mode": "verify_full"
|
||||
"mode": "disabled"
|
||||
},
|
||||
"bootstrap": {
|
||||
"schema_autocreate": true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-backfill-desk/frontend/main.html -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -127,10 +127,36 @@
|
||||
<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>
|
||||
<span class="text-body-secondary small">Un seul run peut être actif ; le monitoring latest-value est coalescé côté backend.</span>
|
||||
</div>
|
||||
</form>
|
||||
<div id="backfillRequestValidation" class="alert alert-secondary mt-4 mb-0" role="status" aria-live="polite" hidden></div>
|
||||
<div id="backfillRunStatusCard" class="card shadow-sm mt-4" hidden>
|
||||
<div class="card-header d-flex align-items-center justify-content-between gap-3 flex-wrap">
|
||||
<span class="fw-semibold">Monitoring latest-value</span>
|
||||
<button id="refreshBackfillStatus" class="btn btn-outline-primary btn-sm" type="button">
|
||||
<i class="fa-solid fa-rotate me-2" aria-hidden="true"></i>Resynchroniser
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row mb-0 app-runtime-list">
|
||||
<dt class="col-sm-5">Job</dt><dd id="runStatusJobId" class="col-sm-7 font-monospace">—</dd>
|
||||
<dt class="col-sm-5">Lifecycle</dt><dd id="runStatusLifecycle" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Phase</dt><dd id="runStatusPhase" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Séquence</dt><dd id="runStatusSequence" class="col-sm-7 font-monospace">—</dd>
|
||||
<dt class="col-sm-5">Scope</dt><dd id="runStatusScope" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Boundary</dt><dd id="runStatusBoundary" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Candidates sel./adm./fin.</dt><dd id="runStatusCandidates" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Frontier / holes</dt><dd id="runStatusFrontier" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">RAW ins./exist./purged</dt><dd id="runStatusEntities" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Observations ins./exist.</dt><dd id="runStatusObservations" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Anomalies</dt><dd id="runStatusAnomalies" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Max in-flight</dt><dd id="runStatusConcurrency" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Checkpoint</dt><dd id="runStatusCheckpoint" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Failure</dt><dd id="runStatusFailure" class="col-sm-7">—</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div id="backfillRequestPreview" class="card bg-body-tertiary border-0 mt-3" hidden>
|
||||
<div class="card-body">
|
||||
<h2 class="h6 mb-3">Projection backend sûre</h2>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import "simplebar";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { BackfillRunStatusDto } from "./bindings/ksp_app_backfill_desk/backfill_status/BackfillRunStatusDto.ts";
|
||||
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";
|
||||
@@ -320,8 +322,9 @@ async function startBackfillRequest(): Promise<void> {
|
||||
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.`;
|
||||
status.textContent = `Job ${started.jobId} démarré (${started.state}).`;
|
||||
}
|
||||
await syncBackfillStatus("start");
|
||||
frontendInfo("main", "Backfill Desk campaign Start accepted", { jobId: started.jobId, state: started.state });
|
||||
} catch (caughtError) {
|
||||
renderRequestValidationError(caughtError);
|
||||
@@ -447,6 +450,76 @@ function bindBackfillRouteSelection(): void {
|
||||
frontendTrace("main", "Backfill Desk HTTP route selection handler installed");
|
||||
}
|
||||
|
||||
|
||||
function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "event" | "startup" | "start" | "user"): void {
|
||||
const card = document.querySelector<HTMLElement>("#backfillRunStatusCard");
|
||||
if (!status) {
|
||||
runStartAccepted = false;
|
||||
refreshStartButton();
|
||||
if (card) {
|
||||
card.hidden = true;
|
||||
}
|
||||
frontendTrace("main", "Backfill Desk latest-value status cleared", { source });
|
||||
return;
|
||||
}
|
||||
runStartAccepted = status.active;
|
||||
refreshStartButton();
|
||||
if (card) {
|
||||
card.hidden = false;
|
||||
}
|
||||
const values: Record<string, string> = {
|
||||
runStatusJobId: status.jobId,
|
||||
runStatusLifecycle: status.completion === null ? status.state : `${status.state} / ${status.completion}`,
|
||||
runStatusPhase: status.phase,
|
||||
runStatusSequence: status.sequence,
|
||||
runStatusScope: status.scopeKind,
|
||||
runStatusBoundary: status.discoveryBoundary ?? "—",
|
||||
runStatusCandidates: `${status.candidatesSelected} / ${status.candidatesAdmitted} / ${status.candidatesFinished}`,
|
||||
runStatusFrontier: `${status.contiguousCompleted} (holes ${status.holes})`,
|
||||
runStatusEntities: `${status.entitiesInserted} / ${status.entitiesExisting} / ${status.entitiesPurged}`,
|
||||
runStatusObservations: `${status.observationsInserted} / ${status.observationsExisting}`,
|
||||
runStatusAnomalies: `missing ${status.missing} / conflits ${status.conflicts} / annulés ${status.cancelledCandidates}`,
|
||||
runStatusConcurrency: status.maximumInFlight.toString(),
|
||||
runStatusCheckpoint: status.checkpointPresent ? "présent" : "absent",
|
||||
runStatusFailure: status.failureDomain === null || status.failureCode === null ? "—" : `${status.failureDomain}/${status.failureCode}`,
|
||||
};
|
||||
for (const [id, value] of Object.entries(values)) {
|
||||
const element = document.querySelector<HTMLElement>(`#${id}`);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
}
|
||||
frontendTrace("main", "Backfill Desk latest-value status rendered", {
|
||||
active: status.active,
|
||||
phase: status.phase,
|
||||
sequence: status.sequence,
|
||||
source,
|
||||
state: status.state,
|
||||
terminal: status.terminal,
|
||||
});
|
||||
}
|
||||
|
||||
async function syncBackfillStatus(source: "startup" | "start" | "user"): Promise<void> {
|
||||
frontendDebug("main", "Backfill Desk latest-value status resynchronization started", { source });
|
||||
const status = await invokeKsp<BackfillRunStatusDto | null>("main", "backfill_status");
|
||||
renderBackfillRunStatus(status, source);
|
||||
frontendDebug("main", "Backfill Desk latest-value status resynchronization completed", { source, statusPresent: status !== null });
|
||||
}
|
||||
|
||||
async function bindBackfillStatusMonitoring(): Promise<void> {
|
||||
await listen<BackfillRunStatusDto>("ksp-backfill-status", event => {
|
||||
renderBackfillRunStatus(event.payload, "event");
|
||||
});
|
||||
const button = document.querySelector<HTMLButtonElement>("#refreshBackfillStatus");
|
||||
if (button) {
|
||||
button.addEventListener("click", () => {
|
||||
frontendDebug("main", "Backfill Desk latest-value status refresh button clicked");
|
||||
void syncBackfillStatus("user").catch(() => frontendWarn("main", "Backfill Desk latest-value status refresh failed"));
|
||||
});
|
||||
}
|
||||
frontendTrace("main", "Backfill Desk latest-value monitoring listener installed");
|
||||
}
|
||||
|
||||
function renderRuntimeStatus(status: ShellStatusDto): void {
|
||||
const values: Record<string, string> = {
|
||||
runtimeVersion: status.applicationVersion,
|
||||
@@ -503,9 +576,10 @@ async function initializeMain(): Promise<void> {
|
||||
bindRuntimeStatusRefresh();
|
||||
bindBackfillRouteSelection();
|
||||
bindCampaignForm();
|
||||
await bindBackfillStatusMonitoring();
|
||||
activateView("backfill", "startup");
|
||||
try {
|
||||
await Promise.all([loadRuntimeStatus("startup"), loadBackfillOptions("startup")]);
|
||||
await Promise.all([loadRuntimeStatus("startup"), loadBackfillOptions("startup"), syncBackfillStatus("startup")]);
|
||||
} catch {
|
||||
frontendWarn("main", "Backfill Desk startup runtime status load failed");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/app_state.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Shared backend state owned by the Backfill Desk Tauri application.
|
||||
|
||||
@@ -225,6 +225,7 @@ impl crate::AppState {
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let handle = runtime.handle();
|
||||
let snapshots = handle.snapshots();
|
||||
let installed = self.backfill_runs.install(job_id.clone(), handle);
|
||||
if let std::result::Result::Err(error) = installed {
|
||||
return std::result::Result::Err(error);
|
||||
@@ -260,7 +261,20 @@ impl crate::AppState {
|
||||
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 });
|
||||
return std::result::Result::Ok(crate::BackfillRunLaunch { job_id, runtime, snapshots, store, transport });
|
||||
}
|
||||
|
||||
/// Returns the current active or retained terminal Backfill status for frontend resynchronization.
|
||||
pub(crate) fn backfill_status(&self) -> ksp_core_lib::Result<std::option::Option<crate::BackfillRunStatusDto>> {
|
||||
let notification = self.backfill_runs.current_notification();
|
||||
let notification = match notification {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return match notification {
|
||||
std::option::Option::Some(value) => crate::project_backfill_status(&value).map(std::option::Option::Some),
|
||||
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
|
||||
};
|
||||
}
|
||||
|
||||
/// Executes one already-admitted Backfill launch and restores application resources after its terminal result.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/backfill_run.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Single-active-run admission and launch ownership for Backfill Desk.
|
||||
|
||||
@@ -9,6 +9,8 @@ pub(crate) struct BackfillRunLaunch {
|
||||
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,
|
||||
/// Independent latest-value source retained by the monitoring bridge.
|
||||
pub(crate) snapshots: ksp_job_backfill_lib::BackfillSnapshotSource,
|
||||
/// 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.
|
||||
@@ -26,6 +28,7 @@ impl crate::BackfillRunLaunch {
|
||||
/// 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>>,
|
||||
last_terminal: std::sync::Mutex<std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>>>,
|
||||
next_sequence: std::sync::atomic::AtomicU64,
|
||||
}
|
||||
|
||||
@@ -35,6 +38,7 @@ impl crate::BackfillRunState {
|
||||
pub(crate) const fn new() -> Self {
|
||||
return Self {
|
||||
active: std::sync::Mutex::new(std::option::Option::None),
|
||||
last_terminal: std::sync::Mutex::new(std::option::Option::None),
|
||||
next_sequence: std::sync::atomic::AtomicU64::new(1),
|
||||
};
|
||||
}
|
||||
@@ -107,10 +111,58 @@ impl crate::BackfillRunState {
|
||||
},
|
||||
};
|
||||
let cancellation_requested = current.handle.is_cancellation_requested();
|
||||
let source = current.handle.snapshots();
|
||||
let notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
|
||||
if notification.state().is_terminal() {
|
||||
let terminal = self.last_terminal.lock();
|
||||
let mut terminal = match terminal {
|
||||
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 terminal monitoring state lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
*terminal = std::option::Option::Some(notification);
|
||||
}
|
||||
*active = std::option::Option::None;
|
||||
return std::result::Result::Ok(cancellation_requested);
|
||||
}
|
||||
|
||||
/// Returns the complete current active snapshot or the retained latest terminal snapshot for resynchronization.
|
||||
pub(crate) fn current_notification(
|
||||
&self,
|
||||
) -> ksp_core_lib::Result<std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>>> {
|
||||
let active = self.active.lock();
|
||||
let 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 monitoring state lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
if let std::option::Option::Some(current) = active.as_ref() {
|
||||
let source = current.handle.snapshots();
|
||||
let notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
|
||||
return std::result::Result::Ok(std::option::Option::Some(notification));
|
||||
}
|
||||
drop(active);
|
||||
let terminal = self.last_terminal.lock();
|
||||
let terminal = match terminal {
|
||||
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 terminal monitoring state lock is poisoned",
|
||||
));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(terminal.clone());
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
194
crates/ksp-app-backfill-desk/src/backfill_status.rs
Normal file
194
crates/ksp-app-backfill-desk/src/backfill_status.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/backfill_status.rs
|
||||
// version: 1
|
||||
|
||||
//! Safe latest-value monitoring projection for one Backfill Desk run.
|
||||
|
||||
use ts_rs::TS; // rust-rules: trait-import
|
||||
|
||||
/// Stable Tauri event carrying the latest complete safe Backfill run status.
|
||||
pub(crate) const BACKFILL_STATUS_EVENT_NAME: &str = "ksp-backfill-status";
|
||||
|
||||
/// Frontend-safe latest-value projection of one concrete Backfill runtime snapshot.
|
||||
#[derive(Clone, Debug, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/backfill_status/BackfillRunStatusDto.ts")]
|
||||
pub(crate) struct BackfillRunStatusDto {
|
||||
/// Whether the represented Job is currently non-terminal.
|
||||
pub(crate) active: bool,
|
||||
/// Number of candidates admitted into hydration.
|
||||
pub(crate) candidates_admitted: u32,
|
||||
/// Number of admitted candidates that reached a known coordinator result.
|
||||
pub(crate) candidates_finished: u32,
|
||||
/// Number of candidates selected by discovery.
|
||||
pub(crate) candidates_selected: u32,
|
||||
/// Number of candidates cancelled before Store submission.
|
||||
pub(crate) cancelled_candidates: u32,
|
||||
/// Whether a safe caller-owned checkpoint currently exists.
|
||||
pub(crate) checkpoint_present: bool,
|
||||
/// Normal terminal completion classification, when applicable.
|
||||
pub(crate) completion: std::option::Option<String>,
|
||||
/// Safe contiguous completion prefix retained by the current checkpoint frontier.
|
||||
pub(crate) contiguous_completed: u32,
|
||||
/// Number of Store content conflicts observed by the run.
|
||||
pub(crate) conflicts: u32,
|
||||
/// Safe discovery boundary code after discovery completes.
|
||||
pub(crate) discovery_boundary: std::option::Option<String>,
|
||||
/// Number of identical canonical RAW transactions already present.
|
||||
pub(crate) entities_existing: u32,
|
||||
/// Number of newly inserted canonical RAW transactions.
|
||||
pub(crate) entities_inserted: u32,
|
||||
/// Number of durable purge tombstones respected by normal Backfill persistence.
|
||||
pub(crate) entities_purged: u32,
|
||||
/// Stable fatal error code retained by a failed Job, when applicable.
|
||||
pub(crate) failure_code: std::option::Option<String>,
|
||||
/// Stable fatal error domain retained by a failed Job, when applicable.
|
||||
pub(crate) failure_domain: std::option::Option<String>,
|
||||
/// Number of known candidate outcomes blocking the contiguous frontier.
|
||||
pub(crate) holes: u32,
|
||||
/// Backend-generated logical Job identity.
|
||||
pub(crate) job_id: String,
|
||||
/// Stable concrete Job family code.
|
||||
pub(crate) job_kind: String,
|
||||
/// Greatest observed candidate concurrency.
|
||||
pub(crate) maximum_in_flight: u32,
|
||||
/// Number of `getTransaction = null` candidates.
|
||||
pub(crate) missing: u32,
|
||||
/// Number of acquisition observations already durable.
|
||||
pub(crate) observations_existing: u32,
|
||||
/// Number of newly inserted acquisition observations.
|
||||
pub(crate) observations_inserted: u32,
|
||||
/// Stable concrete Backfill runtime phase.
|
||||
pub(crate) phase: String,
|
||||
/// Monotone latest-value notification sequence encoded as decimal text.
|
||||
pub(crate) sequence: String,
|
||||
/// Stable Backfill scope category without address/signature payloads.
|
||||
pub(crate) scope_kind: String,
|
||||
/// Stable generic Job lifecycle state.
|
||||
pub(crate) state: String,
|
||||
/// Whether this latest value is terminal.
|
||||
pub(crate) terminal: bool,
|
||||
}
|
||||
|
||||
/// Projects one complete Backfill latest-value notification to the safe desktop monitoring contract.
|
||||
pub(crate) fn project_backfill_status(
|
||||
notification: &ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>,
|
||||
) -> ksp_core_lib::Result<BackfillRunStatusDto> {
|
||||
let snapshot = notification.snapshot();
|
||||
let state = notification.state();
|
||||
let failure = snapshot.failure_code();
|
||||
let candidates_admitted = usize_to_u32(snapshot.candidates_admitted(), "candidates_admitted");
|
||||
let candidates_admitted = match candidates_admitted {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let candidates_finished = usize_to_u32(snapshot.candidates_finished(), "candidates_finished");
|
||||
let candidates_finished = match candidates_finished {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let candidates_selected = usize_to_u32(snapshot.candidates_selected(), "candidates_selected");
|
||||
let candidates_selected = match candidates_selected {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let cancelled_candidates = usize_to_u32(snapshot.cancelled_candidates(), "cancelled_candidates");
|
||||
let cancelled_candidates = match cancelled_candidates {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let contiguous_completed = usize_to_u32(snapshot.contiguous_completed(), "contiguous_completed");
|
||||
let contiguous_completed = match contiguous_completed {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let conflicts = usize_to_u32(snapshot.conflicts(), "conflicts");
|
||||
let conflicts = match conflicts {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let entities_existing = usize_to_u32(snapshot.entities_existing(), "entities_existing");
|
||||
let entities_existing = match entities_existing {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let entities_inserted = usize_to_u32(snapshot.entities_inserted(), "entities_inserted");
|
||||
let entities_inserted = match entities_inserted {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let entities_purged = usize_to_u32(snapshot.entities_purged(), "entities_purged");
|
||||
let entities_purged = match entities_purged {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let holes = usize_to_u32(snapshot.holes(), "holes");
|
||||
let holes = match holes {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let maximum_in_flight = usize_to_u32(snapshot.maximum_in_flight(), "maximum_in_flight");
|
||||
let maximum_in_flight = match maximum_in_flight {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let missing = usize_to_u32(snapshot.missing(), "missing");
|
||||
let missing = match missing {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let observations_existing = usize_to_u32(snapshot.observations_existing(), "observations_existing");
|
||||
let observations_existing = match observations_existing {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let observations_inserted = usize_to_u32(snapshot.observations_inserted(), "observations_inserted");
|
||||
let observations_inserted = match observations_inserted {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(BackfillRunStatusDto {
|
||||
active: !state.is_terminal(),
|
||||
candidates_admitted,
|
||||
candidates_finished,
|
||||
candidates_selected,
|
||||
cancelled_candidates,
|
||||
checkpoint_present: snapshot.checkpoint().is_some(),
|
||||
completion: state.completion().map(|completion| return completion.code().to_owned()),
|
||||
contiguous_completed,
|
||||
conflicts,
|
||||
discovery_boundary: snapshot.discovery_boundary().map(|boundary| return boundary.code().to_owned()),
|
||||
entities_existing,
|
||||
entities_inserted,
|
||||
entities_purged,
|
||||
failure_code: failure.map(|code| return code.code().to_owned()),
|
||||
failure_domain: failure.map(|code| return code.domain().to_owned()),
|
||||
holes,
|
||||
job_id: notification.id().as_str().to_owned(),
|
||||
job_kind: notification.kind().as_str().to_owned(),
|
||||
maximum_in_flight,
|
||||
missing,
|
||||
observations_existing,
|
||||
observations_inserted,
|
||||
phase: snapshot.phase().code().to_owned(),
|
||||
sequence: notification.sequence().value().to_string(),
|
||||
scope_kind: snapshot.scope_kind().code().to_owned(),
|
||||
state: state.code().to_owned(),
|
||||
terminal: state.is_terminal(),
|
||||
});
|
||||
}
|
||||
|
||||
fn usize_to_u32(value: usize, field: &'static str) -> ksp_core_lib::Result<u32> {
|
||||
let converted = u32::try_from(value);
|
||||
return match converted {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_APP_STATE_INVALID, "Backfill Desk cannot project a bounded Job counter to the frontend")
|
||||
.with_context("field", field)
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/backfill_status.rs"]
|
||||
mod tests;
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/lib.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs.
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
mod app_state;
|
||||
mod backfill_request;
|
||||
mod backfill_run;
|
||||
mod backfill_status;
|
||||
mod bootstrap;
|
||||
mod constants;
|
||||
mod dto_backfill;
|
||||
@@ -37,6 +38,12 @@ pub(crate) use self::backfill_request::project_backfill_request;
|
||||
pub(crate) use self::backfill_run::BackfillRunLaunch;
|
||||
/// Single-active-run admission state retained by the application.
|
||||
pub(crate) use self::backfill_run::BackfillRunState;
|
||||
/// Stable Tauri event carrying coalesced latest-value Backfill status.
|
||||
pub(crate) use self::backfill_status::BACKFILL_STATUS_EVENT_NAME;
|
||||
/// Frontend-safe latest-value Backfill runtime projection.
|
||||
pub(crate) use self::backfill_status::BackfillRunStatusDto;
|
||||
/// Projects one complete Job notification to the safe desktop monitoring contract.
|
||||
pub(crate) use self::backfill_status::project_backfill_status;
|
||||
/// 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.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// file: crates/ksp-app-backfill-desk/src/tauri.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
//! Tauri runtime assembly for the KSP Backfill desktop application.
|
||||
|
||||
use tauri::Emitter; // rust-rules: trait-import
|
||||
use tauri::Manager; // rust-rules: trait-import
|
||||
|
||||
/// Runs the Backfill desktop application.
|
||||
@@ -78,6 +79,7 @@ fn configure_commands(builder: tauri::Builder<tauri::Wry>) -> tauri::Builder<tau
|
||||
return builder.invoke_handler(tauri::generate_handler![
|
||||
backfill_options,
|
||||
backfill_start,
|
||||
backfill_status,
|
||||
backfill_validate_request,
|
||||
emit_frontend_log,
|
||||
get_runtime_status,
|
||||
@@ -179,6 +181,11 @@ fn backfill_start(
|
||||
},
|
||||
};
|
||||
let response = launch.response();
|
||||
let monitor_app = app.clone();
|
||||
let monitor_source = launch.snapshots.clone();
|
||||
let _monitor_task = tauri::async_runtime::spawn(async move {
|
||||
monitor_backfill_status(monitor_app, monitor_source).await;
|
||||
});
|
||||
let _run_task = tauri::async_runtime::spawn(async move {
|
||||
let state = app.state::<crate::AppState>();
|
||||
let executed = state.execute_backfill_run(launch).await;
|
||||
@@ -195,6 +202,51 @@ fn backfill_start(
|
||||
return std::result::Result::Ok(response);
|
||||
}
|
||||
|
||||
async fn monitor_backfill_status(app: tauri::AppHandle, source: ksp_job_backfill_lib::BackfillSnapshotSource) {
|
||||
let mut notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source);
|
||||
loop {
|
||||
let projected = crate::project_backfill_status(¬ification);
|
||||
match projected {
|
||||
std::result::Result::Ok(status) => {
|
||||
let main = app.get_webview_window("main");
|
||||
if let std::option::Option::Some(window) = main {
|
||||
let emitted = window.emit(crate::BACKFILL_STATUS_EVENT_NAME, status);
|
||||
if emitted.is_err() {
|
||||
ksp_logging_lib::warn!(
|
||||
target: crate::TRACING_TARGET,
|
||||
domain = crate::TRACING_DOMAIN_RUN,
|
||||
"Backfill Desk could not emit latest-value monitoring status to the main window"
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
std::result::Result::Err(error) => {
|
||||
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 could not project latest-value monitoring status"
|
||||
);
|
||||
},
|
||||
}
|
||||
if notification.state().is_terminal() {
|
||||
return;
|
||||
}
|
||||
let observed = notification.sequence();
|
||||
notification = <ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::wait_for_change(&source, observed).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn backfill_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<std::option::Option<crate::BackfillRunStatusDto>, crate::CommandErrorDto> {
|
||||
let result = state.backfill_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("backfill_status", crate::TRACING_DOMAIN_RUN, &error)),
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn backfill_validate_request(
|
||||
request: crate::BackfillStartRequestDto,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Structural desktop contract checks for the Backfill Desk scaffold.
|
||||
|
||||
@@ -326,3 +326,23 @@ fn pre_008_start_installs_handle_before_non_blocking_spawn_and_keeps_single_run_
|
||||
assert!(frontend.contains("Backfill Desk campaign Start requested"));
|
||||
assert!(frontend.contains("backfill_start"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_latest_value_monitoring_uses_snapshot_source_event_bridge_and_resynchronization_command() {
|
||||
let root = workspace_root();
|
||||
let run = read(root.join("crates/ksp-app-backfill-desk/src/backfill_run.rs"));
|
||||
let status = read(root.join("crates/ksp-app-backfill-desk/src/backfill_status.rs"));
|
||||
let tauri = read(root.join("crates/ksp-app-backfill-desk/src/tauri.rs"));
|
||||
let frontend = read(root.join("crates/ksp-app-backfill-desk/frontend/ts/main.ts"));
|
||||
assert!(run.contains("last_terminal"));
|
||||
assert!(run.contains("current_notification"));
|
||||
assert!(status.contains("BackfillRunStatusDto"));
|
||||
assert!(status.contains("checkpoint_present"));
|
||||
assert!(status.contains("contiguous_completed"));
|
||||
assert!(tauri.contains("backfill_status"));
|
||||
assert!(tauri.contains("wait_for_change"));
|
||||
assert!(tauri.contains("ksp-backfill-status") || tauri.contains("BACKFILL_STATUS_EVENT_NAME"));
|
||||
assert!(frontend.contains("listen<BackfillRunStatusDto>"));
|
||||
assert!(frontend.contains("syncBackfillStatus"));
|
||||
assert!(!frontend.contains("parseBackfillLog"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-backfill-desk/tests/desktop_security.rs
|
||||
// version: 9
|
||||
// version: 10
|
||||
|
||||
//! Security and dependency-boundary checks for the Backfill Desk scaffold.
|
||||
|
||||
@@ -104,7 +104,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, 6);
|
||||
assert_eq!(command_count, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -239,3 +239,16 @@ fn pre_008_start_surface_keeps_payload_and_physical_resources_backend_owned() {
|
||||
assert!(!start_source.contains(forbidden), "Start tracing includes forbidden request payload marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_009_monitoring_projection_exposes_counters_and_codes_without_checkpoint_or_business_payloads() {
|
||||
let root = workspace_root();
|
||||
let status = read(root.join("crates/ksp-app-backfill-desk/src/backfill_status.rs"));
|
||||
let dto = struct_source(status.as_str(), "BackfillRunStatusDto");
|
||||
for required in ["checkpoint_present", "contiguous_completed", "failure_code", "failure_domain", "sequence", "scope_kind"] {
|
||||
assert!(dto.contains(required), "monitoring DTO missing required safe marker {required}");
|
||||
}
|
||||
for forbidden in ["address", "signature", "endpoint", "provider", "credential", "token", "checkpoint:", "payload", "raw_transaction"] {
|
||||
assert!(!dto.contains(forbidden), "monitoring DTO leaks forbidden marker {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
76
crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs
Normal file
76
crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs
|
||||
// version: 1
|
||||
|
||||
fn initial_notification() -> std::option::Option<ksp_job_api::JobNotification<ksp_job_backfill_lib::BackfillJobSnapshot>> {
|
||||
let signature = match ksp_job_backfill_lib::BackfillSignature::new("1".repeat(ksp_job_backfill_lib::MIN_BACKFILL_SIGNATURE_TEXT_BYTES)) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let scope = match ksp_job_backfill_lib::BackfillScope::explicit_signatures(vec![signature]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let job_id = match ksp_job_api::JobId::new("backfill-desk-monitor-test") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let network = match ksp_store_lib::RawNetworkId::new("mainnet-beta") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let request = ksp_job_backfill_lib::BackfillRequest::new(
|
||||
job_id,
|
||||
network,
|
||||
ksp_onchain_transport_lib::HttpRoleName::new("backfill_pool"),
|
||||
ksp_job_backfill_lib::BackfillCommitment::Confirmed,
|
||||
scope,
|
||||
100,
|
||||
10,
|
||||
1,
|
||||
1,
|
||||
std::option::Option::None,
|
||||
);
|
||||
let request = match request {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let runtime = match ksp_job_backfill_lib::BackfillJobRuntime::new(request) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::option::Option::None,
|
||||
};
|
||||
let source = runtime.handle().snapshots();
|
||||
return std::option::Option::Some(<ksp_job_backfill_lib::BackfillSnapshotSource as ksp_job_api::JobSnapshotSource>::current(&source));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latest_value_projection_is_safe_complete_and_uses_decimal_sequence_text() {
|
||||
let notification = match initial_notification() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
let status = crate::project_backfill_status(¬ification);
|
||||
assert!(status.is_ok());
|
||||
let status = match status {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
assert!(status.active);
|
||||
assert!(!status.terminal);
|
||||
assert_eq!(status.job_id, "backfill-desk-monitor-test");
|
||||
assert_eq!(status.job_kind, ksp_job_backfill_lib::BACKFILL_JOB_KIND_CODE);
|
||||
assert_eq!(status.sequence, "0");
|
||||
assert_eq!(status.state, "created");
|
||||
assert_eq!(status.phase, "created");
|
||||
assert_eq!(status.scope_kind, "explicit_signatures");
|
||||
assert!(!status.checkpoint_present);
|
||||
assert_eq!(status.candidates_selected, 0);
|
||||
assert_eq!(status.failure_code, std::option::Option::None);
|
||||
assert_eq!(status.failure_domain, std::option::Option::None);
|
||||
let serialized = serde_json::to_string(&status);
|
||||
assert!(serialized.is_ok());
|
||||
if let std::result::Result::Ok(serialized) = serialized {
|
||||
for forbidden in ["address", "signature\"", "endpoint", "credential", "token", "postgres"] {
|
||||
assert!(!serialized.contains(forbidden));
|
||||
}
|
||||
}
|
||||
}
|
||||
124
deltas/0.3.7/pre.009.md
Normal file
124
deltas/0.3.7/pre.009.md
Normal file
@@ -0,0 +1,124 @@
|
||||
<!-- file: deltas/0.3.7/pre.009.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta `0.3.7-pre.009` — monitoring latest-value Backfill Desk
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
```text
|
||||
0.3.7-pre.008-fix.003
|
||||
workspace.package.version = 0.3.7-pre.8.fix.3
|
||||
```
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Projeter le flux latest-value déjà fourni par `ksp-job-backfill-lib` vers Backfill Desk, avec événement coalescé et commande de resynchronisation, sans parser les logs et sans encore ouvrir Cancel ou Resume.
|
||||
|
||||
## 3. Bridge latest-value
|
||||
|
||||
`BackfillRunLaunch` conserve une `BackfillSnapshotSource` indépendante du runtime exécuté. Le command handler démarre un task de monitoring qui lit la valeur complète courante puis attend `JobSnapshotSource::wait_for_change`; les updates intermédiaires peuvent être coalescées par le contrat Job et chaque notification frontend reste une valeur complète.
|
||||
|
||||
L'événement Tauri est `ksp-backfill-status`. Aucun event bus générique, queue métier ou parsing des logs n'est introduit.
|
||||
|
||||
## 4. Resynchronisation et terminal retenu
|
||||
|
||||
`BackfillRunState` conserve le handle pendant l'activité et retient la dernière `JobNotification<BackfillJobSnapshot>` terminale avant de libérer le slot single-run. `backfill_status` retourne la notification active courante ou ce dernier terminal retenu. L'UI peut ainsi se recaler au démarrage, après Start ou sur demande manuelle même si elle a manqué un événement.
|
||||
|
||||
## 5. DTO de statut sûr
|
||||
|
||||
`BackfillRunStatusDto` expose uniquement :
|
||||
|
||||
```text
|
||||
active / terminal
|
||||
job_id / job_kind
|
||||
sequence (texte décimal)
|
||||
state / completion / phase / scope_kind / discovery_boundary
|
||||
candidates_selected / candidates_admitted / candidates_finished
|
||||
entities_inserted / entities_existing / entities_purged
|
||||
observations_inserted / observations_existing
|
||||
missing / conflicts / cancelled_candidates / holes
|
||||
maximum_in_flight / contiguous_completed
|
||||
checkpoint_present
|
||||
failure_domain / failure_code
|
||||
```
|
||||
|
||||
Aucune adresse, signature, ancre, URL/endpoint/provider, credential, payload RAW, contexte d'erreur ou valeur de checkpoint ne traverse cette surface.
|
||||
|
||||
## 6. Frontend
|
||||
|
||||
Le frontend installe le listener `ksp-backfill-status`, resynchronise via `backfill_status`, affiche le snapshot complet dans une carte dédiée et offre une action **Resynchroniser**. Le bouton Start reste verrouillé tant que le statut est actif et est réactivé sur terminal.
|
||||
|
||||
Cancel, état cancelling piloté par l'opérateur et shutdown coopératif restent réservés à `pre.010`.
|
||||
|
||||
## 7. Version
|
||||
|
||||
```text
|
||||
workspace.package.version = 0.3.7-pre.9
|
||||
label = 0.3.7-pre.009
|
||||
```
|
||||
|
||||
## 8. Fichiers ajoutés
|
||||
|
||||
```text
|
||||
crates/ksp-app-backfill-desk/src/backfill_status.rs
|
||||
crates/ksp-app-backfill-desk/unit_tests/backfill_status.rs
|
||||
deltas/0.3.7/pre.009.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/backfill_run.rs
|
||||
crates/ksp-app-backfill-desk/src/lib.rs
|
||||
crates/ksp-app-backfill-desk/src/tauri.rs
|
||||
crates/ksp-app-backfill-desk/tests/desktop_contract.rs
|
||||
crates/ksp-app-backfill-desk/tests/desktop_security.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.008-fix.003` est intégralement propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, `ksp-job-backfill-lib` et toutes les suites Backfill Desk passent.
|
||||
|
||||
## 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 ;
|
||||
- source latest-value et `wait_for_change` présents, sans log parsing ;
|
||||
- terminal retenu + commande de resynchronisation ;
|
||||
- DTO de monitoring sans payload métier/checkpoint concret ;
|
||||
- capabilities et dépendances inchangées ;
|
||||
- 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.010` :
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## 14. Suite
|
||||
|
||||
`pre.010` ajoutera Cancel, état cancelling, fermeture coopérative et races Start/Cancel/terminal.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# Plan v0.3.7 — Backfill Desk
|
||||
|
||||
@@ -269,7 +269,9 @@ Ouvrir le Start réel en réutilisant strictement le mapping `pre.007` : JobId g
|
||||
|
||||
### pre.009 — monitoring latest-value
|
||||
|
||||
Bridge de snapshots, status DTO, événements coalescés, compteurs complets, resynchronisation et absence de log parsing.
|
||||
Bridge direct de `BackfillJobHandle::snapshots()` vers un DTO applicatif sûr et complet, sans parser les logs. Le slot single-run conserve la source latest-value pendant le run et retient le dernier terminal après libération afin que `backfill_status` puisse resynchroniser l'UI même si un événement a été manqué.
|
||||
|
||||
Le backend émet `ksp-backfill-status` à partir de `JobSnapshotSource::wait_for_change`; cette primitive coalesce nativement les mises à jour intermédiaires et transporte toujours la dernière valeur complète. Le DTO expose JobId/kind, sequence décimale, lifecycle/completion, phase/scope/boundary, compteurs candidates/entities/observations/anomalies, maximum in-flight, `checkpoint_present`, `contiguous_completed` et code d'échec stable. Il n'expose ni adresse, signature, endpoint/provider, payload RAW, contexte d'erreur ni checkpoint lui-même. La terminaison réactive le Start frontend ; Cancel reste absent jusqu'à `pre.010`.
|
||||
|
||||
### pre.010 — Cancel et races terminales
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/024-V0_3_7_BACKFILL_DESK.md -->
|
||||
<!-- version: 14 -->
|
||||
<!-- version: 15 -->
|
||||
|
||||
# Validation v0.3.7 — Backfill Desk
|
||||
|
||||
@@ -420,3 +420,27 @@ Le correctif ajoute un helper de test qui extrait uniquement la déclaration de
|
||||
- [X] audits statiques Rust/Markdown rejoués dans l'environnement d'assemblage ;
|
||||
- [ ] replay opérateur `cargo fmt/check/clippy/test` de `pre.008-fix.003` à exécuter ; aucun `cargo tree` requis car dépendances/features inchangées.
|
||||
|
||||
## 21. `pre.009` — monitoring latest-value et resynchronisation
|
||||
|
||||
Le replay opérateur de `pre.008-fix.003` ferme le couloir Start : audits Rust/Markdown, `cargo check --workspace`, Clippy, les 49 tests unitaires `ksp-job-backfill-lib` et toutes les suites Backfill Desk passent. Aucun `cargo tree` supplémentaire n'était requis pour ce dernier fix test-only sans changement de dépendances/features.
|
||||
|
||||
`pre.009` branche directement la source latest-value déjà possédée par `BackfillJobHandle`. `BackfillRunLaunch` conserve une `BackfillSnapshotSource` indépendante avant déplacement du runtime dans le task, tandis que `BackfillRunState` conserve le handle actif et retient la dernière notification terminale après `finish`. La commande `backfill_status` retourne donc soit la valeur active courante, soit le dernier terminal retenu, ce qui fournit une resynchronisation explicite sans dépendre de l'historique des événements frontend.
|
||||
|
||||
Le bridge Tauri attend `JobSnapshotSource::wait_for_change` et émet `ksp-backfill-status`. Cette primitive latest-value coalesce les mises à jour intermédiaires : aucun event queue métier supplémentaire ni polling de logs n'est introduit. `BackfillRunStatusDto` expose uniquement des métadonnées sûres et bornées : JobId/kind, sequence en texte décimal, lifecycle/completion, phase/scope/boundary, compteurs candidates/entities/observations, missing/conflicts/cancelled/holes, maximum in-flight, `checkpoint_present`, `contiguous_completed` et éventuel `failure_domain/failure_code`. Adresse, signatures, endpoint/provider, payload RAW, contexte d'erreur et checkpoint concret restent backend-only.
|
||||
|
||||
Le frontend installe le listener avant la resynchronisation startup, rend le dernier snapshot complet, propose un bouton manuel de resynchronisation et réactive Start lorsque le snapshot devient terminal. Cancel, état cancelling piloté par l'UI et races fermeture/Cancel/terminal restent réservés à `pre.010`.
|
||||
|
||||
### Gate statique local `pre.009`
|
||||
|
||||
- [X] monitoring branché sur `JobSnapshotSource`, sans parsing de logs ;
|
||||
- [X] événement latest-value coalescé `ksp-backfill-status` ;
|
||||
- [X] commande `backfill_status` de resynchronisation ;
|
||||
- [X] dernier terminal retenu après libération du slot single-run ;
|
||||
- [X] sequence `u64` projetée comme texte décimal ;
|
||||
- [X] compteurs complets projetés avec conversion bornée vers `u32` ;
|
||||
- [X] checkpoint projeté seulement par présence + frontier contigu ;
|
||||
- [X] erreurs projetées seulement par domaine/code stables ;
|
||||
- [X] aucun payload métier, URL/provider/credential ou checkpoint concret exposé ;
|
||||
- [X] frontend réactive Start sur terminal et garde un resync manuel ;
|
||||
- [X] aucun Cancel/Resume ajouté prématurément ;
|
||||
- [ ] `cargo fmt/check/clippy/test` de `pre.009` à rejouer par l'opérateur ; `cargo`/`rustfmt` restent absents du sandbox.
|
||||
|
||||
Reference in New Issue
Block a user