v0.3.7-pre.009
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user