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,5 +1,5 @@
<!-- file: crates/ksp-app-backfill-desk/frontend/main.html -->
<!-- version: 7 -->
<!-- version: 8 -->
<!DOCTYPE html>
<html lang="fr">
@@ -87,7 +87,9 @@
</div>
<div id="backfillAddressGroup" class="col-12">
<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 id="backfillAnchorGroup" class="col-12" hidden>
<label for="backfillAnchorSignature" class="form-label">Signature d'ancrage exclusive</label>
@@ -149,6 +151,7 @@
<div class="card-body">
<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="backfillTerminalSummary" class="alert alert-secondary py-2" role="status" aria-live="polite" hidden></div>
<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>

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
// version: 7
// version: 8
import "bootstrap";
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 { 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 { 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 { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
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 {
renderProgramIdAutocomplete(options.programIdOptions);
populateCodeSelect("backfillScopeKind", options.scopeKinds, scopeLabels);
populateCodeSelect("backfillCommitment", options.commitments, commitmentLabels);
applyNumberInputContract("backfillPageSize", options.limits.maxPageSize, options.limits.defaultPageSize);
@@ -296,13 +346,7 @@ function renderRequestValidationError(caughtError: unknown): void {
if (!status) {
return;
}
let message = "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}`;
}
}
const message = safeCommandErrorText(caughtError, "Requête refusée par le backend.");
status.hidden = false;
status.classList.remove("alert-secondary", "alert-success");
status.classList.add("alert-danger");
@@ -373,6 +417,16 @@ function bindCampaignForm(): void {
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");
if (startButton) {
startButton.addEventListener("click", () => {
@@ -472,6 +526,11 @@ function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "c
if (card) {
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 });
return;
}
@@ -489,6 +548,27 @@ function renderBackfillRunStatus(status: BackfillRunStatusDto | null, source: "c
if (card) {
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> = {
runStatusJobId: status.jobId,
runStatusLifecycle: status.completion === null ? status.state : `${status.state} / ${status.completion}`,
@@ -552,11 +632,18 @@ async function resumeBackfillRun(): Promise<void> {
state: response.state,
});
await syncBackfillStatus("resume");
} catch (_caughtError) {
} catch (caughtError) {
resumeInFlight = false;
if (resumeButton && currentRunStatus?.terminal && currentRunStatus.checkpointPresent) {
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");
}
}
@@ -597,7 +684,14 @@ async function bindBackfillStatusMonitoring(): Promise<void> {
});
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");
if (currentRunStatus?.active) {
cancelButton.disabled = false;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-app-backfill-desk/src/app_state.rs
// version: 9
// version: 10
//! 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(),
config_document_count: document_count,
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(),
});
}
@@ -146,6 +146,7 @@ impl crate::AppState {
http_routes: std::vec::Vec::new(),
limits,
network_coherent: false,
program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: 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
// version: 5
// version: 6
//! Common Tauri DTOs shared by the Backfill Desk shell.
@@ -21,6 +21,45 @@ pub(crate) struct BackfillHttpRouteOptionDto {
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.
///
/// Endpoint URLs, credentials and physical routing details are intentionally absent.
@@ -36,6 +75,8 @@ pub(crate) struct BackfillDeskOptionsDto {
pub(crate) limits: crate::BackfillRequestLimitsDto,
/// Whether the Transport and Store readiness gates jointly permit later Backfill composition.
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.
pub(crate) configured_networks: std::vec::Vec<String>,
/// 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
// version: 10
// version: 11
//! 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;
/// Safe command error projection exposed to Tauri commands.
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.
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.
pub(crate) use self::errors::ERROR_CODE_APP_STATE_INVALID;
/// 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
// version: 5
// version: 6
//! Composite-selected HTTP Transport readiness and route inventory owned by Backfill Desk.
@@ -54,6 +54,7 @@ impl TransportRuntime {
http_routes,
limits,
network_coherent: false,
program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: 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
// version: 13
// version: 14
//! 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("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
// version: 14
// version: 15
//! 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}");
}
}
#[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
// version: 2
// version: 3
fn ready_options() -> crate::BackfillDeskOptionsDto {
let limits = crate::backfill_request_limits();
@@ -33,6 +33,7 @@ fn ready_options() -> crate::BackfillDeskOptionsDto {
],
limits,
network_coherent: true,
program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None,
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
// version: 5
// version: 6
#[test]
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,
configured_networks: vec!["devnet".to_owned()],
network_coherent: true,
program_id_options: crate::program_id_autocomplete_options(),
scope_kinds: crate::backfill_scope_kind_codes(),
store_diagnostic: std::option::Option::None,
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"));
}
}
#[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());
}
}