|
|
|
@@ -1,13 +1,13 @@
|
|
|
|
|
// views/jobs.js — submit sync/copy/move jobs and manage recurring transfers.
|
|
|
|
|
|
|
|
|
|
import { post } from "../rc.js?v=persist-all-jobs-1";
|
|
|
|
|
import { post } from "../rc.js?v=job-stats-1";
|
|
|
|
|
import {
|
|
|
|
|
toast,
|
|
|
|
|
formatBytes,
|
|
|
|
|
formatSpeed,
|
|
|
|
|
formatDuration,
|
|
|
|
|
} from "../state.js?v=persist-all-jobs-1";
|
|
|
|
|
import { t } from "../i18n.js?v=persist-all-jobs-1";
|
|
|
|
|
} from "../state.js?v=job-stats-1";
|
|
|
|
|
import { t } from "../i18n.js?v=job-stats-1";
|
|
|
|
|
import {
|
|
|
|
|
createOneTimeJob,
|
|
|
|
|
createRecurringJob,
|
|
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
|
|
runRecurringJobNow,
|
|
|
|
|
stopOneTimeJob,
|
|
|
|
|
stopRecurringJob,
|
|
|
|
|
} from "../jobs_api.js?v=persist-all-jobs-1";
|
|
|
|
|
} from "../jobs_api.js?v=job-stats-1";
|
|
|
|
|
|
|
|
|
|
let pollTimer = null;
|
|
|
|
|
const LOCAL_FS_VALUE = "__local__";
|
|
|
|
@@ -566,11 +566,13 @@ function renderJobRows(s) {
|
|
|
|
|
|
|
|
|
|
function renderJobRow(job) {
|
|
|
|
|
const snapshot = job.statusSnapshot || {};
|
|
|
|
|
const p = snapshot.progress || {};
|
|
|
|
|
const stats = transferStats(snapshot);
|
|
|
|
|
const id = job.id;
|
|
|
|
|
const key = jobKey(job);
|
|
|
|
|
const running = !!job.running;
|
|
|
|
|
const jobid = job.currentJobid || job.lastJobid;
|
|
|
|
|
const displayedSpeed = stats.speed || averageSpeed(stats);
|
|
|
|
|
const displayedEta = running ? stats.eta : 0;
|
|
|
|
|
|
|
|
|
|
const badge = renderStatusBadge(job.status);
|
|
|
|
|
const jobCell = `
|
|
|
|
@@ -590,12 +592,12 @@ function renderJobRow(job) {
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const pct = p && p.totalBytes > 0 ? Math.min(100, (p.bytes / p.totalBytes) * 100) : 0;
|
|
|
|
|
const pct = stats.totalBytes > 0 ? Math.min(100, (stats.bytes / stats.totalBytes) * 100) : 0;
|
|
|
|
|
const progress = `
|
|
|
|
|
<div style="display:flex;flex-direction:column;gap:4px;min-width:120px">
|
|
|
|
|
<div class="progress"><span style="width:${pct.toFixed(1)}%"></span></div>
|
|
|
|
|
<span class="col-mono" style="font-size:11px;color:var(--color-muted)">
|
|
|
|
|
${formatBytes(p.bytes)} / ${formatBytes(p.totalBytes)}
|
|
|
|
|
${formatBytes(stats.bytes)} / ${formatBytes(stats.totalBytes)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
@@ -619,10 +621,10 @@ function renderJobRow(job) {
|
|
|
|
|
<td>${badge}</td>
|
|
|
|
|
<td>${scheduleCell}</td>
|
|
|
|
|
<td class="col-num">${progress}</td>
|
|
|
|
|
<td class="col-num col-mono">${running ? escapeHtml(formatSpeed(p.speed || 0)) : "—"}</td>
|
|
|
|
|
<td class="col-num col-mono">${running ? escapeHtml(formatDuration(p.eta || 0)) : "—"}</td>
|
|
|
|
|
<td class="col-num col-mono">${p.transfers ?? 0} / ${p.totalTransfers ?? 0}</td>
|
|
|
|
|
<td class="col-num col-mono">${(p.errors && p.errors.length) || 0}</td>
|
|
|
|
|
<td class="col-num col-mono">${escapeHtml(formatSpeed(displayedSpeed))}</td>
|
|
|
|
|
<td class="col-num col-mono">${escapeHtml(formatDuration(displayedEta))}</td>
|
|
|
|
|
<td class="col-num col-mono">${escapeHtml(formatTransferCount(stats))}</td>
|
|
|
|
|
<td class="col-num col-mono">${escapeHtml(String(stats.errors || 0))}</td>
|
|
|
|
|
<td class="col-num">
|
|
|
|
|
<div class="job-actions">
|
|
|
|
|
${detailBtn}
|
|
|
|
@@ -655,6 +657,7 @@ function renderStatusBadge(status) {
|
|
|
|
|
|
|
|
|
|
function renderJobDetailsRow(job) {
|
|
|
|
|
const snapshot = job.statusSnapshot || {};
|
|
|
|
|
const stats = transferStats(snapshot);
|
|
|
|
|
const jobid = job.currentJobid || job.lastJobid || "";
|
|
|
|
|
const details = [
|
|
|
|
|
[job.kind === "once" ? t("jobs.detail_record_id") : t("jobs.detail_schedule_id"), displayJobId(job)],
|
|
|
|
@@ -668,6 +671,11 @@ function renderJobDetailsRow(job) {
|
|
|
|
|
[t("jobs.detail_schedule"), formatSchedule(job)],
|
|
|
|
|
[t("jobs.detail_group"), snapshot.group || ""],
|
|
|
|
|
[t("jobs.detail_execute_id"), snapshot.executeId || ""],
|
|
|
|
|
[t("jobs.detail_bytes"), `${formatBytes(stats.bytes)} / ${formatBytes(stats.totalBytes)}`],
|
|
|
|
|
[t("jobs.detail_speed"), formatSpeed(stats.speed || averageSpeed(stats))],
|
|
|
|
|
[t("jobs.detail_eta"), formatDuration(job.running ? stats.eta : 0)],
|
|
|
|
|
[t("jobs.detail_files"), formatTransferCount(stats)],
|
|
|
|
|
[t("jobs.detail_errors"), String(stats.errors || 0)],
|
|
|
|
|
];
|
|
|
|
|
const output = snapshot && Object.keys(snapshot).length > 0
|
|
|
|
|
? JSON.stringify(snapshot, null, 2)
|
|
|
|
@@ -698,6 +706,54 @@ function renderJobDetailsRow(job) {
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function transferStats(snapshot) {
|
|
|
|
|
const stats = snapshot.stats || snapshot.progress || snapshot || {};
|
|
|
|
|
return {
|
|
|
|
|
bytes: numberOrZero(stats.bytes),
|
|
|
|
|
totalBytes: numberOrZero(stats.totalBytes),
|
|
|
|
|
speed: numberOrZero(stats.speed),
|
|
|
|
|
eta: numberOrNull(stats.eta),
|
|
|
|
|
transfers: numberOrZero(stats.transfers),
|
|
|
|
|
totalTransfers: numberOrZero(stats.totalTransfers),
|
|
|
|
|
checks: numberOrZero(stats.checks),
|
|
|
|
|
totalChecks: numberOrZero(stats.totalChecks),
|
|
|
|
|
transferTime: numberOrZero(stats.transferTime),
|
|
|
|
|
errors: normalizeErrors(stats.errors),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function averageSpeed(stats) {
|
|
|
|
|
if (stats.bytes > 0 && stats.transferTime > 0) {
|
|
|
|
|
return stats.bytes / stats.transferTime;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatTransferCount(stats) {
|
|
|
|
|
if (stats.totalTransfers > 0) {
|
|
|
|
|
return `${stats.transfers} / ${stats.totalTransfers}`;
|
|
|
|
|
}
|
|
|
|
|
if (stats.totalChecks > 0) {
|
|
|
|
|
return `${stats.checks} / ${stats.totalChecks}`;
|
|
|
|
|
}
|
|
|
|
|
return String(stats.transfers || stats.checks || 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeErrors(errors) {
|
|
|
|
|
if (Array.isArray(errors)) return errors.length;
|
|
|
|
|
return numberOrZero(errors);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function numberOrZero(value) {
|
|
|
|
|
const n = Number(value);
|
|
|
|
|
return Number.isFinite(n) ? n : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function numberOrNull(value) {
|
|
|
|
|
const n = Number(value);
|
|
|
|
|
return Number.isFinite(n) ? n : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function detailStatus(job) {
|
|
|
|
|
const status = job.status || "scheduled";
|
|
|
|
|
const key = `jobs.status.${status}`;
|
|
|
|
|