diff --git a/.last-branch b/.last-branch index def0b75..1244e94 100644 --- a/.last-branch +++ b/.last-branch @@ -1 +1 @@ -v20260103-16-reports-snapshot-columns-runid-remark-toggle +v20260103-17-reports-job-filters-exclude-info-jobs 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 6ed153a..1a14c76 100644 --- a/containers/backupchecks/src/backend/app/main/routes_reporting_api.py +++ b/containers/backupchecks/src/backend/app/main/routes_reporting_api.py @@ -302,6 +302,65 @@ def build_report_columns_meta(): ], } + +def build_report_job_filters_meta(): + """Build job filter metadata for reporting UIs. + + Provides available backup_softwares and backup_types derived from active jobs. + """ + # Distinct values across active jobs (exclude known informational jobs). + info_backup_types = {"license key"} + + rows = ( + db.session.query(Job.backup_software, Job.backup_type) + .filter(Job.active.is_(True)) + .all() + ) + + backup_softwares_set = set() + backup_types_set = set() + by_backup_software = {} + + for bs, bt in rows: + bs_val = (bs or "").strip() + bt_val = (bt or "").strip() + if bt_val.lower() in info_backup_types: + continue + + if bs_val: + backup_softwares_set.add(bs_val) + if bt_val: + backup_types_set.add(bt_val) + + if bs_val and bt_val: + by_backup_software.setdefault(bs_val, set()).add(bt_val) + + def _sort_key(v: str): + return (v or "").casefold() + + backup_softwares = [{"key": v, "label": v} for v in sorted(backup_softwares_set, key=_sort_key)] + backup_types = [{"key": v, "label": v} for v in sorted(backup_types_set, key=_sort_key)] + by_backup_software_out = { + k: sorted(list(vset), key=_sort_key) for k, vset in sorted(by_backup_software.items(), key=lambda kv: _sort_key(kv[0])) + } + + return { + "backup_softwares": backup_softwares, + "backup_types": backup_types, + "by_backup_software": by_backup_software_out, + "excluded_backup_types": ["License Key"], + } + + +@main_bp.route("/api/reports/job-filters", methods=["GET"]) +@login_required +def api_reports_job_filters(): + err = _require_reporting_role() + if err is not None: + return err + return build_report_job_filters_meta() + + @main_bp.route("/api/reports/columns", methods=["GET"]) @login_required def api_reports_columns(): @@ -403,6 +462,26 @@ def api_reports_generate(report_id: int): # run_object_links.run_id -> job_runs -> jobs where_customer = "" params = {"rid": report_id, "start_ts": report.period_start, "end_ts": report.period_end} + # Job filters from report_config + where_filters = " AND COALESCE(j.backup_type,'') NOT ILIKE 'license key' " + rc = _safe_json_dict(getattr(report, "report_config", None)) + filters = rc.get("filters") if isinstance(rc, dict) else None + if isinstance(filters, dict): + sel_sw = filters.get("backup_softwares") + sel_bt = filters.get("backup_types") + if isinstance(sel_sw, list): + sel_sw = [str(v).strip() for v in sel_sw if str(v).strip()] + if sel_sw: + where_filters += " AND j.backup_software = ANY(:sel_backup_softwares) " + params["sel_backup_softwares"] = sel_sw + if isinstance(sel_bt, list): + sel_bt = [str(v).strip() for v in sel_bt if str(v).strip()] + # Always exclude known informational jobs like License Key. + sel_bt = [v for v in sel_bt if v.lower() != "license key"] + if sel_bt: + where_filters += " AND j.backup_type = ANY(:sel_backup_types) " + params["sel_backup_types"] = sel_bt + if scope in ("single", "multiple") and customer_ids: where_customer = " AND j.customer_id = ANY(:customer_ids) " params["customer_ids"] = customer_ids @@ -439,6 +518,7 @@ def api_reports_generate(report_id: int): AND jr.run_at >= :start_ts AND jr.run_at < :end_ts {where_customer} + {where_filters} ''' ), params, diff --git a/containers/backupchecks/src/backend/app/main/routes_reports.py b/containers/backupchecks/src/backend/app/main/routes_reports.py index f05d0ca..84be6f9 100644 --- a/containers/backupchecks/src/backend/app/main/routes_reports.py +++ b/containers/backupchecks/src/backend/app/main/routes_reports.py @@ -1,6 +1,6 @@ from .routes_shared import * # noqa: F401,F403 from datetime import date, timedelta -from .routes_reporting_api import build_report_columns_meta +from .routes_reporting_api import build_report_columns_meta, build_report_job_filters_meta def get_default_report_period(): """Return default report period (last 7 days).""" @@ -67,6 +67,7 @@ def reports(): "main/reports.html", initial_reports=items, columns_meta=build_report_columns_meta(), + job_filters_meta=build_report_job_filters_meta(), default_period_start=period_start.isoformat(), default_period_end=period_end.isoformat(), ) @@ -88,6 +89,7 @@ def reports_new(): "main/reports_new.html", initial_customers=customer_items, columns_meta=build_report_columns_meta(), + job_filters_meta=build_report_job_filters_meta(), is_edit=False, initial_report=None, ) @@ -114,6 +116,7 @@ def reports_edit(report_id: int): "main/reports_new.html", initial_customers=customer_items, columns_meta=build_report_columns_meta(), + job_filters_meta=build_report_job_filters_meta(), is_edit=True, initial_report=_build_report_item(r), ) diff --git a/containers/backupchecks/src/templates/main/reports_new.html b/containers/backupchecks/src/templates/main/reports_new.html index c622191..5fe7726 100644 --- a/containers/backupchecks/src/templates/main/reports_new.html +++ b/containers/backupchecks/src/templates/main/reports_new.html @@ -118,10 +118,32 @@