diff --git a/.last-branch b/.last-branch
index f43e141..0390956 100644
--- a/.last-branch
+++ b/.last-branch
@@ -1 +1 @@
-v20260104-12-reports-object-name-job-name-columns-fix
+v20260104-13-reports-jobs-deduplicate-job-name-column
diff --git a/containers/backupchecks/src/templates/main/reports.html b/containers/backupchecks/src/templates/main/reports.html
index af0f7b5..b3c8f7f 100644
--- a/containers/backupchecks/src/templates/main/reports.html
+++ b/containers/backupchecks/src/templates/main/reports.html
@@ -196,16 +196,43 @@
if (items[j].key === key) return items[j].label || key;
}
}
+ // Backwards compatibility: object_name was used for job name in older configs.
+ if (key === 'job_name') {
+ for (var i2 = 0; i2 < reportColumnsMeta.groups.length; i2++) {
+ var items2 = reportColumnsMeta.groups[i2].items || [];
+ for (var j2 = 0; j2 < items2.length; j2++) {
+ if (items2[j2].key === 'object_name') return items2[j2].label || 'Job name';
+ }
+ }
+ return 'Job name';
+ }
return key;
}
+ function uniqPreserveOrder(arr) {
+ var out = [];
+ var seen = {};
+ (arr || []).forEach(function (k) {
+ var key = String(k);
+ if (seen[key]) return;
+ seen[key] = true;
+ out.push(k);
+ });
+ return out;
+ }
+
+ function normalizeCols(arr) {
+ if (!Array.isArray(arr)) return [];
+ return uniqPreserveOrder(arr.map(function (k) { return (k === 'object_name') ? 'job_name' : k; }));
+ }
+
function defaultColsFor(view) {
if (reportColumnsMeta && reportColumnsMeta.defaults && reportColumnsMeta.defaults[view]) {
- return reportColumnsMeta.defaults[view].slice();
+ return normalizeCols(reportColumnsMeta.defaults[view].slice());
}
// hard fallback
- if (view === 'snapshot') return ['object_name','customer_name','job_id','job_name','status','run_at'];
- return ['object_name','total_runs','success_count','warning_count','failed_count','missed_count','success_rate'];
+ if (view === 'snapshot') return ['job_name','customer_name','job_id','status','run_at'];
+ return ['job_name','total_runs','success_count','warning_count','failed_count','missed_count','success_rate'];
}
function selectedColsFor(view) {
@@ -218,9 +245,9 @@
}
if (hasView && Array.isArray(cols)) {
// If an empty list is saved, keep it empty.
- return cols;
+ return normalizeCols(cols);
}
- if (cols && cols.length) return cols;
+ if (cols && cols.length) return normalizeCols(cols);
return defaultColsFor(view);
}
@@ -337,7 +364,8 @@
return (
'
' +
cols.map(function (k) {
- return td(r[k], (k === 'run_at' || k === 'reviewed_at' || k === 'job_id' || k === 'run_id' || k === 'customer_name'));
+ var val = (k === 'job_name') ? ((r.job_name !== null && r.job_name !== undefined && String(r.job_name).length) ? r.job_name : r.object_name) : r[k];
+ return td(val, (k === 'run_at' || k === 'reviewed_at' || k === 'job_id' || k === 'run_id' || k === 'customer_name'));
}).join('') +
'
'
);
diff --git a/containers/backupchecks/src/templates/main/reports_new.html b/containers/backupchecks/src/templates/main/reports_new.html
index 23f9648..27e9b67 100644
--- a/containers/backupchecks/src/templates/main/reports_new.html
+++ b/containers/backupchecks/src/templates/main/reports_new.html
@@ -288,6 +288,102 @@
};
}
+ function uniqPreserveOrder(arr) {
+ var out = [];
+ var seen = {};
+ (arr || []).forEach(function (k) {
+ var key = String(k);
+ if (seen[key]) return;
+ seen[key] = true;
+ out.push(k);
+ });
+ return out;
+ }
+
+ function normalizeJobNameColumns() {
+ // Merge legacy object_name into job_name (single logical column: "Job name").
+ if (repColsMeta && repColsMeta.groups) {
+ repColsMeta.groups.forEach(function (g) {
+ var items = g.items || [];
+ var jobItem = null;
+ var objItem = null;
+
+ items.forEach(function (it) {
+ if (!it) return;
+ if (it.key === 'job_name') jobItem = it;
+ if (it.key === 'object_name') objItem = it;
+ });
+
+ if (objItem && !jobItem) {
+ // convert object_name into job_name
+ objItem.key = 'job_name';
+ objItem.label = 'Job name';
+ jobItem = objItem;
+ objItem = null;
+ }
+
+ if (jobItem) {
+ jobItem.label = 'Job name';
+ // Merge views if object_name existed in the same group
+ if (objItem && Array.isArray(objItem.views)) {
+ var merged = (Array.isArray(jobItem.views) ? jobItem.views.slice() : []);
+ objItem.views.forEach(function (v) {
+ if (merged.indexOf(v) < 0) merged.push(v);
+ });
+ jobItem.views = merged;
+ }
+ }
+
+ // Remove any remaining object_name items
+ g.items = (g.items || []).filter(function (it) { return it && it.key !== 'object_name'; });
+
+ // Remove duplicate job_name items within the same group
+ var seenJob = false;
+ g.items = (g.items || []).filter(function (it) {
+ if (!it) return false;
+ if (it.key !== 'job_name') return true;
+ if (!seenJob) {
+ seenJob = true;
+ it.label = 'Job name';
+ return true;
+ }
+ return false;
+ });
+ });
+
+ // Ensure job_name only appears once across all groups.
+ var seenGlobalJob = false;
+ repColsMeta.groups.forEach(function (g2) {
+ g2.items = (g2.items || []).filter(function (it2) {
+ if (!it2) return false;
+ if (it2.key !== 'job_name') return true;
+ if (!seenGlobalJob) {
+ seenGlobalJob = true;
+ it2.label = 'Job name';
+ return true;
+ }
+ return false;
+ });
+ });
+ }
+
+ // Defaults: replace object_name -> job_name and de-duplicate
+ if (repColsMeta && repColsMeta.defaults) {
+ ['summary', 'snapshot', 'jobs'].forEach(function (v) {
+ var d = repColsMeta.defaults[v];
+ if (!Array.isArray(d)) return;
+ repColsMeta.defaults[v] = uniqPreserveOrder(d.map(function (k) { return (k === 'object_name') ? 'job_name' : k; }));
+ });
+ }
+
+ // Selected: replace object_name -> job_name and de-duplicate
+ ['summary', 'snapshot', 'jobs'].forEach(function (v) {
+ if (repColsSelected[v] === null || typeof repColsSelected[v] === 'undefined') return;
+ if (!Array.isArray(repColsSelected[v])) return;
+ repColsSelected[v] = uniqPreserveOrder(repColsSelected[v].map(function (k) { return (k === 'object_name') ? 'job_name' : k; }));
+ });
+ }
+
// --- Job filters (backup software / backup type) ---
var jobFiltersMeta = window.__jobFiltersMeta || null;
@@ -610,6 +706,7 @@
if (repColsMeta) {
showColsLoading(false);
clearColsError();
+ normalizeJobNameColumns();
ensureDefaultsFromMeta();
qs('rep_cols_tab_summary').addEventListener('click', function () { setColsView('summary'); });
qs('rep_cols_tab_snapshot').addEventListener('click', function () { setColsView('snapshot'); });
@@ -632,6 +729,7 @@
}
repColsMeta = res.json || null;
+ normalizeJobNameColumns();
ensureDefaultsFromMeta();
// bind tabs once metadata is ready
diff --git a/docs/changelog.md b/docs/changelog.md
index d9a0372..3aafd86 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -289,6 +289,15 @@
- Fixed column selection behavior so an explicitly saved empty selection stays empty (defaults are only applied when the view has no configured selection).
- Updated the raw data table and HTML export to show a clear “No columns selected” message when all columns are disabled, instead of still rendering default data.
+---
+
+## v20260104-13-reports-jobs-deduplicate-job-name-column
+
+- Unified `object_name` and `job_name` into a single logical column labeled "Job name".
+- Removed duplicate "Job name" entries from available and selected column lists in Summary, Snapshot, and Jobs views.
+- Added migration logic to automatically clean up existing report configurations containing duplicate job name columns.
+- Ensured report rendering strictly follows the selected columns; when no columns are selected, no table data is shown.
+
================================================================================================================================================
## v0.1.15