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):
- - - - - - - - - - - - + {jobs_th} -""" + "\n".join(job_row_html) + """ +""" + jobs_tr + """
ObjectJobSoftwareTypeLast runStatusMissedOverrideTicketRemark
@@ -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):
- - - - - - - - - - - - - + {snap_th} -""" + "\n".join(row_html) + """ +""" + snap_tr + """
ObjectCustomerJobSoftwareTypeRun atStatusMissedOverrideTicketRemark
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 @@
+
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