Auto-commit local changes before build (2026-01-03 22:22:35)

This commit is contained in:
Ivo Oskamp 2026-01-03 22:22:35 +01:00
parent fdf8ab224f
commit 9571716344
5 changed files with 228 additions and 5 deletions

View File

@ -1 +1 @@
v20260103-16-reports-snapshot-columns-runid-remark-toggle
v20260103-17-reports-job-filters-exclude-info-jobs

View File

@ -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,

View File

@ -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),
)

View File

@ -118,10 +118,32 @@
</div>
<div class="col-12">
<div class="alert alert-info mb-0">
Jobs selection is set to <span class="fw-semibold">all jobs for each selected customer</span> in this iteration.
<div class="fw-semibold mb-1">Jobs filter</div>
<div class="text-muted small mb-3">
Filter which jobs are included in the report by backup software and backup type. Informational jobs (e.g. License Key) are always excluded.
</div>
</div>
<div class="col-12 col-lg-6">
<label class="form-label fw-semibold" for="rep_job_backup_software">Backup software</label>
<select class="form-select" id="rep_job_backup_software" multiple size="6">
{% for item in job_filters_meta.backup_softwares %}
<option value="{{ item.key }}">{{ item.label }}</option>
{% endfor %}
</select>
<div class="form-text">Leave empty to include all backup software.</div>
</div>
<div class="col-12 col-lg-6">
<label class="form-label fw-semibold" for="rep_job_backup_type">Backup type</label>
<select class="form-select" id="rep_job_backup_type" multiple size="8">
{% for item in job_filters_meta.backup_types %}
<option value="{{ item.key }}">{{ item.label }}</option>
{% endfor %}
</select>
<div class="form-text">Leave empty to include all backup types (except informational types).</div>
</div>
</div>
@ -198,6 +220,7 @@
<script>
window.__reportColumnsMeta = {{ columns_meta|tojson }};
window.__jobFiltersMeta = {{ job_filters_meta|tojson }};
window.__initialCustomers = {{ initial_customers|tojson }};
window.__isEdit = {{ 'true' if is_edit else 'false' }};
window.__initialReport = {{ initial_report|tojson }};
@ -235,6 +258,113 @@
};
}
// --- Job filters (backup software / backup type) ---
var jobFiltersMeta = window.__jobFiltersMeta || null;
var repJobFiltersSelected = { backup_softwares: [], backup_types: [] };
if (isEdit && initialReport && initialReport.report_config && initialReport.report_config.filters) {
var f = initialReport.report_config.filters;
repJobFiltersSelected = {
backup_softwares: Array.isArray(f.backup_softwares) ? f.backup_softwares.slice() : [],
backup_types: Array.isArray(f.backup_types) ? f.backup_types.slice() : [],
};
}
function getSelectedValues(selectEl) {
var out = [];
if (!selectEl) return out;
for (var i = 0; i < selectEl.options.length; i++) {
var opt = selectEl.options[i];
if (opt && opt.selected) out.push(opt.value);
}
return out;
}
function setSelectedValues(selectEl, values) {
if (!selectEl) return;
var set = {};
(values || []).forEach(function (v) { set[String(v)] = true; });
for (var i = 0; i < selectEl.options.length; i++) {
var opt = selectEl.options[i];
if (!opt) continue;
opt.selected = !!set[String(opt.value)];
}
}
function rebuildBackupTypeOptions(allowedTypes, preserveSelected) {
var sel = qs('rep_job_backup_type');
if (!sel || !jobFiltersMeta || !Array.isArray(jobFiltersMeta.backup_types)) return;
var currentSelected = preserveSelected ? getSelectedValues(sel) : [];
var allowedSet = null;
if (Array.isArray(allowedTypes) && allowedTypes.length) {
allowedSet = {};
allowedTypes.forEach(function (v) { allowedSet[String(v)] = true; });
}
// rebuild options
sel.innerHTML = '';
jobFiltersMeta.backup_types.forEach(function (item) {
if (!item || !item.key) return;
if (String(item.key).toLowerCase() === 'license key') return; // always excluded
if (allowedSet && !allowedSet[String(item.key)]) return;
var opt = document.createElement('option');
opt.value = item.key;
opt.textContent = item.label || item.key;
sel.appendChild(opt);
});
// restore selection (only if still exists)
var finalSelected = [];
currentSelected.forEach(function (v) {
for (var i = 0; i < sel.options.length; i++) {
if (sel.options[i].value === v) finalSelected.push(v);
}
});
setSelectedValues(sel, finalSelected);
}
function onBackupSoftwareChange() {
var swSel = qs('rep_job_backup_software');
var selected = getSelectedValues(swSel);
if (!jobFiltersMeta || !jobFiltersMeta.by_backup_software) {
rebuildBackupTypeOptions(null, true);
return;
}
// union types for selected software(s)
if (!selected.length) {
rebuildBackupTypeOptions(null, true);
return;
}
var union = {};
selected.forEach(function (sw) {
var arr = jobFiltersMeta.by_backup_software[sw];
if (Array.isArray(arr)) {
arr.forEach(function (t) { union[String(t)] = true; });
}
});
var allowed = Object.keys(union);
rebuildBackupTypeOptions(allowed, true);
}
// Initialize job filter selects
(function initJobFilters() {
var swSel = qs('rep_job_backup_software');
if (swSel) {
swSel.addEventListener('change', onBackupSoftwareChange);
setSelectedValues(swSel, repJobFiltersSelected.backup_softwares);
}
onBackupSoftwareChange();
var btSel = qs('rep_job_backup_type');
if (btSel) setSelectedValues(btSel, repJobFiltersSelected.backup_types);
})();
function colsHintText(viewKey) {
return viewKey === 'snapshot'
? 'Select columns for the snapshot view.'
@ -676,7 +806,7 @@
customer_ids: customerIds,
period_start: buildIso(qs('rep_start_date').value, qs('rep_start_time').value, '00:00'),
period_end: buildIso(qs('rep_end_date').value, qs('rep_end_time').value, '23:59'),
report_config: { columns: repColsSelected, columns_version: 1 }
report_config: { columns: repColsSelected, columns_version: 1, filters: { backup_softwares: getSelectedValues(qs('rep_job_backup_software')), backup_types: getSelectedValues(qs('rep_job_backup_type')), filters_version: 1 } }
};
if (!payload.description) delete payload.description;

View File

@ -144,6 +144,16 @@
- Enabled toggling visibility of `run_id` and `remark` via the column selector.
- Removed forced default visibility so these columns can be fully disabled by the user.
---
## v20260103-17-reports-job-filters-exclude-info-jobs
- Excluded informational jobs (such as License Key and other non-backup jobs) from all report outputs.
- Added configurable filters to reports to select which backup jobs are included.
- Added configurable filters to reports to select which backup types are included.
- Ensured selected job and type filters are consistently applied to snapshot and summary reports.
- Improved report relevance by limiting output to meaningful backup-related data only.
================================================================================================================================================
## v0.1.15