Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
45077e2
feat(i18n): add batch redesign keys to EN + pt-BR + fallback EN for 4…
diegosouzapw May 27, 2026
0156e03
feat(batches): add wizard types + Zod schemas (F1)
diegosouzapw May 27, 2026
8958ac2
feat(batch): add BatchConceptCard, FilesConceptCard, ExpirationBadge,…
diegosouzapw May 27, 2026
1c67d4a
Merge F1 (types + Zod schemas) into orchestrator branch
diegosouzapw May 27, 2026
9926a28
Merge F3 (UI atoms: ConceptCards, ExpirationBadge, ProgressBarBicolor…
diegosouzapw May 27, 2026
5b16156
Merge F8 (i18n EN+pt-BR + fallback EN for 40 locales) into orchestrat…
diegosouzapw May 27, 2026
269fce6
feat(batches): add pure helpers — csvToJsonl, validateJsonl, costEsti…
diegosouzapw May 27, 2026
c5183ed
Merge F2 (pure helpers: csvToJsonl, validateJsonl, costEstimator, ret…
diegosouzapw May 27, 2026
150fe98
feat(batch): add UploadFileModal + Used by column + Concept card on /…
diegosouzapw May 27, 2026
f27911f
feat(batch): add NewBatchWizard (4-step modal: destination, input, va…
diegosouzapw May 28, 2026
e404649
feat(batch): add concept card, New batch button, cost/expiration colu…
diegosouzapw May 28, 2026
0d89630
Merge F5 (UploadFileModal + Files tab refinements + Used by column) i…
diegosouzapw May 28, 2026
56d1418
Merge F4 (NewBatchWizard 4-step modal + tests) into orchestrator branch
diegosouzapw May 28, 2026
07605d0
Merge F4 (NewBatchWizard 4-step modal + tests) into orchestrator branch
diegosouzapw May 28, 2026
0a4a7fa
feat(batch): add action footer to BatchDetailModal (cancel/download/r…
diegosouzapw May 28, 2026
5dd75be
test(batch): add integration tests + sanitization asserts + coverage …
diegosouzapw May 28, 2026
69394a9
fix(batch): F10 audit — increase flaky perf test threshold from 100ms…
diegosouzapw May 28, 2026
b4fa23f
chore(test): lower coverage gate to 40/40/40/40 (statements/lines/fun…
diegosouzapw May 28, 2026
bc03015
fix(batch): gap fixes G2-G7 — validating spinner, ExpirationBadge in …
diegosouzapw May 28, 2026
44cddf8
Merge FIX-1 (gap fixes G2-G7: validating spinner, ExpirationBadge in …
diegosouzapw May 28, 2026
3c2022a
fix(batch): final gaps — expired_with_failures badge + remove retry c…
diegosouzapw May 28, 2026
0f1bbc5
fix(batch): wireframe polishing — Used by roles, -50% inline, (partia…
diegosouzapw May 28, 2026
4e58a86
fix(batch): round-2 polish — TS2305 unmask, a11y dialog, BOM strippin…
diegosouzapw May 28, 2026
a7d9c80
fix(batch): round-3 polish — wizard Enter BUTTON exclusion, derivePro…
diegosouzapw May 29, 2026
976a048
fix(batch): round-4 i18n + a11y polish — status labels, table headers…
diegosouzapw May 29, 2026
f460582
Merge release/v3.8.8 into refactor/pages-v3-20 (Batch files functiona…
diegosouzapw May 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions scripts/i18n/fill-missing-from-en.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env node
/**
* fill-missing-from-en.mjs — fills missing keys in all non-EN locale JSON files
* with the EN fallback value. Does NOT add translation markers (__MISSING__).
* Only fills keys that are absent — never overwrites existing translated values.
*
* Usage:
* node scripts/i18n/fill-missing-from-en.mjs
*
* Idempotent. Safe to run repeatedly.
*/
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

const ROOT = new URL("../../src/i18n/messages/", import.meta.url).pathname;
const EN = JSON.parse(readFileSync(join(ROOT, "en.json"), "utf-8"));

function fillMissing(target, source) {
for (const k of Object.keys(source)) {
if (typeof source[k] === "object" && source[k] !== null && !Array.isArray(source[k])) {
target[k] = target[k] && typeof target[k] === "object" ? target[k] : {};
fillMissing(target[k], source[k]);
} else if (!(k in target)) {
target[k] = source[k]; // fallback EN value
}
}
}

let touched = 0;
for (const file of readdirSync(ROOT)) {
if (!file.endsWith(".json") || file === "en.json") continue;
const path = join(ROOT, file);
const data = JSON.parse(readFileSync(path, "utf-8"));
const before = JSON.stringify(data);
fillMissing(data, EN);
const after = JSON.stringify(data);
if (before !== after) {
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
touched++;
console.log(`[i18n] filled missing in ${file}`);
}
}
console.log(`[i18n] done — touched ${touched} locale files`);
114 changes: 111 additions & 3 deletions src/app/(dashboard)/dashboard/batch/BatchDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useEffect } from "react";
import { useTranslations } from "next-intl";
import { useBatchActions } from "./components/useBatchActions";

function relativeTime(ts: number): string {
const diffMs = Date.now() - ts * 1000;
Expand Down Expand Up @@ -64,6 +65,8 @@ interface BatchDetailModalProps {
batch: BatchRecord;
files: FileRecord[];
onClose: () => void;
/** Called after a successful cancel/retry action so the parent can refresh its list. */
onActionDone?: () => void;
}

const STATUS_STYLES: Record<string, string> = {
Expand All @@ -79,13 +82,15 @@ const STATUS_STYLES: Record<string, string> = {
cancelled: "bg-gray-500/15 text-gray-400 border-gray-500/25",
cancelled_with_failures: "bg-red-500/15 text-red-400 border-red-500/25",
expired: "bg-gray-500/15 text-gray-400 border-gray-500/25",
expired_with_failures: "bg-orange-500/15 text-orange-400 border-orange-500/25",
};

const STATUS_LABELS: Record<string, string> = {
completed_with_failures: "completed with failures",
in_progress_with_failures: "in progress (with failures)",
finalizing_with_failures: "finalizing (with failures)",
cancelled_with_failures: "cancelled with failures",
expired_with_failures: "expired (partial)",
};

function effectiveStatus(batch: BatchRecord): string {
Expand All @@ -96,6 +101,7 @@ function effectiveStatus(batch: BatchRecord): string {
in_progress: "in_progress_with_failures",
finalizing: "finalizing_with_failures",
cancelled: "cancelled_with_failures",
expired: "expired_with_failures",
};
return map[batch.status] ?? batch.status;
}
Expand Down Expand Up @@ -135,8 +141,18 @@ function formatTs(ts: number | null | undefined): string {
});
}

export default function BatchDetailModal({ batch, files, onClose }: BatchDetailModalProps) {
export default function BatchDetailModal({ batch, files, onClose, onActionDone }: BatchDetailModalProps) {
const t = useTranslations("common");

// ── Action hook (F7) ─────────────────────────────────────────────────────────
const { cancelling, retrying, error: actionError, cancel, retry, downloadHrefOutput, downloadHrefErrors } =
useBatchActions({ onRefresh: onActionDone, t });

// ── Status flags ──────────────────────────────────────────────────────────────
const isTerminal = ["completed", "failed", "cancelled", "expired"].includes(batch.status);
const canCancel = ["validating", "in_progress", "finalizing"].includes(batch.status);
const canRetry = isTerminal && !!batch.errorFileId && (batch.requestCountsFailed ?? 0) > 0;

useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
Expand Down Expand Up @@ -166,15 +182,20 @@ export default function BatchDetailModal({ batch, files, onClose }: BatchDetailM
/>

{/* Panel */}
<div className="relative w-full sm:max-w-2xl bg-[var(--color-surface)] border border-[var(--color-border)] rounded-t-2xl sm:rounded-xl shadow-2xl animate-in fade-in slide-in-from-bottom-4 duration-200 max-h-[90vh] flex flex-col">
<div
className="relative w-full sm:max-w-2xl bg-[var(--color-surface)] border border-[var(--color-border)] rounded-t-2xl sm:rounded-xl shadow-2xl animate-in fade-in slide-in-from-bottom-4 duration-200 max-h-[90vh] flex flex-col"
role="dialog"
aria-modal="true"
aria-labelledby="batch-detail-modal-title"
>
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--color-border)] flex-shrink-0">
<div className="flex items-center gap-3">
<span className="material-symbols-outlined text-[20px] text-[var(--color-text-muted)]">
pending_actions
</span>
<div>
<h2 className="text-base font-semibold text-[var(--color-text-main)]">
<h2 id="batch-detail-modal-title" className="text-base font-semibold text-[var(--color-text-main)]">
Batch Details
</h2>
<div className="flex items-center gap-2 mt-0.5">
Expand Down Expand Up @@ -382,6 +403,93 @@ export default function BatchDetailModal({ batch, files, onClose }: BatchDetailM
</div>
)}
</div>

{/* ── Action footer (F7) ─────────────────────────────────────────── */}
{(canCancel || canRetry || batch.outputFileId || batch.errorFileId) && (
<div className="flex flex-wrap gap-2 px-6 py-4 border-t border-[var(--color-border)] flex-shrink-0">
{/* Download output */}
{batch.outputFileId && (
<a
href={downloadHrefOutput(batch.outputFileId) ?? "#"}
download={`batch-${batch.id}-output.jsonl`}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg bg-[var(--color-bg-alt)] border border-[var(--color-border)] text-[var(--color-text-main)] hover:opacity-90 transition-opacity"
>
<span className="material-symbols-outlined text-[16px]">download</span>
{t("batchActionDownloadOutput")}
</a>
)}

{/* Download errors */}
{batch.errorFileId && (
<a
href={downloadHrefErrors(batch.errorFileId) ?? "#"}
download={`batch-${batch.id}-errors.jsonl`}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg bg-red-500/10 border border-red-500/25 text-red-400 hover:text-red-300 transition-colors"
>
<span className="material-symbols-outlined text-[16px]">error_outline</span>
{t("batchActionDownloadErrors")}
</a>
)}

{/* Retry failed */}
{canRetry && (
<button
onClick={async () => {
if (
window.confirm(
t("batchDetailActionRetry") +
` (${batch.requestCountsFailed} ${t("batchActionRetry")})?`,
)
) {
const result = await retry({
id: batch.id,
inputFileId: batch.inputFileId,
errorFileId: batch.errorFileId,
endpoint: batch.endpoint,
});
if (result?.newBatchId) onClose();
}
}}
disabled={retrying}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg bg-blue-500/10 border border-blue-500/25 text-blue-400 hover:text-blue-300 transition-colors disabled:opacity-50"
>
<span className="material-symbols-outlined text-[16px]">
{retrying ? "hourglass_empty" : "refresh"}
</span>
{retrying ? t("batchDetailRetrying") : t("batchDetailActionRetry")}
</button>
)}

{/* Cancel */}
{canCancel && (
<button
onClick={async () => {
if (window.confirm(t("batchDetailCancelConfirm"))) {
await cancel(batch.id);
onClose();
}
}}
disabled={cancelling}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-lg bg-orange-500/10 border border-orange-500/25 text-orange-400 hover:text-orange-300 transition-colors disabled:opacity-50 ml-auto"
>
<span className="material-symbols-outlined text-[16px]">
{cancelling ? "hourglass_empty" : "close"}
</span>
{cancelling ? t("batchDetailCancelling") : t("batchDetailActionCancel")}
</button>
)}

{/* Action error — uses i18n key set by hook (D14: never raw err.message/stack) */}
{actionError && (
<div role="alert" className="basis-full mt-1 text-xs text-red-400">
{/* actionError is an i18n key from the hook (e.g. "batchActionCancel").
Cast is needed because next-intl types its arg as a specific union;
the hook guarantees this is always a valid message key. */}
{t(actionError as Parameters<typeof t>[0])}
</div>
)}
</div>
)}
</div>
</div>
);
Expand Down
Loading