diff --git a/.last-branch b/.last-branch
index 563c3b1..de76ca9 100644
--- a/.last-branch
+++ b/.last-branch
@@ -1 +1 @@
-v20260104-04-reports-html-jobs-table-fix
+v20260104-05-reports-html-jobs-columns-selection
diff --git a/containers/backupchecks/src/backend/app/main/routes_reporting_api.py b/containers/backupchecks/src/backend/app/main/routes_reporting_api.py
index 208fcc1..992ad86 100644
--- a/containers/backupchecks/src/backend/app/main/routes_reporting_api.py
+++ b/containers/backupchecks/src/backend/app/main/routes_reporting_api.py
@@ -192,6 +192,7 @@ def build_report_columns_meta():
"views": [
{"key": "summary", "label": "Summary"},
{"key": "snapshot", "label": "Snapshot"},
+ {"key": "jobs", "label": "Jobs"},
],
"defaults": {
"summary": [
@@ -220,6 +221,18 @@ def build_report_columns_meta():
"remark",
"reviewed_at",
],
+ "jobs": [
+ "object_name",
+ "job_name",
+ "backup_software",
+ "backup_type",
+ "run_at",
+ "status",
+ "missed",
+ "override_applied",
+ "ticket_number",
+ "remark",
+ ],
},
"groups": [
{
@@ -238,35 +251,35 @@ def build_report_columns_meta():
{
"name": "Job Information",
"items": [
- {"key": "job_name", "label": "Job name", "views": ["snapshot"]},
+ {"key": "job_name", "label": "Job name", "views": ["snapshot", "jobs"]},
{"key": "job_id", "label": "Job ID", "views": ["snapshot"]},
- {"key": "backup_software", "label": "Job type", "views": ["snapshot"]},
- {"key": "backup_type", "label": "Repository / Target", "views": ["snapshot"]},
- {"key": "customer_name", "label": "Customer", "views": ["snapshot", "summary"]},
+ {"key": "backup_software", "label": "Software", "views": ["snapshot", "jobs"]},
+ {"key": "backup_type", "label": "Type", "views": ["snapshot", "jobs"]},
+ {"key": "customer_name", "label": "Customer", "views": ["snapshot", "summary", "jobs"]},
],
},
{
"name": "Status",
"items": [
- {"key": "status", "label": "Last run status", "views": ["snapshot"]},
- {"key": "missed", "label": "Missed", "views": ["snapshot"]},
- {"key": "override_applied", "label": "Override applied", "views": ["snapshot"]},
+ {"key": "status", "label": "Status", "views": ["snapshot", "jobs"]},
+ {"key": "missed", "label": "Missed", "views": ["snapshot", "jobs"]},
+ {"key": "override_applied", "label": "Override", "views": ["snapshot", "jobs"]},
{"key": "run_id", "label": "Run ID", "views": ["snapshot"]},
- {"key": "ticket_number", "label": "Ticket number", "views": ["snapshot"]},
- {"key": "remark", "label": "Remark", "views": ["snapshot"]},
+ {"key": "ticket_number", "label": "Ticket", "views": ["snapshot", "jobs"]},
+ {"key": "remark", "label": "Remark", "views": ["snapshot", "jobs"]},
],
},
{
"name": "Time",
"items": [
- {"key": "run_at", "label": "Start time", "views": ["snapshot"]},
+ {"key": "run_at", "label": "Last run", "views": ["snapshot", "jobs"]},
{"key": "reviewed_at", "label": "Reviewed at", "views": ["snapshot"]},
],
},
{
"name": "Object",
"items": [
- {"key": "object_name", "label": "Object name", "views": ["snapshot", "summary"]},
+ {"key": "object_name", "label": "Object", "views": ["snapshot", "summary", "jobs"]},
],
},
{
@@ -1103,6 +1116,68 @@ def _export_html_response(report: ReportDefinition, report_id: int, view: str):
include_customers = html_content in ("customers", "both")
include_jobs = html_content in ("jobs", "both")
+ cols_meta = build_report_columns_meta()
+ label_map: dict[str, str] = {}
+ allowed_by_view: dict[str, set[str]] = {}
+ for g in (cols_meta.get("groups") or []):
+ for it in (g.get("items") or []):
+ k = (it.get("key") or "").strip()
+ if not k:
+ continue
+ label_map[k] = (it.get("label") or k)
+ for v in (it.get("views") or []):
+ allowed_by_view.setdefault(v, set()).add(k)
+
+ def _selected_cols(view_key: str) -> list[str]:
+ cols = []
+ rc_cols = rc.get("columns") if isinstance(rc, dict) else None
+ if isinstance(rc_cols, dict):
+ v = rc_cols.get(view_key)
+ if isinstance(v, list):
+ cols = [str(x).strip() for x in v if str(x).strip()]
+
+ if not cols:
+ d = (cols_meta.get("defaults") or {}).get(view_key)
+ if isinstance(d, list):
+ cols = [str(x).strip() for x in d if str(x).strip()]
+
+ allowed = allowed_by_view.get(view_key) or set()
+ cols = [c for c in cols if c in allowed]
+ if not cols:
+ cols = [c for c in (cols_meta.get("defaults") or {}).get(view_key, []) if c in allowed]
+ return cols
+
+ def _td(value: str, cls: str = "") -> str:
+ cl = f" class='{cls}'" if cls else ""
+ return f"
{value} "
+
+ def _th(label: str, cls: str = "") -> str:
+ cl = f" class='{cls}'" if cls else ""
+ return f"{label} "
+
+ def _render_table(view_key: str, rows: list[dict]) -> tuple[str, str]:
+ cols = _selected_cols(view_key)
+
+ th = []
+ for k in cols:
+ cls = "text-end" if k in ("missed", "override_applied") else ""
+ th.append(_th(_esc(label_map.get(k) or k), cls))
+
+ tr = []
+ for r in rows:
+ tds = []
+ for k in cols:
+ if k in ("missed", "override_applied"):
+ v = "1" if bool(r.get(k)) else "0"
+ tds.append(_td(_esc(v), "text-end"))
+ elif k == "run_at":
+ tds.append(_td(_esc(r.get(k) or ""), "text-muted small"))
+ else:
+ tds.append(_td(_esc(r.get(k) or "")))
+ tr.append("" + "".join(tds) + " ")
+
+ return "".join(th), "\n".join(tr)
+
# Snapshot preview table can be requested explicitly via view=snapshot.
want_snapshot_table = (view or "summary").strip().lower() == "snapshot"
@@ -1157,22 +1232,7 @@ def _export_html_response(report: ReportDefinition, report_id: int, view: str):
}
)
- job_row_html = []
- for j in jobs_rows:
- job_row_html.append(
- ""
- f"{_esc(j['object_name'])} "
- f"{_esc(j['job_name'])} "
- f"{_esc(j['backup_software'])} "
- f"{_esc(j['backup_type'])} "
- f"{_esc(j['run_at'])} "
- f"{_esc(j['status'])} "
- f"{'1' if j['missed'] else '0'} "
- f"{'1' if j['override_applied'] else '0'} "
- f"{_esc(j['ticket_number'])} "
- f"{_esc(j['remark'])} "
- " "
- )
+ jobs_th, jobs_tr = _render_table("jobs", jobs_rows)
jobs_table_html = """
@@ -1185,21 +1245,10 @@ def _export_html_response(report: ReportDefinition, report_id: int, view: str):
-
- Object
- Job
- Software
- Type
- Last run
- Status
- Missed
- Override
- Ticket
- Remark
-
+ {jobs_th}
-""" + "\n".join(job_row_html) + """
+""" + jobs_tr + """
@@ -1252,23 +1301,7 @@ def _export_html_response(report: ReportDefinition, report_id: int, view: str):
}
)
- row_html = []
- for s in snap_rows:
- row_html.append(
- "
"
- f"{_esc(s['object_name'])} "
- f"{_esc(s['customer_name'])} "
- f"{_esc(s['job_name'])} "
- f"{_esc(s['backup_software'])} "
- f"{_esc(s['backup_type'])} "
- f"{_esc(s['run_at'])} "
- f"{_esc(s['status'])} "
- f"{'1' if s['missed'] else '0'} "
- f"{'1' if s['override_applied'] else '0'} "
- f"{_esc(s['ticket_number'])} "
- f"{_esc(s['remark'])} "
- " "
- )
+ snap_th, snap_tr = _render_table("snapshot", snap_rows)
snapshot_table_html = """
@@ -1281,22 +1314,10 @@ def _export_html_response(report: ReportDefinition, report_id: int, view: str):
-
- Object
- Customer
- Job
- Software
- Type
- Run at
- Status
- Missed
- Override
- Ticket
- Remark
-
+ {snap_th}
-""" + "\n".join(row_html) + """
+""" + snap_tr + """
diff --git a/containers/backupchecks/src/templates/main/reports_new.html b/containers/backupchecks/src/templates/main/reports_new.html
index 16846f9..ed0e37a 100644
--- a/containers/backupchecks/src/templates/main/reports_new.html
+++ b/containers/backupchecks/src/templates/main/reports_new.html
@@ -166,6 +166,7 @@
Summary
Snapshot
+ Jobs
Select columns for the summary view.
@@ -274,13 +275,14 @@
// --- Report content / column selector ---
var repColsView = 'summary';
var repColsMeta = window.__reportColumnsMeta || null;
- var repColsSelected = { summary: [], snapshot: [] };
+ var repColsSelected = { summary: [], snapshot: [], jobs: [] };
if (isEdit && initialReport && initialReport.report_config && initialReport.report_config.columns) {
var cols = initialReport.report_config.columns;
repColsSelected = {
summary: Array.isArray(cols.summary) ? cols.summary.slice() : [],
snapshot: Array.isArray(cols.snapshot) ? cols.snapshot.slice() : [],
+ jobs: Array.isArray(cols.jobs) ? cols.jobs.slice() : [],
};
}
@@ -392,23 +394,20 @@
})();
function colsHintText(viewKey) {
- return viewKey === 'snapshot'
- ? 'Select columns for the snapshot view.'
- : 'Select columns for the summary view.';
+ if (viewKey === 'snapshot') return 'Select columns for the snapshot view.';
+ if (viewKey === 'jobs') return 'Select columns for the jobs view (HTML/PDF).';
+ return 'Select columns for the summary view.';
}
function setColsView(viewKey) {
- repColsView = (viewKey === 'snapshot') ? 'snapshot' : 'summary';
+ repColsView = (viewKey === 'snapshot' || viewKey === 'jobs') ? viewKey : 'summary';
var a = qs('rep_cols_tab_summary');
var b = qs('rep_cols_tab_snapshot');
- if (repColsView === 'summary') {
- a.classList.add('active');
- b.classList.remove('active');
- } else {
- b.classList.add('active');
- a.classList.remove('active');
- }
+ var c = qs('rep_cols_tab_jobs');
+ a.classList.toggle('active', repColsView === 'summary');
+ b.classList.toggle('active', repColsView === 'snapshot');
+ c.classList.toggle('active', repColsView === 'jobs');
qs('rep_cols_hint').textContent = colsHintText(repColsView);
renderColsAvailable();
@@ -435,7 +434,7 @@
function ensureDefaultsFromMeta() {
if (!repColsMeta || !repColsMeta.defaults) return;
- ['summary', 'snapshot'].forEach(function (v) {
+ ['summary', 'snapshot', 'jobs'].forEach(function (v) {
if (!repColsSelected[v] || !repColsSelected[v].length) {
repColsSelected[v] = (repColsMeta.defaults[v] || []).slice();
}
@@ -612,6 +611,7 @@
ensureDefaultsFromMeta();
qs('rep_cols_tab_summary').addEventListener('click', function () { setColsView('summary'); });
qs('rep_cols_tab_snapshot').addEventListener('click', function () { setColsView('snapshot'); });
+ qs('rep_cols_tab_jobs').addEventListener('click', function () { setColsView('jobs'); });
setColsView('summary');
return;
}
@@ -635,6 +635,7 @@
// bind tabs once metadata is ready
qs('rep_cols_tab_summary').addEventListener('click', function () { setColsView('summary'); });
qs('rep_cols_tab_snapshot').addEventListener('click', function () { setColsView('snapshot'); });
+ qs('rep_cols_tab_jobs').addEventListener('click', function () { setColsView('jobs'); });
setColsView('summary');
})
diff --git a/docs/changelog.md b/docs/changelog.md
index 3841936..c4eaf46 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -217,6 +217,15 @@
- Restored the jobs table below the charts in HTML reports for single-customer selections.
- Ensured the latest snapshot per job is displayed correctly in the HTML output.
+---
+
+## v20260104-05-reports-html-jobs-columns-selection
+
+- Added configurable column selection for the Jobs table in report create/edit views.
+- Exposed all Jobs table columns as selectable options instead of using fixed/hardcoded columns.
+- Ensured the HTML report Jobs table only renders columns explicitly selected in the report configuration.
+- Aligned Jobs table rendering logic with Snapshot and Summary column selection behavior.
+
================================================================================================================================================
## v0.1.15