diff --git a/AppImage/components/pbs-panel.tsx b/AppImage/components/pbs-panel.tsx
index 5dff026e..b18a3e12 100644
--- a/AppImage/components/pbs-panel.tsx
+++ b/AppImage/components/pbs-panel.tsx
@@ -208,11 +208,16 @@ export function PBSPanel({ localOnly = false, view = "all" }: { localOnly?: bool
{(["name", "url", ...(method === "password" && !form.id ? ["username", "password"] : ["token_id", "token_secret"]), "fingerprint"] as const).map(field => {
const secret = field === "token_secret" || field === "password"
- return
-
+ const help = field === "url" ? "pbs.urlHelp" : field === "username" ? "pbs.usernameHelp"
+ : field === "token_id" ? "pbs.tokenIdHelp" : field === "token_secret"
+ ? form.id ? "pbs.tokenSecretKeepHelp" : "pbs.tokenSecretHelp" : undefined
+ return
+
setForm({ ...form, [field]: event.target.value })} />
+ {help &&
{t(help)}
}
})}
@@ -324,7 +329,7 @@ function PBSResultBadge({ status, running = false, tone }: { status?: string; ru
const badge =
{running ? t("pbs.running") : pbsResultText(status, t)}
- if (running || !status || /^ok$/i.test(status)) return badge
+ if (running || !status || status === "unknown" || /^ok$/i.test(status) || /^WARNINGS:\s*\d+\s*$/i.test(status)) return badge
return
{badge}
{status}
diff --git a/AppImage/components/pbs-problem-message.tsx b/AppImage/components/pbs-problem-message.tsx
index 459701c4..f8286da9 100644
--- a/AppImage/components/pbs-problem-message.tsx
+++ b/AppImage/components/pbs-problem-message.tsx
@@ -1,16 +1,17 @@
"use client"
-import { pbsErrorText } from "../lib/pbs-messages"
+import { pbsErrorText, pbsIsKnownError } from "../lib/pbs-messages"
import { useT } from "../lib/i18n/provider"
-// Keep PBS's original diagnostic available without making it the UI message.
+// Known application messages are already explained in the active language.
+// Preserve unrecognized external diagnostics as escaped technical details.
export function PBSProblemMessage({ problem, raw, fallback, className = "" }: {
problem?: unknown; raw?: string; fallback?: string; className?: string
}) {
const t = useT()
return
{pbsErrorText(problem, t, fallback)}
- {raw &&
+ {raw && !pbsIsKnownError(problem) &&
{t("pbs.technicalDetails")}
{raw}
}
diff --git a/AppImage/lib/pbs-messages.ts b/AppImage/lib/pbs-messages.ts
index 6ce6faa3..6314d317 100644
--- a/AppImage/lib/pbs-messages.ts
+++ b/AppImage/lib/pbs-messages.ts
@@ -20,18 +20,26 @@ const ERROR_KEYS: Record = {
pbs_datastore_offline: "pbs.errors.datastoreOffline", pbs_datastore_unavailable: "pbs.errors.datastoreUnavailable",
}
-// Accept structured codes from fetchApi, partial status or a datastore. Legacy
-// English errors remain diagnostics; never interpret arbitrary text as a key.
-export function pbsErrorText(error: unknown, t: Translate, fallback = "pbs.loadFailed"): string {
+// Accept structured codes, never arbitrary error text as a translation key.
+function pbsErrorTranslation(error: unknown): { key: string; params?: { count: number } } | null {
const body = error && typeof error === "object" && "body" in error ? error.body : error
- if (!body || typeof body !== "object" || !("code" in body) || typeof body.code !== "string") return t(fallback)
+ if (!body || typeof body !== "object" || !("code" in body) || typeof body.code !== "string") return null
if (body.code === "pbs_connection_limit") {
const params = "params" in body ? body.params : null
const count = params && typeof params === "object" && "count" in params ? params.count : null
return typeof count === "number" && Number.isSafeInteger(count) && count > 0
- ? t("pbs.errors.connectionLimit", { count }) : t(fallback)
+ ? { key: "pbs.errors.connectionLimit", params: { count } } : null
}
- return Object.prototype.hasOwnProperty.call(ERROR_KEYS, body.code) ? t(ERROR_KEYS[body.code]) : t(fallback)
+ return Object.prototype.hasOwnProperty.call(ERROR_KEYS, body.code) ? { key: ERROR_KEYS[body.code] } : null
+}
+
+export function pbsIsKnownError(error: unknown): boolean {
+ return pbsErrorTranslation(error) !== null
+}
+
+export function pbsErrorText(error: unknown, t: Translate, fallback = "pbs.loadFailed"): string {
+ const translated = pbsErrorTranslation(error)
+ return translated ? t(translated.key, translated.params) : t(fallback)
}
export function pbsDiagnostic(error: unknown): string | undefined {
diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json
index f4a3b71c..7bc7b31c 100644
--- a/AppImage/messages/en/common.json
+++ b/AppImage/messages/en/common.json
@@ -14,7 +14,7 @@
"signInWith": "Sign in with",
"methodPassword": "User and password",
"methodToken": "API token",
- "username": "User (user@realm)",
+ "username": "User",
"password": "Password",
"passwordHelp": "The password is used once to create a read-only API token (Audit role) for this Monitor. Only that token is saved, never the password. Enable Monitor authentication first.",
"cancel": "Cancel",
@@ -27,10 +27,15 @@
"editConnection": "Connection to edit",
"add": "Add a new connection",
"name": "Name",
- "url": "HTTPS URL (include :8007 for the native PBS port)",
- "token_id": "API token ID (user@realm!token)",
- "token_secret": "API token secret (leave empty to keep the stored secret)",
- "fingerprint": "SHA256 certificate fingerprint (optional)",
+ "url": "HTTPS address",
+ "urlHelp": "Include :8007 for the default PBS port, for example https://pbs.example:8007.",
+ "usernameHelp": "Use user@realm, for example root@pam.",
+ "tokenIdHelp": "Use user@realm!token, for example monitor@pbs!proxmenux.",
+ "tokenSecretHelp": "Enter the secret of this API token.",
+ "tokenSecretKeepHelp": "Leave empty to keep the saved token secret.",
+ "token_id": "API token ID",
+ "token_secret": "API token secret",
+ "fingerprint": "SHA256 fingerprint (optional)",
"tlsHelp": "Leave the fingerprint empty for normal CA and hostname verification. For a self-signed certificate, copy its SHA256 fingerprint from the PBS console or another trusted channel. The pin is checked before sending credentials.",
"saved": "PBS connection verified and saved.",
"saveFailed": "Could not save the PBS connection.",
diff --git a/AppImage/messages/sk/common.json b/AppImage/messages/sk/common.json
index 801b456e..418bf19b 100644
--- a/AppImage/messages/sk/common.json
+++ b/AppImage/messages/sk/common.json
@@ -7310,7 +7310,7 @@
"signInWith": "Prihlásiť sa pomocou",
"methodPassword": "Používateľ a heslo",
"methodToken": "API token",
- "username": "Používateľ (user@realm)",
+ "username": "Používateľ",
"password": "Heslo",
"passwordHelp": "Heslo sa použije iba raz na vytvorenie API tokenu iba na čítanie s rolou Audit pre tento Monitor. Uloží sa iba token, heslo sa neukladá. Najprv zapni overovanie v Monitore.",
"cancel": "Zrušiť",
@@ -7323,10 +7323,15 @@
"editConnection": "Pripojenie na úpravu",
"add": "Pridať nové pripojenie",
"name": "Názov",
- "url": "Adresa HTTPS (pre štandardný port PBS uveď :8007)",
- "token_id": "ID API tokenu (user@realm!token)",
- "token_secret": "Tajný kľúč API tokenu (prázdne pole zachová uložený kľúč)",
- "fingerprint": "Odtlačok SHA256 certifikátu (voliteľný)",
+ "url": "Adresa HTTPS",
+ "urlHelp": "Štandardný port PBS je 8007, napríklad https://pbs.example:8007.",
+ "usernameHelp": "Použi meno a overovaciu oblasť oddelené znakom @, napríklad root@pam.",
+ "tokenIdHelp": "Použi formát meno@oblasť!token, napríklad monitor@pbs!proxmenux.",
+ "tokenSecretHelp": "Zadaj tajný kľúč k tomuto API tokenu.",
+ "tokenSecretKeepHelp": "Prázdne pole zachová uložený tajný kľúč tokenu.",
+ "token_id": "ID API tokenu",
+ "token_secret": "Tajný kľúč API tokenu",
+ "fingerprint": "Odtlačok SHA256 (voliteľný)",
"tlsHelp": "Pri bežnom overovaní certifikačnej autority a názvu hostiteľa nechaj odtlačok prázdny. Pri certifikáte s vlastným podpisom skopíruj odtlačok SHA256 z konzoly PBS alebo iného dôveryhodného zdroja. Kontroluje sa ešte pred odoslaním prihlasovacích údajov.",
"saved": "Pripojenie PBS bolo overené a uložené.",
"saveFailed": "Nepodarilo sa uložiť pripojenie PBS.",
diff --git a/tests/test_pbs_i18n.cjs b/tests/test_pbs_i18n.cjs
index 1712b9d8..93d92605 100644
--- a/tests/test_pbs_i18n.cjs
+++ b/tests/test_pbs_i18n.cjs
@@ -97,11 +97,28 @@ async function main() {
assert.equal(messages.pbsTaskType('garbage_collection',t),t('pbs.taskTypes.garbageCollection'));
assert.equal(messages.pbsTaskType('future',t),t('pbs.taskTypes.other',{type:'future'}));
assert.equal(messages.pbsServiceState('constructor',t),t('pbs.serviceStates.unknown'));
+ assert.equal(t('pbs.url'),locale==='sk'?'Adresa HTTPS':'HTTPS address');
+ const diagnosticFixture=fixture(locale),problem=diagnosticFixture.load('AppImage/components/pbs-problem-message.tsx').PBSProblemMessage;
+ for(const code of codes){
+ if(code==='pbs_unknown')continue;
+ const err=failure(code);err.body.params={count:32};
+ const rendered=problem({problem:err,raw:'RAW DIAGNOSTIC'});
+ assert.ok(!text(rendered).includes('RAW DIAGNOSTIC'),'Known '+code+' must not repeat an English application message');
+ assert.ok(!flatten(rendered).some(n=>n.type==='details'));
+ }
+ for(const code of ['future','__proto__','constructor']){
+ const rendered=problem({problem:failure(code),raw:'RAW
DIAGNOSTIC'});
+ assert.ok(text(rendered).includes('RAW
DIAGNOSTIC'));assert.ok(flatten(rendered).some(n=>n.type==='details'));
+ }
const unknown=fixture(locale);unknown.status.tasks=[{upid:'unknown',endtime:1700000000,status:'unknown'}];unknown.status.sync=[{id:'unknown','last-run-state':'unknown'}];
unknown.render();unknown.render();
const unknownBadges=unknown.nodes().filter(n=>n.type==='Badge'&&text(n)===t('pbs.states.unknown'));
assert.equal(unknownBadges.length,2);assert.ok(unknownBadges.every(n=>!n.props.className.includes('red-')));
+ unknown.status.tasks=[{upid:'warn',endtime:1700000000,status:'WARNINGS: 3'},{upid:'raw',endtime:1700000000,status:'RAW EXTERNAL TASK ERROR'}];unknown.status.sync=[];unknown.render();
+ const taskDetails=unknown.nodes().filter(n=>n.type==='details');assert.equal(taskDetails.length,1);assert.ok(text(taskDetails[0]).includes('RAW EXTERNAL TASK ERROR'));
const f=fixture(locale);f.render();f.render();f.button(t('pbs.manage')).props.onClick();f.render();
+ assert.equal(f.input('url').props['aria-describedby'],'pbs-url-help');
+ assert.ok(f.text().includes(t('pbs.urlHelp')));assert.ok(f.text().includes(t('pbs.usernameHelp')));
f.input('name').props.onChange({target:{value:'Draft'}});f.render();
f.input('password').props.onChange({target:{value:'fake-one-time-password'}});f.render();
f.button(t('pbs.cancel')).props.onClick();f.render();assert.equal(f.calls.length,0);
@@ -111,8 +128,11 @@ async function main() {
assert.ok(f.text().includes(t('pbs.errors.invalidUrl')));assert.equal(f.input('url').props.value,'http://bad.invalid');
assert.equal(f.calls[0].path,'/api/pbs/servers/login');assert.ok(!Object.hasOwn(JSON.parse(f.calls[0].body),'token_secret'));
f.button(t('pbs.methodToken')).props.onClick();f.render();await f.button(t('pbs.save')).props.onClick();f.render();
+ assert.ok(f.text().includes(t('pbs.tokenSecretHelp')));assert.ok(!f.text().includes(t('pbs.tokenSecretKeepHelp')));
+ assert.equal(f.input('token_secret').props['aria-describedby'],'pbs-token_secret-help');
assert.equal(f.calls[1].path,'/api/pbs/servers');assert.ok(!Object.hasOwn(JSON.parse(f.calls[1].body),'password'));
f.select(t('pbs.editConnection')).props.onValueChange('saved');f.render();await f.button(t('pbs.remove')).props.onClick();f.render();
+ assert.ok(f.text().includes(t('pbs.tokenSecretKeepHelp')));assert.ok(!f.text().includes(t('pbs.tokenSecretHelp')));
assert.deepEqual(f.confirmations,[t('pbs.removeConfirm')]);assert.equal(f.calls.length,2,'Declined remove has no DELETE');
const remove=fixture(locale,{confirm:true});remove.render();remove.render();remove.button(t('pbs.manage')).props.onClick();remove.render();remove.select(t('pbs.editConnection')).props.onValueChange('saved');remove.render();
await remove.button(t('pbs.remove')).props.onClick();remove.render();assert.equal(remove.calls.length,1);assert.equal(remove.calls[0].method,'DELETE');