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"])
|
@main_bp.route("/api/reports/columns", methods=["GET"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_reports_columns():
|
def api_reports_columns():
|
||||||
@ -403,6 +462,26 @@ def api_reports_generate(report_id: int):
|
|||||||
# run_object_links.run_id -> job_runs -> jobs
|
# run_object_links.run_id -> job_runs -> jobs
|
||||||
where_customer = ""
|
where_customer = ""
|
||||||
params = {"rid": report_id, "start_ts": report.period_start, "end_ts": report.period_end}
|
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:
|
if scope in ("single", "multiple") and customer_ids:
|
||||||
where_customer = " AND j.customer_id = ANY(:customer_ids) "
|
where_customer = " AND j.customer_id = ANY(:customer_ids) "
|
||||||
params["customer_ids"] = 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 >= :start_ts
|
||||||
AND jr.run_at < :end_ts
|
AND jr.run_at < :end_ts
|
||||||
{where_customer}
|
{where_customer}
|
||||||
|
{where_filters}
|
||||||
'''
|
'''
|
||||||
),
|
),
|
||||||
params,
|
params,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from .routes_shared import * # noqa: F401,F403
|
from .routes_shared import * # noqa: F401,F403
|
||||||
from datetime import date, timedelta
|
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():
|
def get_default_report_period():
|
||||||
"""Return default report period (last 7 days)."""
|
"""Return default report period (last 7 days)."""
|
||||||
@ -67,6 +67,7 @@ def reports():
|
|||||||
"main/reports.html",
|
"main/reports.html",
|
||||||
initial_reports=items,
|
initial_reports=items,
|
||||||
columns_meta=build_report_columns_meta(),
|
columns_meta=build_report_columns_meta(),
|
||||||
|
job_filters_meta=build_report_job_filters_meta(),
|
||||||
default_period_start=period_start.isoformat(),
|
default_period_start=period_start.isoformat(),
|
||||||
default_period_end=period_end.isoformat(),
|
default_period_end=period_end.isoformat(),
|
||||||
)
|
)
|
||||||
@ -88,6 +89,7 @@ def reports_new():
|
|||||||
"main/reports_new.html",
|
"main/reports_new.html",
|
||||||
initial_customers=customer_items,
|
initial_customers=customer_items,
|
||||||
columns_meta=build_report_columns_meta(),
|
columns_meta=build_report_columns_meta(),
|
||||||
|
job_filters_meta=build_report_job_filters_meta(),
|
||||||
is_edit=False,
|
is_edit=False,
|
||||||
initial_report=None,
|
initial_report=None,
|
||||||
)
|
)
|
||||||
@ -114,6 +116,7 @@ def reports_edit(report_id: int):
|
|||||||
"main/reports_new.html",
|
"main/reports_new.html",
|
||||||
initial_customers=customer_items,
|
initial_customers=customer_items,
|
||||||
columns_meta=build_report_columns_meta(),
|
columns_meta=build_report_columns_meta(),
|
||||||
|
job_filters_meta=build_report_job_filters_meta(),
|
||||||
is_edit=True,
|
is_edit=True,
|
||||||
initial_report=_build_report_item(r),
|
initial_report=_build_report_item(r),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -118,10 +118,32 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="alert alert-info mb-0">
|
<div class="fw-semibold mb-1">Jobs filter</div>
|
||||||
Jobs selection is set to <span class="fw-semibold">all jobs for each selected customer</span> in this iteration.
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@ -198,6 +220,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.__reportColumnsMeta = {{ columns_meta|tojson }};
|
window.__reportColumnsMeta = {{ columns_meta|tojson }};
|
||||||
|
window.__jobFiltersMeta = {{ job_filters_meta|tojson }};
|
||||||
window.__initialCustomers = {{ initial_customers|tojson }};
|
window.__initialCustomers = {{ initial_customers|tojson }};
|
||||||
window.__isEdit = {{ 'true' if is_edit else 'false' }};
|
window.__isEdit = {{ 'true' if is_edit else 'false' }};
|
||||||
window.__initialReport = {{ initial_report|tojson }};
|
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) {
|
function colsHintText(viewKey) {
|
||||||
return viewKey === 'snapshot'
|
return viewKey === 'snapshot'
|
||||||
? 'Select columns for the snapshot view.'
|
? 'Select columns for the snapshot view.'
|
||||||
@ -676,7 +806,7 @@
|
|||||||
customer_ids: customerIds,
|
customer_ids: customerIds,
|
||||||
period_start: buildIso(qs('rep_start_date').value, qs('rep_start_time').value, '00:00'),
|
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'),
|
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;
|
if (!payload.description) delete payload.description;
|
||||||
|
|||||||
@ -144,6 +144,16 @@
|
|||||||
- Enabled toggling visibility of `run_id` and `remark` via the column selector.
|
- 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.
|
- 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
|
## v0.1.15
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user