Auto-commit local changes before build (2026-01-03 22:22:35)
This commit is contained in:
parent
fdf8ab224f
commit
9571716344
@ -1 +1 @@
|
||||
v20260103-16-reports-snapshot-columns-runid-remark-toggle
|
||||
v20260103-17-reports-job-filters-exclude-info-jobs
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user