From d51741532a5e59845bfe9c0a7cb3cc3a836f1b7f Mon Sep 17 00:00:00 2001 From: Ivo Oskamp Date: Sun, 4 Jan 2026 16:59:43 +0100 Subject: [PATCH] Auto-commit local changes before build (2026-01-04 16:59:43) --- .last-branch | 2 +- .../src/templates/main/reports.html | 40 ++++++-- .../src/templates/main/reports_new.html | 98 +++++++++++++++++++ docs/changelog.md | 9 ++ 4 files changed, 142 insertions(+), 7 deletions(-) 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