v0.3.7-pre.007
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-backfill-desk/frontend/main.html -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -47,12 +47,12 @@
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 flex-wrap mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Backfill</h1>
|
||||
<p class="text-body-secondary mb-0">Scaffold desktop prêt. Les contrôles Backfill seront branchés dans les tranches runtime suivantes.</p>
|
||||
<p class="text-body-secondary mb-0">Mainnet par défaut, transport HTTP sélectionnable et mapping de campagne validé côté Rust avant tout démarrage de job.</p>
|
||||
</div>
|
||||
<span id="appVersionBadge" class="badge text-bg-secondary">—</span>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<div class="col-xl-7">
|
||||
<div class="col-xl-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header fw-semibold">Transport HTTP</div>
|
||||
<div class="card-body">
|
||||
@@ -71,12 +71,77 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-5">
|
||||
<div class="app-placeholder h-100">
|
||||
<i class="fa-solid fa-clock-rotate-left text-body-secondary" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h2 class="h5">Campagne Backfill</h2>
|
||||
<p class="text-body-secondary mb-0">Les scopes, commitments et le bouton Start seront branchés dans la tranche suivante. Le transport reste HTTP uniquement en v0.3.7.</p>
|
||||
<div class="col-xl-8">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header fw-semibold">Campagne Backfill</div>
|
||||
<div class="card-body">
|
||||
<form id="backfillCampaignForm" novalidate>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<label for="backfillScopeKind" class="form-label">Scope</label>
|
||||
<select id="backfillScopeKind" class="form-select" disabled></select>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<label for="backfillCommitment" class="form-label">Commitment</label>
|
||||
<select id="backfillCommitment" class="form-select" disabled></select>
|
||||
</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">
|
||||
</div>
|
||||
<div id="backfillAnchorGroup" class="col-12" hidden>
|
||||
<label for="backfillAnchorSignature" class="form-label">Signature d'ancrage exclusive</label>
|
||||
<input id="backfillAnchorSignature" class="form-control font-monospace" type="text" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div id="backfillExplicitSignaturesGroup" class="col-12" hidden>
|
||||
<label for="backfillExplicitSignatures" class="form-label">Signatures explicites</label>
|
||||
<textarea id="backfillExplicitSignatures" class="form-control font-monospace" rows="5" autocomplete="off" spellcheck="false"></textarea>
|
||||
<div id="backfillExplicitSignaturesHelp" class="form-text">Une signature par ligne.</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-xxl-3">
|
||||
<label for="backfillPageSize" class="form-label">Page size</label>
|
||||
<input id="backfillPageSize" class="form-control" type="number" step="1">
|
||||
</div>
|
||||
<div class="col-md-6 col-xxl-3">
|
||||
<label for="backfillMaxPages" class="form-label">Max pages</label>
|
||||
<input id="backfillMaxPages" class="form-control" type="number" step="1">
|
||||
</div>
|
||||
<div class="col-md-6 col-xxl-3">
|
||||
<label for="backfillMaxCandidates" class="form-label">Max candidates</label>
|
||||
<input id="backfillMaxCandidates" class="form-control" type="number" step="1">
|
||||
</div>
|
||||
<div class="col-md-6 col-xxl-3">
|
||||
<label for="backfillHydrationConcurrency" class="form-label">Concurrence</label>
|
||||
<input id="backfillHydrationConcurrency" class="form-control" type="number" step="1">
|
||||
</div>
|
||||
<div id="backfillMinContextSlotGroup" class="col-lg-6">
|
||||
<label for="backfillMinContextSlot" class="form-label">Min context slot</label>
|
||||
<input id="backfillMinContextSlot" class="form-control font-monospace" type="text" inputmode="numeric" autocomplete="off" spellcheck="false" placeholder="optionnel">
|
||||
<div class="form-text">Chaîne décimale validée côté Rust pour préserver la précision u64.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3 flex-wrap mt-4">
|
||||
<button id="validateBackfillRequest" class="btn btn-primary" type="submit" disabled>
|
||||
<i class="fa-solid fa-shield-halved me-2" aria-hidden="true"></i>Valider la requête
|
||||
</button>
|
||||
<span class="text-body-secondary small">Cette tranche ne démarre aucun job.</span>
|
||||
</div>
|
||||
</form>
|
||||
<div id="backfillRequestValidation" class="alert alert-secondary mt-4 mb-0" role="status" aria-live="polite" hidden></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>
|
||||
<dl class="row mb-0 app-runtime-list">
|
||||
<dt class="col-sm-5">Réseau</dt><dd id="validatedNetwork" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Route HTTP</dt><dd id="validatedHttpRole" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Commitment</dt><dd id="validatedCommitment" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Scope</dt><dd id="validatedScopeKind" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Signatures explicites</dt><dd id="validatedExplicitSignatureCount" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">page/pages/candidates/concurrence</dt><dd id="validatedBounds" class="col-sm-7">—</dd>
|
||||
<dt class="col-sm-5">Min context slot</dt><dd id="validatedMinContextSlot" class="col-sm-7">—</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// file: crates/ksp-app-backfill-desk/frontend/ts/main.ts
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import "simplebar";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
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 { 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 { ShellStatusDto } from "./bindings/ksp_app_backfill_desk/dto_common/ShellStatusDto.ts";
|
||||
@@ -21,6 +23,19 @@ const viewTitles: Record<ViewId, string> = {
|
||||
diagnostics: "Diagnostics",
|
||||
};
|
||||
|
||||
const commitmentLabels: Record<string, string> = {
|
||||
confirmed: "confirmed",
|
||||
finalized: "finalized",
|
||||
};
|
||||
|
||||
const scopeLabels: Record<string, string> = {
|
||||
latest_address: "Adresse — historique le plus récent",
|
||||
before_address: "Adresse — avant une signature",
|
||||
after_address: "Adresse — après une signature",
|
||||
explicit_signatures: "Signatures explicites",
|
||||
};
|
||||
|
||||
|
||||
function isViewId(value: string | undefined): value is ViewId {
|
||||
return value === "backfill" || value === "diagnostics";
|
||||
}
|
||||
@@ -79,6 +94,245 @@ function bindNavigation(): void {
|
||||
frontendTrace("main", "Backfill Desk navigation handlers installed");
|
||||
}
|
||||
|
||||
function populateCodeSelect(selectId: string, values: string[], labels: Record<string, string>): void {
|
||||
const select = document.querySelector<HTMLSelectElement>(`#${selectId}`);
|
||||
if (!select) {
|
||||
return;
|
||||
}
|
||||
const previous = select.value;
|
||||
select.replaceChildren();
|
||||
for (const value of values) {
|
||||
const option = document.createElement("option");
|
||||
option.value = value;
|
||||
option.textContent = labels[value] ?? value;
|
||||
select.append(option);
|
||||
}
|
||||
select.disabled = values.length === 0;
|
||||
if (values.includes(previous)) {
|
||||
select.value = previous;
|
||||
}
|
||||
}
|
||||
|
||||
function applyNumberInputContract(inputId: string, maximum: number, defaultValue: number): void {
|
||||
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
input.min = "1";
|
||||
input.max = maximum.toString();
|
||||
if (input.value === "") {
|
||||
input.value = defaultValue.toString();
|
||||
}
|
||||
}
|
||||
|
||||
function renderCampaignContract(options: BackfillDeskOptionsDto): void {
|
||||
populateCodeSelect("backfillScopeKind", options.scopeKinds, scopeLabels);
|
||||
populateCodeSelect("backfillCommitment", options.commitments, commitmentLabels);
|
||||
applyNumberInputContract("backfillPageSize", options.limits.maxPageSize, options.limits.defaultPageSize);
|
||||
applyNumberInputContract("backfillMaxPages", options.limits.maxPages, options.limits.defaultMaxPages);
|
||||
applyNumberInputContract("backfillMaxCandidates", options.limits.maxCandidates, options.limits.defaultMaxCandidates);
|
||||
applyNumberInputContract("backfillHydrationConcurrency", options.limits.maxHydrationConcurrency, options.limits.defaultHydrationConcurrency);
|
||||
const signatureHelp = document.querySelector<HTMLElement>("#backfillExplicitSignaturesHelp");
|
||||
if (signatureHelp) {
|
||||
signatureHelp.textContent = `Une signature par ligne, ${options.limits.minSignatureTextBytes}–${options.limits.maxSignatureTextBytes} octets encodés ; maximum ${options.limits.maxCandidates}.`;
|
||||
}
|
||||
const validateButton = document.querySelector<HTMLButtonElement>("#validateBackfillRequest");
|
||||
if (validateButton) {
|
||||
validateButton.disabled = !options.compositionReady || options.httpRoutes.length === 0;
|
||||
}
|
||||
updateCampaignScopeFields();
|
||||
frontendTrace("main", "Backfill Desk campaign contract rendered", {
|
||||
commitmentCount: options.commitments.length,
|
||||
compositionReady: options.compositionReady,
|
||||
scopeCount: options.scopeKinds.length,
|
||||
});
|
||||
}
|
||||
|
||||
function currentScopeKind(): string {
|
||||
const select = document.querySelector<HTMLSelectElement>("#backfillScopeKind");
|
||||
return select?.value ?? "";
|
||||
}
|
||||
|
||||
function updateCampaignScopeFields(): void {
|
||||
const scopeKind = currentScopeKind();
|
||||
const addressGroup = document.querySelector<HTMLElement>("#backfillAddressGroup");
|
||||
const anchorGroup = document.querySelector<HTMLElement>("#backfillAnchorGroup");
|
||||
const signaturesGroup = document.querySelector<HTMLElement>("#backfillExplicitSignaturesGroup");
|
||||
const minContextSlotGroup = document.querySelector<HTMLElement>("#backfillMinContextSlotGroup");
|
||||
const addressScope = scopeKind === "latest_address" || scopeKind === "before_address" || scopeKind === "after_address";
|
||||
const anchoredScope = scopeKind === "before_address" || scopeKind === "after_address";
|
||||
const explicitScope = scopeKind === "explicit_signatures";
|
||||
if (addressGroup) {
|
||||
addressGroup.hidden = !addressScope;
|
||||
}
|
||||
if (anchorGroup) {
|
||||
anchorGroup.hidden = !anchoredScope;
|
||||
}
|
||||
if (signaturesGroup) {
|
||||
signaturesGroup.hidden = !explicitScope;
|
||||
}
|
||||
if (minContextSlotGroup) {
|
||||
minContextSlotGroup.hidden = explicitScope;
|
||||
}
|
||||
frontendTrace("main", "Backfill Desk campaign scope fields updated", { scopeKind: scopeKind || "none" });
|
||||
}
|
||||
|
||||
function invalidateCampaignValidation(source: "input" | "scope" | "route"): void {
|
||||
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
|
||||
if (status) {
|
||||
status.hidden = true;
|
||||
status.textContent = "";
|
||||
status.classList.remove("alert-success", "alert-danger");
|
||||
status.classList.add("alert-secondary");
|
||||
}
|
||||
const summary = document.querySelector<HTMLElement>("#backfillRequestPreview");
|
||||
if (summary) {
|
||||
summary.hidden = true;
|
||||
}
|
||||
frontendTrace("main", "Backfill Desk campaign validation invalidated", { source });
|
||||
}
|
||||
|
||||
function numericInputValue(inputId: string): number {
|
||||
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
|
||||
if (!input || !Number.isSafeInteger(input.valueAsNumber) || input.valueAsNumber < 0) {
|
||||
return 0;
|
||||
}
|
||||
return input.valueAsNumber;
|
||||
}
|
||||
|
||||
function rawOptionalInput(inputId: string, enabled: boolean): string | null {
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
const input = document.querySelector<HTMLInputElement>(`#${inputId}`);
|
||||
if (!input || input.value === "") {
|
||||
return null;
|
||||
}
|
||||
return input.value;
|
||||
}
|
||||
|
||||
function explicitSignatureLines(): string[] {
|
||||
const textarea = document.querySelector<HTMLTextAreaElement>("#backfillExplicitSignatures");
|
||||
if (!textarea || currentScopeKind() !== "explicit_signatures") {
|
||||
return [];
|
||||
}
|
||||
return textarea.value.split(/\r?\n/).filter(line => line.length > 0);
|
||||
}
|
||||
|
||||
function buildBackfillStartRequest(): BackfillStartRequestDto {
|
||||
const scopeKind = currentScopeKind();
|
||||
const role = document.querySelector<HTMLSelectElement>("#backfillHttpRoute")?.value ?? "";
|
||||
const commitment = document.querySelector<HTMLSelectElement>("#backfillCommitment")?.value ?? "";
|
||||
const addressScope = scopeKind === "latest_address" || scopeKind === "before_address" || scopeKind === "after_address";
|
||||
const anchoredScope = scopeKind === "before_address" || scopeKind === "after_address";
|
||||
const minContextSlotEnabled = scopeKind !== "explicit_signatures";
|
||||
return {
|
||||
address: rawOptionalInput("backfillAddress", addressScope),
|
||||
anchorSignature: rawOptionalInput("backfillAnchorSignature", anchoredScope),
|
||||
commitment,
|
||||
explicitSignatures: explicitSignatureLines(),
|
||||
hydrationConcurrency: numericInputValue("backfillHydrationConcurrency"),
|
||||
httpRole: role,
|
||||
maxCandidates: numericInputValue("backfillMaxCandidates"),
|
||||
maxPages: numericInputValue("backfillMaxPages"),
|
||||
minContextSlot: rawOptionalInput("backfillMinContextSlot", minContextSlotEnabled),
|
||||
pageSize: numericInputValue("backfillPageSize"),
|
||||
scopeKind,
|
||||
};
|
||||
}
|
||||
|
||||
function renderRequestPreview(preview: BackfillRequestPreviewDto): void {
|
||||
const values: Record<string, string> = {
|
||||
validatedNetwork: preview.network,
|
||||
validatedHttpRole: preview.httpRole,
|
||||
validatedCommitment: preview.commitment,
|
||||
validatedScopeKind: preview.scopeKind,
|
||||
validatedExplicitSignatureCount: preview.explicitSignatureCount.toString(),
|
||||
validatedBounds: `${preview.pageSize} / ${preview.maxPages} / ${preview.maxCandidates} / ${preview.hydrationConcurrency}`,
|
||||
validatedMinContextSlot: preview.minContextSlotPresent ? "présent" : "absent",
|
||||
};
|
||||
for (const [id, value] of Object.entries(values)) {
|
||||
const element = document.querySelector<HTMLElement>(`#${id}`);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
}
|
||||
const summary = document.querySelector<HTMLElement>("#backfillRequestPreview");
|
||||
if (summary) {
|
||||
summary.hidden = false;
|
||||
}
|
||||
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
|
||||
if (status) {
|
||||
status.hidden = false;
|
||||
status.classList.remove("alert-secondary", "alert-danger");
|
||||
status.classList.add("alert-success");
|
||||
status.textContent = "Requête valide. Aucun job n'a été démarré.";
|
||||
}
|
||||
frontendTrace("main", "Backfill Desk validated request preview rendered", {
|
||||
commitment: preview.commitment,
|
||||
httpRole: preview.httpRole,
|
||||
scopeKind: preview.scopeKind,
|
||||
});
|
||||
}
|
||||
|
||||
function renderRequestValidationError(caughtError: unknown): void {
|
||||
const status = document.querySelector<HTMLElement>("#backfillRequestValidation");
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
status.hidden = false;
|
||||
status.classList.remove("alert-secondary", "alert-success");
|
||||
status.classList.add("alert-danger");
|
||||
status.textContent = message;
|
||||
}
|
||||
|
||||
async function validateBackfillRequest(): Promise<void> {
|
||||
const request = buildBackfillStartRequest();
|
||||
frontendDebug("main", "Backfill Desk campaign validation requested", {
|
||||
commitment: request.commitment,
|
||||
httpRole: request.httpRole,
|
||||
scopeKind: request.scopeKind,
|
||||
});
|
||||
try {
|
||||
const preview = await invokeKsp<BackfillRequestPreviewDto>("main", "backfill_validate_request", { request });
|
||||
renderRequestPreview(preview);
|
||||
frontendDebug("main", "Backfill Desk campaign validation completed", {
|
||||
commitment: preview.commitment,
|
||||
httpRole: preview.httpRole,
|
||||
scopeKind: preview.scopeKind,
|
||||
});
|
||||
} catch (caughtError) {
|
||||
renderRequestValidationError(caughtError);
|
||||
frontendWarn("main", "Backfill Desk campaign validation failed");
|
||||
}
|
||||
}
|
||||
|
||||
function bindCampaignForm(): void {
|
||||
const scope = document.querySelector<HTMLSelectElement>("#backfillScopeKind");
|
||||
if (scope) {
|
||||
scope.addEventListener("change", () => {
|
||||
frontendDebug("main", "Backfill Desk campaign scope changed", { scopeKind: scope.value || "none" });
|
||||
updateCampaignScopeFields();
|
||||
invalidateCampaignValidation("scope");
|
||||
});
|
||||
}
|
||||
const form = document.querySelector<HTMLFormElement>("#backfillCampaignForm");
|
||||
if (form) {
|
||||
form.addEventListener("input", () => invalidateCampaignValidation("input"));
|
||||
form.addEventListener("submit", event => {
|
||||
event.preventDefault();
|
||||
void validateBackfillRequest();
|
||||
});
|
||||
}
|
||||
frontendTrace("main", "Backfill Desk campaign form handlers installed");
|
||||
}
|
||||
|
||||
function routeLabel(route: BackfillHttpRouteOptionDto): string {
|
||||
if (route.pooled) {
|
||||
@@ -88,6 +342,7 @@ function routeLabel(route: BackfillHttpRouteOptionDto): string {
|
||||
}
|
||||
|
||||
function renderBackfillOptions(options: BackfillDeskOptionsDto): void {
|
||||
renderCampaignContract(options);
|
||||
const select = document.querySelector<HTMLSelectElement>("#backfillHttpRoute");
|
||||
if (select) {
|
||||
const previous = select.value;
|
||||
@@ -143,6 +398,7 @@ function bindBackfillRouteSelection(): void {
|
||||
}
|
||||
select.addEventListener("change", () => {
|
||||
frontendDebug("main", "Backfill Desk HTTP route selection changed", { role: select.value || "none" });
|
||||
invalidateCampaignValidation("route");
|
||||
});
|
||||
frontendTrace("main", "Backfill Desk HTTP route selection handler installed");
|
||||
}
|
||||
@@ -202,6 +458,7 @@ async function initializeMain(): Promise<void> {
|
||||
bindNavigation();
|
||||
bindRuntimeStatusRefresh();
|
||||
bindBackfillRouteSelection();
|
||||
bindCampaignForm();
|
||||
activateView("backfill", "startup");
|
||||
try {
|
||||
await Promise.all([loadRuntimeStatus("startup"), loadBackfillOptions("startup")]);
|
||||
|
||||
Reference in New Issue
Block a user