v0.3.7-pre.012

This commit is contained in:
2026-09-02 20:29:50 +02:00
parent 618f940310
commit 4c16607e22
14 changed files with 388 additions and 26 deletions

View File

@@ -1,12 +1,12 @@
# file: Cargo.toml # file: Cargo.toml
# version: 435 # version: 436
[workspace] [workspace]
resolver = "3" 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"] 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] [workspace.package]
version = "0.3.7-pre.11" version = "0.3.7-pre.12"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-app-backfill-desk/frontend/main.html --> <!-- file: crates/ksp-app-backfill-desk/frontend/main.html -->
<!-- version: 7 --> <!-- version: 8 -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
@@ -87,7 +87,9 @@
</div> </div>
<div id="backfillAddressGroup" class="col-12"> <div id="backfillAddressGroup" class="col-12">
<label for="backfillAddress" class="form-label">Adresse Solana</label> <label for="backfillAddress" class="form-label">Adresse Solana</label>
<input id="backfillAddress" class="form-control font-monospace" type="text" autocomplete="off" spellcheck="false"> <input id="backfillAddress" class="form-control font-monospace" type="text" list="backfillProgramIds" autocomplete="off" spellcheck="false" aria-describedby="backfillProgramIdAutocompleteHelp">
<datalist id="backfillProgramIds"></datalist>
<div id="backfillProgramIdAutocompleteHelp" class="form-text">Suggestions issues du registre canonique KSP ; toute adresse Solana valide reste acceptée.</div>
</div> </div>
<div id="backfillAnchorGroup" class="col-12" hidden> <div id="backfillAnchorGroup" class="col-12" hidden>
<label for="backfillAnchorSignature" class="form-label">Signature d'ancrage exclusive</label> <label for="backfillAnchorSignature" class="form-label">Signature d'ancrage exclusive</label>
@@ -149,6 +151,7 @@
<div class="card-body"> <div class="card-body">
<div id="backfillCancelFeedback" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div> <div id="backfillCancelFeedback" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
<div id="backfillResumeFeedback" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div> <div id="backfillResumeFeedback" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
<div id="backfillTerminalSummary" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
<dl class="row mb-0 app-runtime-list"> <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">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">Lifecycle</dt><dd id="runStatusLifecycle" class="col-sm-7"></dd>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts // file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
// version: 7 // version: 8
import "bootstrap"; import "bootstrap";
import ResizeObserver from "resize-observer-polyfill"; import ResizeObserver from "resize-observer-polyfill";
@@ -14,6 +14,7 @@ import type { BackfillStartRequestDto } from "./bindings/ksp_app_backfill_desk/d
import type { BackfillStartResponseDto } from "./bindings/ksp_app_backfill_desk/dto_backfill/BackfillStartResponseDto.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 { 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 { BackfillHttpRouteOptionDto } from "./bindings/ksp_app_backfill_desk/dto_common/BackfillHttpRouteOptionDto.ts";
import type { ProgramIdAutocompleteOptionDto } from "./bindings/ksp_app_backfill_desk/dto_common/ProgramIdAutocompleteOptionDto.ts";
import type { ShellStatusDto } from "./bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts"; import type { ShellStatusDto } from "./bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts";
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log"; import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
import { invokeKsp } from "./invoke"; import { invokeKsp } from "./invoke";
@@ -141,7 +142,56 @@ function refreshStartButton(): void {
} }
} }
function renderProgramIdAutocomplete(options: ProgramIdAutocompleteOptionDto[]): void {
const datalist = document.querySelector<HTMLDataListElement>("#backfillProgramIds");
if (!datalist) {
return;
}
datalist.replaceChildren();
for (const program of options) {
const option = document.createElement("option");
option.value = program.programId;
option.label = `${program.name}${program.code}`;
option.dataset.code = program.code;
option.dataset.domain = program.domain;
option.dataset.family = program.family;
option.dataset.name = program.name;
option.dataset.protocol = program.protocol;
datalist.append(option);
}
const help = document.querySelector<HTMLElement>("#backfillProgramIdAutocompleteHelp");
if (help) {
help.textContent = `${options.length} Program ID KSP disponibles comme suggestions ; la saisie reste libre.`;
}
frontendTrace("main", "Backfill Desk Program ID autocomplete dataset rendered", { optionCount: options.length });
}
function selectedProgramAutocompleteEntry(address: string): HTMLOptionElement | null {
const datalist = document.querySelector<HTMLDataListElement>("#backfillProgramIds");
if (!datalist) {
return null;
}
for (const option of Array.from(datalist.options)) {
if (option.value === address) {
return option;
}
}
return null;
}
function safeCommandErrorText(caughtError: unknown, fallback: string): string {
if (typeof caughtError === "object" && caughtError !== null) {
const candidate = caughtError as { code?: unknown; domain?: unknown; message?: unknown };
if (typeof candidate.domain === "string" && typeof candidate.code === "string" && typeof candidate.message === "string") {
return `${candidate.domain}/${candidate.code}: ${candidate.message}`;
}
}
return fallback;
}
function renderCampaignContract(options: BackfillDeskOptionsDto): void { function renderCampaignContract(options: BackfillDeskOptionsDto): void {
renderProgramIdAutocomplete(options.programIdOptions);
populateCodeSelect("backfillScopeKind", options.scopeKinds, scopeLabels); populateCodeSelect("backfillScopeKind", options.scopeKinds, scopeLabels);
populateCodeSelect("backfillCommitment", options.commitments, commitmentLabels); populateCodeSelect("backfillCommitment", options.commitments, commitmentLabels);
applyNumberInputContract("backfillPageSize", options.limits.maxPageSize, options.limits.defaultPageSize); applyNumberInputContract("backfillPageSize", options.limits.maxPageSize, options.limits.defaultPageSize);
@@ -296,13 +346,7 @@ function renderRequestValidationError(caughtError: unknown): void {
if (!status) { if (!status) {
return; return;
} }
let message = "Requête refusée par le backend."; const message = safeCommandErrorText(caughtError, "Requête refusée par le backend.");
if (typeof caughtError === "object" && caughtError !== null) {
const candidate = caughtError as { code?: unknown; domain?: unknown; message?: unknown };
if (typeof candidate.domain === "string" && typeof candidate.code === "string" && typeof candidate.message === "string") {
message = `${candidate.domain}/${candidate.code}: ${candidate.message}`;
}
}
status.hidden = false; status.hidden = false;
status.classList.remove("alert-secondary", "alert-success"); status.classList.remove("alert-secondary", "alert-success");
status.classList.add("alert-danger"); status.classList.add("alert-danger");
@@ -373,6 +417,16 @@ function bindCampaignForm(): void {
void validateBackfillRequest(); void validateBackfillRequest();
}); });
} }
const address = document.querySelector<HTMLInputElement>("#backfillAddress");
if (address) {
address.addEventListener("change", () => {
const selected = selectedProgramAutocompleteEntry(address.value);
frontendTrace("main", "Backfill Desk address autocomplete selection evaluated", {
programCode: selected?.dataset.code ?? null,
registryMatch: selected !== null,
});
});
}
const startButton = document.querySelector<HTMLButtonElement>("#startBackfillRequest"); const startButton = document.querySelector<HTMLButtonElement>("#startBackfillRequest");
if (startButton) { if (startButton) {
startButton.addEventListener("click", () => { startButton.addEventListener("click", () => {
@@ -472,6 +526,11 @@ function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "c
if (card) { if (card) {
card.hidden = true; card.hidden = true;
} }
const terminalSummary = document.querySelector<HTMLElement>("#backfillTerminalSummary");
if (terminalSummary) {
terminalSummary.hidden = true;
terminalSummary.textContent = "";
}
frontendTrace("main", "Backfill Desk latest-value status cleared", { source }); frontendTrace("main", "Backfill Desk latest-value status cleared", { source });
return; return;
} }
@@ -489,6 +548,27 @@ function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "c
if (card) { if (card) {
card.hidden = false; card.hidden = false;
} }
const terminalSummary = document.querySelector<HTMLElement>("#backfillTerminalSummary");
if (terminalSummary) {
terminalSummary.hidden = !status.terminal;
terminalSummary.classList.remove("alert-success", "alert-warning", "alert-danger", "alert-secondary");
if (status.terminal) {
const failure = status.failureDomain === null || status.failureCode === null ? null : `${status.failureDomain}/${status.failureCode}`;
if (status.state === "completed") {
terminalSummary.classList.add("alert-success");
terminalSummary.textContent = `Campagne terminée — ${status.contiguousCompleted} candidats contigus durablement traités.`;
} else if (status.state === "cancelled") {
terminalSummary.classList.add("alert-warning");
terminalSummary.textContent = `Campagne annulée coopérativement — checkpoint ${status.checkpointPresent ? "disponible" : "absent"}.`;
} else {
terminalSummary.classList.add("alert-danger");
terminalSummary.textContent = `Campagne terminée en échec${failure === null ? "" : `${failure}`}.`;
}
} else {
terminalSummary.classList.add("alert-secondary");
terminalSummary.textContent = "";
}
}
const values: Record<string, string> = { const values: Record<string, string> = {
runStatusJobId: status.jobId, runStatusJobId: status.jobId,
runStatusLifecycle: status.completion === null ? status.state : `${status.state} / ${status.completion}`, runStatusLifecycle: status.completion === null ? status.state : `${status.state} / ${status.completion}`,
@@ -552,11 +632,18 @@ async function resumeBackfillRun(): Promise<void> {
state: response.state, state: response.state,
}); });
await syncBackfillStatus("resume"); await syncBackfillStatus("resume");
} catch (_caughtError) { } catch (caughtError) {
resumeInFlight = false; resumeInFlight = false;
if (resumeButton && currentRunStatus?.terminal && currentRunStatus.checkpointPresent) { if (resumeButton && currentRunStatus?.terminal && currentRunStatus.checkpointPresent) {
resumeButton.disabled = false; resumeButton.disabled = false;
} }
const feedback = document.querySelector<HTMLElement>("#backfillResumeFeedback");
if (feedback) {
feedback.hidden = false;
feedback.classList.remove("alert-secondary", "alert-success");
feedback.classList.add("alert-danger");
feedback.textContent = safeCommandErrorText(caughtError, "La reprise du checkpoint a été refusée.");
}
frontendWarn("main", "Backfill Desk in-session Resume failed"); frontendWarn("main", "Backfill Desk in-session Resume failed");
} }
} }
@@ -597,7 +684,14 @@ async function bindBackfillStatusMonitoring(): Promise<void> {
}); });
return syncBackfillStatus("cancel"); return syncBackfillStatus("cancel");
}) })
.catch(() => { .catch(caughtError => {
const feedback = document.querySelector<HTMLElement>("#backfillCancelFeedback");
if (feedback) {
feedback.hidden = false;
feedback.classList.remove("alert-secondary", "alert-success");
feedback.classList.add("alert-danger");
feedback.textContent = safeCommandErrorText(caughtError, "La demande d'annulation a été refusée.");
}
frontendWarn("main", "Backfill Desk cooperative cancellation failed"); frontendWarn("main", "Backfill Desk cooperative cancellation failed");
if (currentRunStatus?.active) { if (currentRunStatus?.active) {
cancelButton.disabled = false; cancelButton.disabled = false;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/app_state.rs // file: crates/ksp-app-backfill-desk/src/app_state.rs
// version: 9 // version: 10
//! Shared backend state owned by the Backfill Desk Tauri application. //! Shared backend state owned by the Backfill Desk Tauri application.
@@ -123,7 +123,7 @@ impl crate::AppState {
application_version: env!("CARGO_PKG_VERSION").to_owned(), application_version: env!("CARGO_PKG_VERSION").to_owned(),
config_document_count: document_count, config_document_count: document_count,
fallback_logging_active: runtime.fallback_active, fallback_logging_active: runtime.fallback_active,
shell_phase: "pre.011-resume-checkpoint".to_owned(), shell_phase: "pre.012-frontend-polish".to_owned(),
startup_diagnostic: runtime.startup_diagnostic.clone(), startup_diagnostic: runtime.startup_diagnostic.clone(),
}); });
} }
@@ -146,6 +146,7 @@ impl crate::AppState {
http_routes: std::vec::Vec::new(), http_routes: std::vec::Vec::new(),
limits, limits,
network_coherent: false, network_coherent: false,
program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(), scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None, store_diagnostic: std::option::Option::None,
store_network: std::option::Option::None, store_network: std::option::Option::None,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/dto_common.rs // file: crates/ksp-app-backfill-desk/src/dto_common.rs
// version: 5 // version: 6
//! Common Tauri DTOs shared by the Backfill Desk shell. //! Common Tauri DTOs shared by the Backfill Desk shell.
@@ -21,6 +21,45 @@ pub(crate) struct BackfillHttpRouteOptionDto {
pub(crate) role: String, pub(crate) role: String,
} }
/// Safe Program ID entry exposed only to populate the free-form address autocomplete dataset.
///
/// Every value is derived from the canonical `ksp-core-lib` registry. The frontend remains free to submit any valid Solana address; this DTO is advisory only.
#[derive(Clone, Debug, serde::Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_backfill_desk/dto_common/ProgramIdAutocompleteOptionDto.ts")]
pub(crate) struct ProgramIdAutocompleteOptionDto {
/// Stable KSP machine-readable Program ID code.
pub(crate) code: String,
/// Broad functional domain from the canonical registry.
pub(crate) domain: String,
/// Functional family from the canonical registry.
pub(crate) family: String,
/// Human-readable program name.
pub(crate) name: String,
/// Canonical Base58 Program ID used as the HTML datalist value.
pub(crate) program_id: String,
/// Owning protocol/project label from the canonical registry.
pub(crate) protocol: String,
}
/// Projects the canonical KSP Program ID registry to the safe autocomplete dataset.
#[must_use]
pub(crate) fn program_id_autocomplete_options() -> std::vec::Vec<ProgramIdAutocompleteOptionDto> {
let entries = ksp_core_lib::entries();
let mut options = std::vec::Vec::with_capacity(entries.len());
for entry in entries {
options.push(ProgramIdAutocompleteOptionDto {
code: entry.code().to_owned(),
domain: entry.domain().to_owned(),
family: entry.family().to_owned(),
name: entry.name().to_owned(),
program_id: entry.program_id().to_owned(),
protocol: entry.protocol().to_owned(),
});
}
return options;
}
/// Safe Backfill Desk options contract combining readiness, HTTP routes and Job-owned campaign bounds. /// Safe Backfill Desk options contract combining readiness, HTTP routes and Job-owned campaign bounds.
/// ///
/// Endpoint URLs, credentials and physical routing details are intentionally absent. /// Endpoint URLs, credentials and physical routing details are intentionally absent.
@@ -36,6 +75,8 @@ pub(crate) struct BackfillDeskOptionsDto {
pub(crate) limits: crate::BackfillRequestLimitsDto, pub(crate) limits: crate::BackfillRequestLimitsDto,
/// Whether the Transport and Store readiness gates jointly permit later Backfill composition. /// Whether the Transport and Store readiness gates jointly permit later Backfill composition.
pub(crate) composition_ready: bool, pub(crate) composition_ready: bool,
/// Canonical KSP Program IDs exposed as a frontend autocomplete dataset; never an allow-list.
pub(crate) program_id_options: std::vec::Vec<ProgramIdAutocompleteOptionDto>,
/// Distinct enabled HTTP cluster labels selected by the active Transport profile. /// Distinct enabled HTTP cluster labels selected by the active Transport profile.
pub(crate) configured_networks: std::vec::Vec<String>, pub(crate) configured_networks: std::vec::Vec<String>,
/// Whether the selected Store network exactly matches the one coherent Transport network. /// Whether the selected Store network exactly matches the one coherent Transport network.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/lib.rs // file: crates/ksp-app-backfill-desk/src/lib.rs
// version: 10 // version: 11
//! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs. //! Tauri desktop application scaffold for controlling and inspecting KSP RAW backfill jobs.
@@ -116,8 +116,12 @@ pub(crate) use self::dto_common::BackfillDeskOptionsDto;
pub(crate) use self::dto_common::BackfillHttpRouteOptionDto; pub(crate) use self::dto_common::BackfillHttpRouteOptionDto;
/// Safe command error projection exposed to Tauri commands. /// Safe command error projection exposed to Tauri commands.
pub(crate) use self::dto_common::CommandErrorDto; pub(crate) use self::dto_common::CommandErrorDto;
/// Safe canonical Program ID entry exposed only for free-form address autocomplete.
pub(crate) use self::dto_common::ProgramIdAutocompleteOptionDto;
/// Safe scaffold/runtime snapshot exposed to the shell. /// Safe scaffold/runtime snapshot exposed to the shell.
pub(crate) use self::dto_common::ShellStatusDto; pub(crate) use self::dto_common::ShellStatusDto;
/// Projects the canonical Core Program ID registry to autocomplete entries.
pub(crate) use self::dto_common::program_id_autocomplete_options;
/// Shared Backfill Desk runtime state is internally inconsistent. /// Shared Backfill Desk runtime state is internally inconsistent.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID; pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// Shared Backfill Desk runtime state cannot be locked safely. /// Shared Backfill Desk runtime state cannot be locked safely.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/transport_runtime.rs // file: crates/ksp-app-backfill-desk/src/transport_runtime.rs
// version: 5 // version: 6
//! Composite-selected HTTP Transport readiness and route inventory owned by Backfill Desk. //! Composite-selected HTTP Transport readiness and route inventory owned by Backfill Desk.
@@ -54,6 +54,7 @@ impl TransportRuntime {
http_routes, http_routes,
limits, limits,
network_coherent: false, network_coherent: false,
program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(), scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None, store_diagnostic: std::option::Option::None,
store_network: std::option::Option::None, store_network: std::option::Option::None,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs // file: crates/ksp-app-backfill-desk/tests/desktop_contract.rs
// version: 13 // version: 14
//! Structural desktop contract checks for the Backfill Desk scaffold. //! Structural desktop contract checks for the Backfill Desk scaffold.
@@ -404,3 +404,33 @@ fn pre_011_resume_reissues_rust_only_checkpoint_for_new_backend_job_without_ipc_
assert!(html.contains("resumeBackfillRun")); assert!(html.contains("resumeBackfillRun"));
assert!(html.contains("Reprendre")); assert!(html.contains("Reprendre"));
} }
#[test]
fn pre_012_free_address_autocomplete_is_html_datalist_derived_from_core_registry() {
let root = app_root();
let dto = read_text(root.join("src/dto_common.rs").as_path());
assert!(dto.contains("ksp_core_lib::entries()"));
assert!(dto.contains("ProgramIdAutocompleteOptionDto"));
assert!(dto.contains("program_id_autocomplete_options"));
let html = read_text(root.join("frontend/main.html").as_path());
assert!(html.contains("list=\"backfillProgramIds\""));
assert!(html.contains("<datalist id=\"backfillProgramIds\"></datalist>"));
assert!(html.contains("toute adresse Solana valide reste acceptée"));
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
for required in [
"renderProgramIdAutocomplete(options.programIdOptions)",
"option.value = program.programId",
"option.dataset.code = program.code",
"option.dataset.domain = program.domain",
"registryMatch: selected !== null",
"Backfill Desk Program ID autocomplete dataset rendered",
] {
assert!(frontend.contains(required), "missing pre.012 autocomplete marker {required}");
}
for forbidden in
["AddressLookupTab1e1111111111111111111111111", "ComputeBudget111111111111111111111111111111", "Vote111111111111111111111111111111111111111"]
{
assert!(!frontend.contains(forbidden), "frontend hardcodes canonical Program ID {forbidden}");
assert!(!html.contains(forbidden), "HTML hardcodes canonical Program ID {forbidden}");
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/tests/desktop_security.rs // file: crates/ksp-app-backfill-desk/tests/desktop_security.rs
// version: 14 // version: 15
//! Security and dependency-boundary checks for the Backfill Desk scaffold. //! Security and dependency-boundary checks for the Backfill Desk scaffold.
@@ -339,3 +339,29 @@ fn pre_011_resume_surface_exposes_only_new_job_acknowledgement_and_never_checkpo
assert!(!resume_source.contains(forbidden), "Resume frontend sends or logs forbidden payload marker {forbidden}"); assert!(!resume_source.contains(forbidden), "Resume frontend sends or logs forbidden payload marker {forbidden}");
} }
} }
#[test]
fn pre_012_program_id_autocomplete_exposes_only_public_registry_metadata_and_no_browser_storage() {
let root = app_root();
let dto_source = read_text(root.join("src/dto_common.rs").as_path());
let dto = struct_source(dto_source.as_str(), "ProgramIdAutocompleteOptionDto");
for required in ["code", "domain", "family", "name", "program_id", "protocol"] {
assert!(dto.contains(required), "Program ID autocomplete DTO missing safe registry field {required}");
}
for forbidden in ["url", "credential", "token", "secret", "endpoint", "connection", "private_key"] {
assert!(!dto.contains(forbidden), "Program ID autocomplete DTO leaks forbidden marker {forbidden}");
}
let frontend = read_text(root.join("frontend/ts/main.ts").as_path());
for forbidden in ["localStorage", "sessionStorage", "indexedDB", "document.cookie"] {
assert!(!frontend.contains(forbidden), "frontend persists campaign/autocomplete material via {forbidden}");
}
let selection_marker = frontend.find("Backfill Desk address autocomplete selection evaluated");
assert!(selection_marker.is_some());
let selection_source = match selection_marker {
std::option::Option::Some(index) => &frontend[index..std::cmp::min(index + 320, frontend.len())],
std::option::Option::None => "",
};
assert!(selection_source.contains("programCode"));
assert!(selection_source.contains("registryMatch"));
assert!(!selection_source.contains("address.value"));
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs // file: crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
// version: 2 // version: 3
fn ready_options() -> crate::BackfillDeskOptionsDto { fn ready_options() -> crate::BackfillDeskOptionsDto {
let limits = crate::backfill_request_limits(); let limits = crate::backfill_request_limits();
@@ -33,6 +33,7 @@ fn ready_options() -> crate::BackfillDeskOptionsDto {
], ],
limits, limits,
network_coherent: true, network_coherent: true,
program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(), scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None, store_diagnostic: std::option::Option::None,
store_network: std::option::Option::Some("mainnet-beta".to_owned()), store_network: std::option::Option::Some("mainnet-beta".to_owned()),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/unit_tests/dto_common.rs // file: crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
// version: 5 // version: 6
#[test] #[test]
fn command_error_projection_excludes_arbitrary_context_and_source_values() { fn command_error_projection_excludes_arbitrary_context_and_source_values() {
@@ -28,6 +28,7 @@ fn transport_options_projection_contains_only_safe_transport_metadata() {
composition_ready: true, composition_ready: true,
configured_networks: vec!["devnet".to_owned()], configured_networks: vec!["devnet".to_owned()],
network_coherent: true, network_coherent: true,
program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(), scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None, store_diagnostic: std::option::Option::None,
store_network: std::option::Option::Some("devnet".to_owned()), store_network: std::option::Option::Some("devnet".to_owned()),
@@ -49,3 +50,18 @@ fn transport_options_projection_contains_only_safe_transport_metadata() {
assert!(!serialized.contains("database")); assert!(!serialized.contains("database"));
} }
} }
#[test]
fn program_id_autocomplete_dataset_is_derived_exactly_from_core_registry() {
let options = crate::program_id_autocomplete_options();
let entries = ksp_core_lib::entries();
assert_eq!(options.len(), entries.len());
for (option, entry) in options.iter().zip(entries.iter()) {
assert_eq!(option.code, entry.code());
assert_eq!(option.name, entry.name());
assert_eq!(option.program_id, entry.program_id());
assert_eq!(option.domain, entry.domain());
assert_eq!(option.family, entry.family());
assert_eq!(option.protocol, entry.protocol());
}
}

130
deltas/0.3.7/pre.012.md Normal file
View File

@@ -0,0 +1,130 @@
<!-- file: deltas/0.3.7/pre.012.md -->
<!-- version: 1 -->
# Delta `0.3.7-pre.012` — frontend fonctionnel, summary terminal et autocomplete Program ID
## 1. Base requise
```text
0.3.7-pre.011
workspace.package.version = 0.3.7-pre.11
```
## 2. Objectif
Terminer le frontend fonctionnel sans ouvrir de nouveau runtime : enrichir les feedbacks terminal/erreurs et ajouter à l'adresse un autocomplete HTML libre alimenté exclusivement par le registre canonique `ksp-core-lib::entries()`/`ProgramIdEntry`.
## 3. Dataset Program ID canonique
`ProgramIdAutocompleteOptionDto` projette uniquement les métadonnées publiques nécessaires à l'UX :
```text
program_id
code
name
domain
family
protocol
```
`program_id_autocomplete_options()` parcourt directement `ksp_core_lib::entries()`. Aucune liste de Program IDs n'est copiée dans Backfill Desk ni dans le frontend.
`BackfillDeskOptionsDto` transporte ce dataset sûr avec les autres options de campagne. Aucun endpoint, URL, credential, secret ou donnée Store n'est ajouté.
## 4. Autocomplete libre côté HTML
L'input `backfillAddress` reste un `<input type="text">` libre et référence :
```text
<datalist id="backfillProgramIds">
```
Le frontend construit chaque `<option>` avec le Program ID canonique comme `value`, un label lisible et des `data-*` sûrs (`code`, `name`, `domain`, `family`, `protocol`). Une adresse valide qui n'appartient pas au registre reste acceptée normalement par le mapping Rust existant.
L'instrumentation de sélection ne journalise jamais l'adresse : seulement le code KSP éventuel et `registryMatch`. Aucun browser storage n'est introduit.
## 5. Polish monitoring et erreurs
La carte latest-value affiche désormais un résumé terminal explicite pour `completed`, `cancelled` ou `failed`. Les échecs Cancel et Resume sont projetés dans leurs feedbacks UI via le contrat d'erreur sûr déjà existant, sans contexte arbitraire ni payload métier.
Aucun comportement Job/Store/Transport, checkpoint, Resume ou Cancel n'est modifié.
## 6. Version
```text
workspace.package.version = 0.3.7-pre.12
label = 0.3.7-pre.012
```
## 7. Fichiers ajoutés
```text
deltas/0.3.7/pre.012.md
```
## 8. 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/dto_common.rs
crates/ksp-app-backfill-desk/src/lib.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/backfill_request.rs
crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md
docs/validation/024-V0_3_7_BACKFILL_DESK.md
```
## 9. Fichiers supprimés
Aucun.
## 10. Gate précédent acquis
Le replay opérateur de `0.3.7-pre.011` est propre : audits Rust/Markdown, `cargo check --workspace`, Clippy, 50 tests unitaires Job Backfill et toutes les suites Backfill Desk passent. Le smoke Tauri confirme également la reprise in-session réelle avec nouveau JobId backend et checkpoint réémis Rust-only.
## 11. 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 :
- dataset dérivé exclusivement de `ksp_core_lib::entries()` ;
- aucune valeur Program ID canonique hardcodée dans HTML/TypeScript ;
- `<datalist>` relié à un input adresse toujours libre ;
- `data-*` limités aux métadonnées publiques du registre ;
- aucune adresse journalisée pendant l'évaluation autocomplete ;
- aucun `localStorage`, `sessionStorage`, IndexedDB ou cookie ajouté ;
- summary terminal et feedbacks Cancel/Resume sûrs ;
- aucune dépendance/feature Cargo modifiée ;
- `std.store.json` inchangé ;
- syntaxe TypeScript validée par transpilation locale ;
- contrôle différentiel des headers et reconstruction stricte du delta.
## 12. 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.013` :
```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
```
Aucun `cargo tree` n'est requis : dépendances et features Cargo sont inchangées.
## 13. Suite
`pre.013` est réservé au hardening/completude : tests desktop/security/dependency/composition/release-completeness et canaries DTO, sans nouvelle capacité métier.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md --> <!-- file: docs/plans/028-V0_3_7_BACKFILL_DESK_PLAN.md -->
<!-- version: 11 --> <!-- version: 12 -->
# Plan v0.3.7 — Backfill Desk # Plan v0.3.7 — Backfill Desk
@@ -292,6 +292,8 @@ La commande `backfill_resume()` alloue un nouveau JobId backend, refuse un run a
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. 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.
La tranche matérialise ce contrat via `ProgramIdAutocompleteOptionDto`, ajouté à `BackfillDeskOptionsDto`. Chaque `<option>` du `datalist` reçoit le Program ID canonique comme `value` et des `data-*` sûrs (`code`, `name`, `domain`, `family`, `protocol`) uniquement pour l'UX. Une sélection issue du registre ne journalise que le code KSP et le booléen de correspondance, jamais l'adresse saisie. La carte de monitoring reçoit en plus un résumé terminal explicite et les échecs Cancel/Resume sont projetés dans des feedbacks sûrs sans contexte arbitraire.
### pre.013 — hardening et complétude ### pre.013 — hardening et complétude
Tests desktop/security/dependency/composition/release completeness, canaries DTO et scan des dépendances/interdictions. Tests desktop/security/dependency/composition/release completeness, canaries DTO et scan des dépendances/interdictions.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/024-V0_3_7_BACKFILL_DESK.md --> <!-- file: docs/validation/024-V0_3_7_BACKFILL_DESK.md -->
<!-- version: 19 --> <!-- version: 20 -->
# Validation v0.3.7 — Backfill Desk # Validation v0.3.7 — Backfill Desk
@@ -525,5 +525,18 @@ Le frontend expose **Reprendre** uniquement pour un snapshot terminal avec `chec
- [X] frontend Resume sans checkpoint ni payload de campagne ; - [X] frontend Resume sans checkpoint ni payload de campagne ;
- [X] aucun fichier/table/JSON de checkpoint durable ajouté ; - [X] aucun fichier/table/JSON de checkpoint durable ajouté ;
- [X] aucune dépendance/feature Cargo ajoutée ; - [X] aucune dépendance/feature Cargo ajoutée ;
- [ ] `cargo fmt/check/clippy/test` de `pre.011` à rejouer par l'opérateur. - [X] replay opérateur `cargo fmt/check/clippy/test` de `pre.011` propre : 50 tests unitaires Job Backfill et toutes les suites Backfill Desk passent.
Le smoke Tauri `pre.011` confirme également le Resume réel : après un timeout Transport sur un run terminal, `backfill_resume` réémet le checkpoint Rust-only vers un nouveau JobId backend, le nouveau run est admis et repart sur le même rôle HTTP ; un Cancel ciblé ultérieur est accepté puis terminalise proprement le run repris.
## Pre.012 — frontend fonctionnel, summary terminal et autocomplete Program ID
- [X] `BackfillDeskOptionsDto` expose un dataset Program ID dérivé exclusivement de `ksp-core-lib::entries()`.
- [X] `ProgramIdAutocompleteOptionDto` ne contient que `program_id`, `code`, `name`, `domain`, `family` et `protocol`.
- [X] L'input `backfillAddress` reste un champ texte libre et référence `backfillProgramIds` via l'attribut HTML `list`.
- [X] Le frontend construit le `datalist` sans Program ID hardcodé et ajoute des `data-*` sûrs pour l'UX.
- [X] La sélection autocomplete ne journalise jamais l'adresse, seulement le code KSP éventuel et un booléen `registryMatch`.
- [X] Aucun `localStorage`, `sessionStorage`, IndexedDB ou cookie n'est ajouté.
- [X] Le monitoring affiche un résumé terminal explicite et les erreurs Cancel/Resume sont rendues via la projection d'erreur sûre.
- [X] Aucune dépendance/feature Cargo, aucun Store/Transport/Job runtime et aucun `std.store.json` modifié.
- [ ] Gate opérateur `cargo fmt` + audits + check/clippy/tests Backfill à rejouer avant `pre.013`.