diff --git a/Cargo.toml b/Cargo.toml
index db92a8a..c0180ab 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,12 +1,12 @@
# file: Cargo.toml
-# version: 435
+# version: 436
[workspace]
resolver = "3"
members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
[workspace.package]
-version = "0.3.7-pre.11"
+version = "0.3.7-pre.12"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
diff --git a/crates/ksp-app-backfill-desk/frontend/main.html b/crates/ksp-app-backfill-desk/frontend/main.html
index f9d95ee..e4eb607 100644
--- a/crates/ksp-app-backfill-desk/frontend/main.html
+++ b/crates/ksp-app-backfill-desk/frontend/main.html
@@ -1,5 +1,5 @@
-
+
@@ -87,7 +87,9 @@
@@ -149,6 +151,7 @@
+
- Job
- —
- Lifecycle
- —
diff --git a/crates/ksp-app-backfill-desk/frontend/ts/main.ts b/crates/ksp-app-backfill-desk/frontend/ts/main.ts
index 1f00563..3fac34d 100644
--- a/crates/ksp-app-backfill-desk/frontend/ts/main.ts
+++ b/crates/ksp-app-backfill-desk/frontend/ts/main.ts
@@ -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("#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("#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("#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("#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("#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("#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("#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 = {
runStatusJobId: status.jobId,
runStatusLifecycle: status.completion === null ? status.state : `${status.state} / ${status.completion}`,
@@ -552,11 +632,18 @@ async function resumeBackfillRun(): Promise {
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("#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 {
});
return syncBackfillStatus("cancel");
})
- .catch(() => {
+ .catch(caughtError => {
+ const feedback = document.querySelector("#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;
diff --git a/crates/ksp-app-backfill-desk/src/app_state.rs b/crates/ksp-app-backfill-desk/src/app_state.rs
index e3cebe6..0ac403a 100644
--- a/crates/ksp-app-backfill-desk/src/app_state.rs
+++ b/crates/ksp-app-backfill-desk/src/app_state.rs
@@ -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,
diff --git a/crates/ksp-app-backfill-desk/src/dto_common.rs b/crates/ksp-app-backfill-desk/src/dto_common.rs
index 980d2b9..e3a9a90 100644
--- a/crates/ksp-app-backfill-desk/src/dto_common.rs
+++ b/crates/ksp-app-backfill-desk/src/dto_common.rs
@@ -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 {
+ 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,
/// Distinct enabled HTTP cluster labels selected by the active Transport profile.
pub(crate) configured_networks: std::vec::Vec,
/// Whether the selected Store network exactly matches the one coherent Transport network.
diff --git a/crates/ksp-app-backfill-desk/src/lib.rs b/crates/ksp-app-backfill-desk/src/lib.rs
index 768191e..f4c6782 100644
--- a/crates/ksp-app-backfill-desk/src/lib.rs
+++ b/crates/ksp-app-backfill-desk/src/lib.rs
@@ -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.
diff --git a/crates/ksp-app-backfill-desk/src/transport_runtime.rs b/crates/ksp-app-backfill-desk/src/transport_runtime.rs
index dd39c4c..2eaf0e5 100644
--- a/crates/ksp-app-backfill-desk/src/transport_runtime.rs
+++ b/crates/ksp-app-backfill-desk/src/transport_runtime.rs
@@ -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,
diff --git a/crates/ksp-app-backfill-desk/tests/desktop_contract.rs b/crates/ksp-app-backfill-desk/tests/desktop_contract.rs
index 49b0218..fff0125 100644
--- a/crates/ksp-app-backfill-desk/tests/desktop_contract.rs
+++ b/crates/ksp-app-backfill-desk/tests/desktop_contract.rs
@@ -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(""));
+ 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}");
+ }
+}
diff --git a/crates/ksp-app-backfill-desk/tests/desktop_security.rs b/crates/ksp-app-backfill-desk/tests/desktop_security.rs
index e96a3ce..76d09cc 100644
--- a/crates/ksp-app-backfill-desk/tests/desktop_security.rs
+++ b/crates/ksp-app-backfill-desk/tests/desktop_security.rs
@@ -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"));
+}
diff --git a/crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs b/crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
index b18ff41..7b5c5ed 100644
--- a/crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
+++ b/crates/ksp-app-backfill-desk/unit_tests/backfill_request.rs
@@ -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()),
diff --git a/crates/ksp-app-backfill-desk/unit_tests/dto_common.rs b/crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
index 822efcd..96b0408 100644
--- a/crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
+++ b/crates/ksp-app-backfill-desk/unit_tests/dto_common.rs
@@ -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());
+ }
+}
diff --git a/deltas/0.3.7/pre.012.md b/deltas/0.3.7/pre.012.md
new file mode 100644
index 0000000..2b90490
--- /dev/null
+++ b/deltas/0.3.7/pre.012.md
@@ -0,0 +1,130 @@
+
+
+
+# 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 `` libre et référence :
+
+```text
+