diff --git a/src/components/onboarding/provisioning-status.tsx b/src/components/onboarding/provisioning-status.tsx
index 6dd877c..8f959b7 100644
--- a/src/components/onboarding/provisioning-status.tsx
+++ b/src/components/onboarding/provisioning-status.tsx
@@ -432,25 +432,35 @@ export function ProvisioningStatus({ requestId, canAct }: Props) {
{t("phase")}
- {conditions.map((c, i) => (
-
- {c.type}
-
- {c.reason || c.status}
-
-
- ))}
+ {/* Setup progress. The operator reports readiness as a list of
+ internal K8s conditions (OpenBao policy, LiteLLM key, network
+ policy, …) — meaningful to operators, jargon to customers.
+ We surface the *shape* of that progress (how many steps are
+ done) without leaking the internal names. */}
+ {conditions.length > 0 &&
+ (() => {
+ const done = conditions.filter((c) => c.status === "True").length;
+ const total = conditions.length;
+ const pct = Math.round((done / total) * 100);
+ return (
+
+
+
+ {t("setupProgress")}
+
+
+ {t("setupStepsComplete", { done, total })}
+
+
+
+
+ );
+ })()}
);
diff --git a/src/components/onboarding/wizard.tsx b/src/components/onboarding/wizard.tsx
index 3703733..abfcf18 100644
--- a/src/components/onboarding/wizard.tsx
+++ b/src/components/onboarding/wizard.tsx
@@ -420,18 +420,51 @@ export function OnboardingWizard({
[]
);
- // Validate that all secret-requiring enabled packages have complete credentials
- const packageCredentialsValid = (): boolean => {
+ // Enabled packages that still need something from the user before the
+ // configure step can advance — a missing credential field or an
+ // unaccepted disclaimer. Returns the package defs so the UI can name
+ // exactly what's blocking the (otherwise silently disabled) Next
+ // button instead of greying it out with no explanation.
+ const incompletePackages = (): PackageDef[] => {
+ const out: PackageDef[] = [];
for (const pkgId of config.packages) {
const def = PACKAGE_CATALOG.find((p) => p.id === pkgId);
- if (!def?.requiresSecrets) continue;
- const secrets = packageSecrets[pkgId] || {};
- for (const field of def.secrets || []) {
- if (!secrets[field.key]?.trim()) return false;
+ if (!def) continue;
+ let incomplete = false;
+ if (def.requiresSecrets) {
+ const secrets = packageSecrets[pkgId] || {};
+ for (const field of def.secrets || []) {
+ if (!secrets[field.key]?.trim()) {
+ incomplete = true;
+ break;
+ }
+ }
}
- if (def.disclaimerKey && !disclaimerAccepted[pkgId]) return false;
+ if (def.disclaimerKey && !disclaimerAccepted[pkgId]) incomplete = true;
+ if (incomplete) out.push(def);
}
- return true;
+ return out;
+ };
+
+ const packageCredentialsValid = (): boolean =>
+ incompletePackages().length === 0;
+
+ // Map zod field paths to human labels for the confirm-step error
+ // summary, so a stray validation failure reads "Postal code" rather
+ // than "billingAddress.postalCode". Unknown paths fall back to the
+ // raw path (this defence-in-depth list should rarely render at all).
+ const fieldLabel = (path: string): string => {
+ const map: Record = {
+ instanceName: t("instanceName"),
+ agentName: t("agentName"),
+ "billingAddress.company": t("billingCompany"),
+ "billingAddress.street": t("billingStreet"),
+ "billingAddress.postalCode": t("billingPostalCode"),
+ "billingAddress.city": t("billingCity"),
+ "billingAddress.country": t("billingCountry"),
+ "billingAddress.vatNumber": t("billingVatNumber"),
+ };
+ return map[path] ?? path;
};
const handleSubmit = async () => {
@@ -984,20 +1017,33 @@ export function OnboardingWizard({
-
-
-
+
+ {(() => {
+ const blocking = incompletePackages();
+ if (blocking.length === 0) return null;
+ return (
+
+ {t("packagesIncompleteHint", {
+ packages: blocking.map((p) => p.name).join(", "),
+ })}
+
+ );
+ })()}
+
+
+
+
)}
@@ -1380,7 +1426,8 @@ export function OnboardingWizard({
{Object.entries(errors).map(([path, msg]) => (
-
- {path}: {msg}
+ {fieldLabel(path)}:{" "}
+ {msg}
))}