diff --git a/web/src/app.css b/web/src/app.css index caf9ef0..6eaf533 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1056,3 +1056,16 @@ table.tbl a.rowlink:hover { padding: 12px 14px; } } + +.tbl .chk-col { + width: 32px; + text-align: center; + padding-right: 0; +} +.tbl .chk-col input[type='checkbox'] { + cursor: pointer; +} +.tbl .chk-col input[type='checkbox']:disabled { + cursor: not-allowed; + opacity: 0.4; +} diff --git a/web/src/pages/TaskDetail.tsx b/web/src/pages/TaskDetail.tsx index a5291d6..1ce3f64 100644 --- a/web/src/pages/TaskDetail.tsx +++ b/web/src/pages/TaskDetail.tsx @@ -90,6 +90,7 @@ export function TaskDetail({ id }: { id: number }) { const [live, setLive] = useState>({}) const [showRuns, setShowRuns] = useState(false) const [errorsFor, setErrorsFor] = useState<{ id: number; src_login: string } | null>(null) + const [selected, setSelected] = useState>(new Set()) const fileInputRef = useRef(null) function reload() { @@ -332,7 +333,8 @@ export function TaskDetail({ id }: { id: number }) { setBusy('run') setError(null) try { - await runTask(id) + const ids = accounts.filter((a) => selected.has(a.id)).map((a) => a.id) + await runTask(id, ids.length ? ids : undefined) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to start run') } finally { @@ -366,7 +368,33 @@ export function TaskDetail({ id }: { id: number }) { } const { task, accounts } = data - const allTested = accounts.length > 0 && accounts.every((a) => a.test_src_status === 'ok' && a.test_dst_status === 'ok') + const isRunning = task.status === 'running' + // A row is selectable only when both connection tests pass and no run is live. + const selectableIds = accounts + .filter((a) => a.test_src_status === 'ok' && a.test_dst_status === 'ok') + .map((a) => a.id) + const selectableSet = new Set(selectableIds) + // Effective set: the checked accounts, or all accounts when nothing is checked. + const effectiveSelected = accounts.filter((a) => selected.has(a.id)) + const runSet = effectiveSelected.length > 0 ? effectiveSelected : accounts + const runReady = + runSet.length > 0 && runSet.every((a) => a.test_src_status === 'ok' && a.test_dst_status === 'ok') + const allSelectableChecked = + selectableIds.length > 0 && selectableIds.every((id) => selected.has(id)) + const someSelectableChecked = selectableIds.some((id) => selected.has(id)) + + function toggleOne(accId: number, checked: boolean) { + setSelected((prev) => { + const next = new Set(prev) + if (checked) next.add(accId) + else next.delete(accId) + return next + }) + } + + function toggleAll(checked: boolean) { + setSelected(checked ? new Set(selectableIds) : new Set()) + } // Prefer live (WS) copied/skipped over the DB values, which only advance per // folder — so the summary moves in real time during a large folder. const totals = accounts.reduce( @@ -418,10 +446,20 @@ export function TaskDetail({ id }: { id: number }) { - - {!allTested && accounts.length > 0 && run unlocks once every account tests OK on both sides} + {!runReady && accounts.length > 0 && ( + + {effectiveSelected.length > 0 + ? 'selected accounts must pass both connection tests' + : 'run unlocks once every account tests OK on both sides'} + + )}
@@ -540,6 +578,18 @@ export function TaskDetail({ id }: { id: number }) { + @@ -554,11 +604,20 @@ export function TaskDetail({ id }: { id: number }) { {accounts.length === 0 ? ( - + ) : ( accounts.map((a) => ( +
+ { + if (el) el.indeterminate = !allSelectableChecked && someSelectableChecked + }} + disabled={isRunning || selectableIds.length === 0} + onChange={(e) => toggleAll(e.target.checked)} + /> + Account Src test Dst test
no accounts yet — add one or import a CSV aboveno accounts yet — add one or import a CSV above
+ toggleOne(a.id, e.target.checked)} + /> +
{a.src_login}