fix(downloads): compact finalization statuses

Normalize German and English finalization producer labels before they reach package and item status cells. Derive percentages from valid fractions, clamp them to the visible 0-100 range, and reduce malformed fractions or zero totals to the phase name.

Preserve legacy percentage labels while preventing finalization lines ending in archive names from being classified as bare extraction archives. Cover current and legacy forms, both languages, package and item rendering, localization, and invalid numeric input.
This commit is contained in:
Sucukdeluxe
2026-08-12 00:09:18 +02:00
parent 15f53f9c92
commit 1664478770
4 changed files with 75 additions and 2 deletions
+4
View File
@@ -254,6 +254,8 @@ function translateDynamic(value: string, language: AppLanguage): string {
if (result) return `${result[1]} completed${result[2].replace(/(\d+) Fehler/g, "$1 errors").replace(/(\d+) abgebrochen/g, "$1 cancelled")}`;
const extracting = value.match(/^Entpacken (\d+%)$/);
if (extracting) return `Extracting ${extracting[1]}`;
const finalizing = value.match(/^Finalisieren - (\d+%)$/);
if (finalizing) return `Finalizing - ${finalizing[1]}`;
const checkedUntil = value.match(/^Account geprüft — (.+) bis (.+)$/);
if (checkedUntil) return `Account checked — ${checkedUntil[1]} until ${checkedUntil[2]}`;
const checked = value.match(/^Account geprüft — (.+)$/);
@@ -403,6 +405,8 @@ function translateDynamic(value: string, language: AppLanguage): string {
if (result) return `${result[1]} fertig${result[2].replace(/(\d+) errors/g, "$1 Fehler").replace(/(\d+) cancelled/g, "$1 abgebrochen")}`;
const extracting = value.match(/^Extracting (\d+%)$/);
if (extracting) return `Entpacken ${extracting[1]}`;
const finalizing = value.match(/^Finalizing - (\d+%)$/);
if (finalizing) return `Finalisieren - ${finalizing[1]}`;
const checkedUntil = value.match(/^Account checked — (.+) until (.+)$/);
if (checkedUntil) return `Account geprüft — ${checkedUntil[1]} bis ${checkedUntil[2]}`;
const credentialsFor = value.match(/^Credentials for (.+)$/);
@@ -153,6 +153,20 @@ export function compactDownloadStatus(value: string): string {
if (extracting) return `Entpacken - ${extracting[1]}%`;
const extractingEnglish = status.match(/Extracting\s+(\d+)%/i);
if (extractingEnglish) return `Extracting - ${extractingEnglish[1]}%`;
const finalizing = status.match(/^(Finalisieren|Finalizing)\b/i);
if (finalizing) {
const fraction = status.match(/\(([^)]*)\)/);
if (fraction) {
const [currentValue, totalValue] = fraction[1].split("/");
const current = Number(currentValue?.trim());
const total = Number(totalValue?.trim());
if (Number.isFinite(current) && Number.isFinite(total) && total > 0) return `${finalizing[1]} - ${progress((current / total) * 100)}%`;
return finalizing[1];
}
const percentage = status.match(/-\s*(-?\d+(?:\.\d+)?)%/);
if (percentage) return `${finalizing[1]} - ${progress(Number(percentage[1]))}%`;
return finalizing[1];
}
return status;
}
@@ -447,9 +461,10 @@ function packageCell(row: DownloadPackageRow, column: string, packageSpeedBps: n
if (column === "status") {
const audio = entry.audioStripSummary ? formatAudioStripSummary(entry.audioStripSummary) : null;
const rawPostProcessLabel = entry.postProcessLabel?.trim() || "";
const postProcessLabel = entry.status === "extracting" && /(?:^|[\\/])[^\\/]+\.(?:rar|zip|7z|tar|gz|bz2|xz)(?:\.\d+)?$/i.test(rawPostProcessLabel)
const compactPostProcessLabel = compactDownloadStatus(rawPostProcessLabel);
const postProcessLabel = entry.status === "extracting" && compactPostProcessLabel === rawPostProcessLabel && /(?:^|[\\/])[^\\/]+\.(?:rar|zip|7z|tar|gz|bz2|xz)(?:\.\d+)?$/i.test(rawPostProcessLabel)
? "Entpacken - Ausstehend"
: compactDownloadStatus(rawPostProcessLabel);
: compactPostProcessLabel;
const extractFailure = row.allItems.find((item) => /^Entpack-Fehler\b/i.test(item.fullStatus || ""));
const waitsForDisk = row.allItems.some((item) => compactDownloadStatus(item.fullStatus || "") === "Warte auf Festplatte");
const details = `${stats.done}/${stats.total}${stats.failed > 0 ? ` · ${stats.failed} Fehler` : ""}${stats.cancelled > 0 ? ` · ${stats.cancelled} abgebrochen` : ""}${postProcessLabel ? ` · ${postProcessLabel}` : ""}${extractFailure ? " · Entpack-Fehler" : ""}${audio ? ` · ${audio.text}` : ""}`;
+45
View File
@@ -923,6 +923,51 @@ describe("download table row contracts", () => {
expect(html).not.toContain(">release.part1.rar</span>");
});
it.each([
["package", "Finalisieren (1/2) · release.part1.rar", "Finalisieren - 50%"],
["package", "Finalizing - 50% * release.part1.rar", "Finalizing - 50%"],
["item", "Finalizing (3/4) · release.part1.rar", "Finalizing - 75%"],
["item", "Finalisieren - 50% * release.part1.rar", "Finalisieren - 50%"]
])("shows %s finalization status without archive details", (target, rawStatus, expectedStatus) => {
const html = target === "package"
? renderToStaticMarkup(PackageCardContent({
actions: createActions(),
columnOrder: ["status"],
editing: false,
editingName: "",
gridTemplate: "220px",
packageSpeedBps: 0,
row: {
package: { ...pkg("finalization-package", "Finalization", ["finalization-item"]), postProcessLabel: rawStatus, status: "extracting" } as PackageEntry,
items: [item("finalization-item", "finalization-package", "completed")],
allItems: [item("finalization-item", "finalization-package", "completed")],
collapsed: true
},
selectedIds: new Set<string>(),
selectedVersion: 0
}))
: renderToStaticMarkup(ItemRowContent({
actions: createActions(),
columnOrder: ["status"],
gridTemplate: "220px",
item: item("finalization-item", "finalization-package", "completed", { fullStatus: rawStatus }),
selected: false
}));
expect(html).toContain(`aria-label="${expectedStatus}"`);
expect(html.match(new RegExp(`>${expectedStatus}</span>`, "g"))).toHaveLength(2);
expect(html).not.toContain(">release.part1.rar</span>");
});
it.each([
["Finalisieren (3/2) · release.part1.rar", "Finalisieren - 100%"],
["Finalizing (-1/2) · release.part1.rar", "Finalizing - 0%"],
["Finalisieren (1/0) · release.part1.rar", "Finalisieren"],
["Finalizing (invalid/2) · release.part1.rar", "Finalizing"]
])("derives only finite finalization percentages", (rawStatus, expectedStatus) => {
expect(compactDownloadStatus(rawStatus)).toBe(expectedStatus);
});
it("renders meter text in clipped track and fill layers", () => {
const html = renderToStaticMarkup(ItemRowContent({
actions: createActions(),
+9
View File
@@ -22,6 +22,15 @@ describe("renderer localization", () => {
expect(translateUiText("100.001100.005 of 100.005", "de")).toBe("100.001100.005 von 100.005");
});
it.each([
["Finalisieren - 0%", "Finalizing - 0%"],
["Finalisieren - 50%", "Finalizing - 50%"],
["Finalisieren - 100%", "Finalizing - 100%"]
])("translates compact finalization progress %s in both directions", (german, english) => {
expect(translateUiText(german, "en")).toBe(english);
expect(translateUiText(english, "de")).toBe(german);
});
it("translates the complete history surface including status values", () => {
const translations = new Map([
["Alle Einträge", "All entries"],