Add per-user Run Checks sort and filter preferences
This commit is contained in:
parent
e7ccff89ee
commit
2e6ff18878
@ -4,8 +4,8 @@ import calendar
|
||||
|
||||
from datetime import date, datetime, time, timedelta, timezone
|
||||
|
||||
from flask import jsonify, render_template, request, url_for
|
||||
from urllib.parse import urljoin
|
||||
from flask import flash, jsonify, redirect, render_template, request, url_for
|
||||
from urllib.parse import urlencode, urljoin
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy import and_, or_, func, text
|
||||
|
||||
@ -42,6 +42,64 @@ from ..ticketing_utils import link_open_internal_tickets_to_run
|
||||
|
||||
|
||||
AUTOTASK_TERMINAL_STATUS_IDS = {5}
|
||||
RUN_CHECKS_SORT_MODES = {"customer", "status"}
|
||||
RUN_CHECKS_STATUS_FILTER_VALUES = ("critical", "missed", "warning", "success_override", "success")
|
||||
|
||||
|
||||
def _parse_bool_flag(raw: str | None, default: bool = False) -> bool:
|
||||
if raw is None:
|
||||
return bool(default)
|
||||
return str(raw).strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _normalize_run_checks_sort_mode(raw: str | None) -> str:
|
||||
v = (raw or "").strip().lower()
|
||||
if v not in RUN_CHECKS_SORT_MODES:
|
||||
return "customer"
|
||||
return v
|
||||
|
||||
|
||||
def _normalize_run_checks_status_filters(values: list[str] | tuple[str, ...] | None) -> list[str]:
|
||||
selected = {(v or "").strip().lower() for v in (values or []) if (v or "").strip()}
|
||||
return [k for k in RUN_CHECKS_STATUS_FILTER_VALUES if k in selected]
|
||||
|
||||
|
||||
def _row_status_categories(status_counts: dict[str, int] | None) -> set[str]:
|
||||
counts = status_counts or {}
|
||||
|
||||
failed_count = int(counts.get("Failed", 0) or 0) + int(counts.get("Error", 0) or 0)
|
||||
warning_count = int(counts.get("Warning", 0) or 0)
|
||||
missed_count = int(counts.get("Missed", 0) or 0)
|
||||
success_override_count = int(counts.get("Success (override)", 0) or 0)
|
||||
success_count = int(counts.get("Success", 0) or 0)
|
||||
|
||||
cats: set[str] = set()
|
||||
if failed_count > 0:
|
||||
cats.add("critical")
|
||||
if missed_count > 0:
|
||||
cats.add("missed")
|
||||
if warning_count > 0:
|
||||
cats.add("warning")
|
||||
if success_override_count > 0:
|
||||
cats.add("success_override")
|
||||
if success_count > 0:
|
||||
cats.add("success")
|
||||
return cats
|
||||
|
||||
|
||||
def _row_primary_status_rank(status_counts: dict[str, int] | None) -> int:
|
||||
cats = _row_status_categories(status_counts)
|
||||
if "critical" in cats:
|
||||
return 0
|
||||
if "missed" in cats:
|
||||
return 1
|
||||
if "warning" in cats:
|
||||
return 2
|
||||
if "success_override" in cats:
|
||||
return 3
|
||||
if "success" in cats:
|
||||
return 4
|
||||
return 9
|
||||
|
||||
|
||||
def _is_hidden_3cx_non_backup(backup_software: str | None, backup_type: str | None) -> bool:
|
||||
@ -837,7 +895,35 @@ def _ensure_missed_runs_for_job(job: Job, start_from: date, end_inclusive: date)
|
||||
def run_checks_page():
|
||||
"""Run Checks page: list jobs that have runs to review (including generated missed runs)."""
|
||||
|
||||
q = (request.args.get("q") or "").strip()
|
||||
default_q = (getattr(current_user, "run_checks_filter_q", None) or "").strip()
|
||||
default_sort_mode = _normalize_run_checks_sort_mode(getattr(current_user, "run_checks_sort_mode", "customer"))
|
||||
default_status_filters = _normalize_run_checks_status_filters(
|
||||
(getattr(current_user, "run_checks_filter_statuses", "") or "").split(",")
|
||||
)
|
||||
default_has_ticket = bool(getattr(current_user, "run_checks_filter_has_ticket", False))
|
||||
default_has_remark = bool(getattr(current_user, "run_checks_filter_has_remark", False))
|
||||
|
||||
q_arg = request.args.get("q")
|
||||
q = (q_arg if q_arg is not None else default_q).strip()
|
||||
|
||||
sort_mode_arg = request.args.get("sort")
|
||||
sort_mode = _normalize_run_checks_sort_mode(sort_mode_arg if sort_mode_arg is not None else default_sort_mode)
|
||||
|
||||
status_args = [(x or "").strip().lower() for x in request.args.getlist("status") if (x or "").strip()]
|
||||
if status_args:
|
||||
selected_status_filters = _normalize_run_checks_status_filters(status_args)
|
||||
else:
|
||||
statuses_csv_arg = request.args.get("statuses")
|
||||
if statuses_csv_arg is not None:
|
||||
selected_status_filters = _normalize_run_checks_status_filters(statuses_csv_arg.split(","))
|
||||
else:
|
||||
selected_status_filters = default_status_filters
|
||||
|
||||
has_ticket_arg = request.args.get("has_ticket")
|
||||
has_ticket = _parse_bool_flag(has_ticket_arg, default=default_has_ticket)
|
||||
|
||||
has_remark_arg = request.args.get("has_remark")
|
||||
has_remark = _parse_bool_flag(has_remark_arg, default=default_has_remark)
|
||||
|
||||
def _patterns(raw: str) -> list[str]:
|
||||
out = []
|
||||
@ -1159,6 +1245,41 @@ def run_checks_page():
|
||||
}
|
||||
)
|
||||
|
||||
if selected_status_filters:
|
||||
selected_set = set(selected_status_filters)
|
||||
payload = [
|
||||
row for row in payload
|
||||
if bool(_row_status_categories(row.get("status_counts", {})) & selected_set)
|
||||
]
|
||||
|
||||
if has_ticket:
|
||||
payload = [row for row in payload if bool(row.get("has_active_ticket", False))]
|
||||
|
||||
if has_remark:
|
||||
payload = [row for row in payload if bool(row.get("has_active_remark", False))]
|
||||
|
||||
if sort_mode == "status":
|
||||
payload.sort(
|
||||
key=lambda row: (
|
||||
_row_primary_status_rank(row.get("status_counts", {})),
|
||||
str(row.get("customer_name") or "").lower(),
|
||||
str(row.get("backup_software") or "").lower(),
|
||||
str(row.get("backup_type") or "").lower(),
|
||||
str(row.get("job_name") or "").lower(),
|
||||
int(row.get("job_id") or 0),
|
||||
)
|
||||
)
|
||||
else:
|
||||
payload.sort(
|
||||
key=lambda row: (
|
||||
str(row.get("customer_name") or "").lower(),
|
||||
str(row.get("backup_software") or "").lower(),
|
||||
str(row.get("backup_type") or "").lower(),
|
||||
str(row.get("job_name") or "").lower(),
|
||||
int(row.get("job_id") or 0),
|
||||
)
|
||||
)
|
||||
|
||||
settings = _get_or_create_settings()
|
||||
autotask_enabled = bool(getattr(settings, "autotask_enabled", False))
|
||||
|
||||
@ -1169,9 +1290,54 @@ def run_checks_page():
|
||||
include_reviewed=include_reviewed,
|
||||
autotask_enabled=autotask_enabled,
|
||||
q=q,
|
||||
sort_mode=sort_mode,
|
||||
selected_status_filters=selected_status_filters,
|
||||
has_ticket=has_ticket,
|
||||
has_remark=has_remark,
|
||||
)
|
||||
|
||||
|
||||
@main_bp.route("/run-checks/preferences", methods=["POST"])
|
||||
@login_required
|
||||
@roles_required("admin", "operator")
|
||||
def run_checks_save_preferences():
|
||||
q = (request.form.get("q") or "").strip()[:255]
|
||||
sort_mode = _normalize_run_checks_sort_mode(request.form.get("sort"))
|
||||
selected_status_filters = _normalize_run_checks_status_filters(request.form.getlist("status"))
|
||||
has_ticket = _parse_bool_flag(request.form.get("has_ticket"), default=False)
|
||||
has_remark = _parse_bool_flag(request.form.get("has_remark"), default=False)
|
||||
|
||||
current_user.run_checks_sort_mode = sort_mode
|
||||
current_user.run_checks_filter_statuses = ",".join(selected_status_filters)
|
||||
current_user.run_checks_filter_has_ticket = has_ticket
|
||||
current_user.run_checks_filter_has_remark = has_remark
|
||||
current_user.run_checks_filter_q = q or None
|
||||
|
||||
db.session.commit()
|
||||
flash("Run Checks preferences saved.", "success")
|
||||
|
||||
include_reviewed = False
|
||||
if get_active_role() == "admin":
|
||||
include_reviewed = _parse_bool_flag(request.form.get("include_reviewed"), default=False)
|
||||
|
||||
params: list[tuple[str, str]] = [("sort", sort_mode)]
|
||||
if q:
|
||||
params.append(("q", q))
|
||||
for status in selected_status_filters:
|
||||
params.append(("status", status))
|
||||
if has_ticket:
|
||||
params.append(("has_ticket", "1"))
|
||||
if has_remark:
|
||||
params.append(("has_remark", "1"))
|
||||
if include_reviewed:
|
||||
params.append(("include_reviewed", "1"))
|
||||
|
||||
target = url_for("main.run_checks_page")
|
||||
if params:
|
||||
target = f"{target}?{urlencode(params, doseq=True)}"
|
||||
return redirect(target)
|
||||
|
||||
|
||||
@main_bp.route("/api/run-checks/details")
|
||||
@login_required
|
||||
@roles_required("admin", "operator")
|
||||
|
||||
@ -10,29 +10,74 @@ from .routes_shared import main_bp
|
||||
def user_settings():
|
||||
"""User self-service settings.
|
||||
|
||||
Currently allows the logged-in user to change their own password.
|
||||
Allows the logged-in user to manage password and Run Checks preferences.
|
||||
"""
|
||||
|
||||
def _parse_bool_flag(raw: str | None, default: bool = False) -> bool:
|
||||
if raw is None:
|
||||
return bool(default)
|
||||
return str(raw).strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
def _normalize_sort_mode(raw: str | None) -> str:
|
||||
v = (raw or "").strip().lower()
|
||||
return v if v in ("customer", "status") else "customer"
|
||||
|
||||
def _normalize_status_filters(values: list[str] | None) -> list[str]:
|
||||
allowed = ("critical", "missed", "warning", "success_override", "success")
|
||||
selected = {(x or "").strip().lower() for x in (values or []) if (x or "").strip()}
|
||||
return [k for k in allowed if k in selected]
|
||||
|
||||
def _prefs_payload():
|
||||
selected = [x.strip().lower() for x in (current_user.run_checks_filter_statuses or "").split(",") if x.strip()]
|
||||
return {
|
||||
"run_checks_sort_mode": _normalize_sort_mode(current_user.run_checks_sort_mode),
|
||||
"run_checks_selected_statuses": _normalize_status_filters(selected),
|
||||
"run_checks_filter_has_ticket": bool(current_user.run_checks_filter_has_ticket),
|
||||
"run_checks_filter_has_remark": bool(current_user.run_checks_filter_has_remark),
|
||||
"run_checks_filter_q": (current_user.run_checks_filter_q or ""),
|
||||
}
|
||||
|
||||
if request.method == "POST":
|
||||
form_name = (request.form.get("form_name") or "password").strip().lower()
|
||||
|
||||
if form_name == "run_checks_preferences":
|
||||
current_user.run_checks_sort_mode = _normalize_sort_mode(request.form.get("run_checks_sort_mode"))
|
||||
current_user.run_checks_filter_statuses = ",".join(
|
||||
_normalize_status_filters(request.form.getlist("run_checks_status"))
|
||||
)
|
||||
current_user.run_checks_filter_has_ticket = _parse_bool_flag(
|
||||
request.form.get("run_checks_filter_has_ticket"),
|
||||
default=False,
|
||||
)
|
||||
current_user.run_checks_filter_has_remark = _parse_bool_flag(
|
||||
request.form.get("run_checks_filter_has_remark"),
|
||||
default=False,
|
||||
)
|
||||
q = (request.form.get("run_checks_filter_q") or "").strip()[:255]
|
||||
current_user.run_checks_filter_q = q or None
|
||||
db.session.commit()
|
||||
flash("Run Checks preferences updated.", "success")
|
||||
return redirect(url_for("main.user_settings"))
|
||||
|
||||
current_password = request.form.get("current_password") or ""
|
||||
new_password = (request.form.get("new_password") or "").strip()
|
||||
confirm_password = (request.form.get("confirm_password") or "").strip()
|
||||
|
||||
if not current_user.check_password(current_password):
|
||||
flash("Current password is incorrect.", "danger")
|
||||
return render_template("main/user_settings.html")
|
||||
return render_template("main/user_settings.html", **_prefs_payload())
|
||||
|
||||
if not new_password:
|
||||
flash("New password is required.", "danger")
|
||||
return render_template("main/user_settings.html")
|
||||
return render_template("main/user_settings.html", **_prefs_payload())
|
||||
|
||||
if new_password != confirm_password:
|
||||
flash("Passwords do not match.", "danger")
|
||||
return render_template("main/user_settings.html")
|
||||
return render_template("main/user_settings.html", **_prefs_payload())
|
||||
|
||||
current_user.set_password(new_password)
|
||||
db.session.commit()
|
||||
flash("Password updated.", "success")
|
||||
return redirect(url_for("main.user_settings"))
|
||||
|
||||
return render_template("main/user_settings.html")
|
||||
return render_template("main/user_settings.html", **_prefs_payload())
|
||||
|
||||
@ -715,6 +715,71 @@ def migrate_users_theme_preference() -> None:
|
||||
print("[migrations] migrate_users_theme_preference completed.")
|
||||
|
||||
|
||||
def migrate_users_run_checks_preferences() -> None:
|
||||
"""Add Run Checks preference columns to users if missing."""
|
||||
table = "users"
|
||||
columns = [
|
||||
("run_checks_sort_mode", "VARCHAR(32) NOT NULL DEFAULT 'customer'"),
|
||||
("run_checks_filter_statuses", "TEXT NOT NULL DEFAULT ''"),
|
||||
("run_checks_filter_has_ticket", "BOOLEAN NOT NULL DEFAULT FALSE"),
|
||||
("run_checks_filter_has_remark", "BOOLEAN NOT NULL DEFAULT FALSE"),
|
||||
("run_checks_filter_q", "VARCHAR(255) NULL"),
|
||||
]
|
||||
|
||||
try:
|
||||
engine = db.get_engine()
|
||||
except Exception as exc:
|
||||
print(f"[migrations] Could not get engine for users Run Checks preferences migration: {exc}")
|
||||
return
|
||||
|
||||
try:
|
||||
with engine.begin() as conn:
|
||||
for column, ddl in columns:
|
||||
if _column_exists_on_conn(conn, table, column):
|
||||
continue
|
||||
conn.execute(text(f'ALTER TABLE "{table}" ADD COLUMN {column} {ddl}'))
|
||||
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE "users"
|
||||
SET run_checks_sort_mode = 'customer'
|
||||
WHERE run_checks_sort_mode IS NULL OR run_checks_sort_mode = '';
|
||||
"""
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE "users"
|
||||
SET run_checks_filter_statuses = ''
|
||||
WHERE run_checks_filter_statuses IS NULL;
|
||||
"""
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE "users"
|
||||
SET run_checks_filter_has_ticket = FALSE
|
||||
WHERE run_checks_filter_has_ticket IS NULL;
|
||||
"""
|
||||
)
|
||||
)
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE "users"
|
||||
SET run_checks_filter_has_remark = FALSE
|
||||
WHERE run_checks_filter_has_remark IS NULL;
|
||||
"""
|
||||
)
|
||||
)
|
||||
print("[migrations] migrate_users_run_checks_preferences completed.")
|
||||
except Exception as exc:
|
||||
print(f"[migrations] Failed to migrate users Run Checks preferences: {exc}")
|
||||
|
||||
|
||||
def migrate_system_settings_eml_retention() -> None:
|
||||
"""Add ingest_eml_retention_days to system_settings if missing.
|
||||
|
||||
@ -1210,6 +1275,7 @@ def run_migrations() -> None:
|
||||
migrate_add_username_to_users()
|
||||
migrate_make_email_nullable()
|
||||
migrate_users_theme_preference()
|
||||
migrate_users_run_checks_preferences()
|
||||
migrate_system_settings_eml_retention()
|
||||
migrate_system_settings_auto_import_cutoff_date()
|
||||
migrate_system_settings_daily_jobs_start_date()
|
||||
|
||||
@ -21,6 +21,12 @@ class User(db.Model, UserMixin):
|
||||
role = db.Column(db.String(50), nullable=False, default="viewer")
|
||||
# UI theme preference: 'auto' (follow OS), 'light', 'dark'
|
||||
theme_preference = db.Column(db.String(16), nullable=False, default="auto")
|
||||
# Run Checks user preferences
|
||||
run_checks_sort_mode = db.Column(db.String(32), nullable=False, default="customer")
|
||||
run_checks_filter_statuses = db.Column(db.Text, nullable=False, default="")
|
||||
run_checks_filter_has_ticket = db.Column(db.Boolean, nullable=False, default=False)
|
||||
run_checks_filter_has_remark = db.Column(db.Boolean, nullable=False, default=False)
|
||||
run_checks_filter_q = db.Column(db.String(255), nullable=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
def set_password(self, password: str) -> None:
|
||||
|
||||
@ -41,6 +41,81 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="border rounded p-3 mb-3">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-12 col-lg-4">
|
||||
<label class="form-label mb-1" for="rc_filter_q">Search</label>
|
||||
<input
|
||||
class="form-control form-control-sm"
|
||||
id="rc_filter_q"
|
||||
type="text"
|
||||
name="q"
|
||||
value="{{ q or '' }}"
|
||||
placeholder="Customer, backup software, type, job name"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-3">
|
||||
<label class="form-label mb-1" for="rc_filter_sort">Sort order</label>
|
||||
<select class="form-select form-select-sm" id="rc_filter_sort" name="sort">
|
||||
<option value="customer" {% if sort_mode == 'customer' %}selected{% endif %}>Customer > Backup > Type > Job</option>
|
||||
<option value="status" {% if sort_mode == 'status' %}selected{% endif %}>Critical > Missed > Warning > Success (override) > Success</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-lg-5">
|
||||
<label class="form-label mb-1 d-block">Status filter</label>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<div class="form-check form-check-inline m-0">
|
||||
<input class="form-check-input" type="checkbox" id="rc_status_critical" name="status" value="critical" {% if 'critical' in selected_status_filters %}checked{% endif %}>
|
||||
<label class="form-check-label" for="rc_status_critical">Critical</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline m-0">
|
||||
<input class="form-check-input" type="checkbox" id="rc_status_missed" name="status" value="missed" {% if 'missed' in selected_status_filters %}checked{% endif %}>
|
||||
<label class="form-check-label" for="rc_status_missed">Missed</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline m-0">
|
||||
<input class="form-check-input" type="checkbox" id="rc_status_warning" name="status" value="warning" {% if 'warning' in selected_status_filters %}checked{% endif %}>
|
||||
<label class="form-check-label" for="rc_status_warning">Warning</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline m-0">
|
||||
<input class="form-check-input" type="checkbox" id="rc_status_success_override" name="status" value="success_override" {% if 'success_override' in selected_status_filters %}checked{% endif %}>
|
||||
<label class="form-check-label" for="rc_status_success_override">Success (override)</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline m-0">
|
||||
<input class="form-check-input" type="checkbox" id="rc_status_success" name="status" value="success" {% if 'success' in selected_status_filters %}checked{% endif %}>
|
||||
<label class="form-check-label" for="rc_status_success">Success</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="rc_has_ticket" name="has_ticket" value="1" {% if has_ticket %}checked{% endif %}>
|
||||
<label class="form-check-label" for="rc_has_ticket">Only jobs with active ticket</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="rc_has_remark" name="has_remark" value="1" {% if has_remark %}checked{% endif %}>
|
||||
<label class="form-check-label" for="rc_has_remark">Only jobs with active remark</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-5 d-flex gap-2 justify-content-lg-end">
|
||||
{% if is_admin and include_reviewed %}
|
||||
<input type="hidden" name="include_reviewed" value="1" />
|
||||
{% endif %}
|
||||
<button type="submit" class="btn btn-sm btn-primary">Apply</button>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('main.run_checks_page') }}">Reset</a>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
formmethod="post"
|
||||
formaction="{{ url_for('main.run_checks_save_preferences') }}"
|
||||
>
|
||||
Save as my default
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="small text-muted mb-2" id="rc_status"></div>
|
||||
|
||||
<div class="table-responsive">
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
<div class="card-body">
|
||||
<h2 class="h6">Change password</h2>
|
||||
<form method="post" class="row g-3">
|
||||
<input type="hidden" name="form_name" value="password" />
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="current_password">Current password</label>
|
||||
<input class="form-control" type="password" id="current_password" name="current_password" autocomplete="current-password" required />
|
||||
@ -30,4 +31,77 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3" style="max-width: 50rem;">
|
||||
<div class="card-body">
|
||||
<h2 class="h6">Run Checks preferences</h2>
|
||||
<form method="post" class="row g-3">
|
||||
<input type="hidden" name="form_name" value="run_checks_preferences" />
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="run_checks_filter_q">Default search filter</label>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
id="run_checks_filter_q"
|
||||
name="run_checks_filter_q"
|
||||
value="{{ run_checks_filter_q or '' }}"
|
||||
maxlength="255"
|
||||
placeholder="Customer, backup software, type, job name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-8">
|
||||
<label class="form-label" for="run_checks_sort_mode">Default sort order</label>
|
||||
<select class="form-select" id="run_checks_sort_mode" name="run_checks_sort_mode">
|
||||
<option value="customer" {% if run_checks_sort_mode == 'customer' %}selected{% endif %}>Customer > Backup > Type > Job</option>
|
||||
<option value="status" {% if run_checks_sort_mode == 'status' %}selected{% endif %}>Critical > Missed > Warning > Success (override) > Success</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label d-block">Default status filter</label>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="run_checks_status_critical" name="run_checks_status" value="critical" {% if 'critical' in run_checks_selected_statuses %}checked{% endif %}>
|
||||
<label class="form-check-label" for="run_checks_status_critical">Critical</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="run_checks_status_missed" name="run_checks_status" value="missed" {% if 'missed' in run_checks_selected_statuses %}checked{% endif %}>
|
||||
<label class="form-check-label" for="run_checks_status_missed">Missed</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="run_checks_status_warning" name="run_checks_status" value="warning" {% if 'warning' in run_checks_selected_statuses %}checked{% endif %}>
|
||||
<label class="form-check-label" for="run_checks_status_warning">Warning</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="run_checks_status_success_override" name="run_checks_status" value="success_override" {% if 'success_override' in run_checks_selected_statuses %}checked{% endif %}>
|
||||
<label class="form-check-label" for="run_checks_status_success_override">Success (override)</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="run_checks_status_success" name="run_checks_status" value="success" {% if 'success' in run_checks_selected_statuses %}checked{% endif %}>
|
||||
<label class="form-check-label" for="run_checks_status_success">Success</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="d-flex flex-wrap gap-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="run_checks_filter_has_ticket" name="run_checks_filter_has_ticket" value="1" {% if run_checks_filter_has_ticket %}checked{% endif %}>
|
||||
<label class="form-check-label" for="run_checks_filter_has_ticket">Only jobs with active ticket</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="run_checks_filter_has_remark" name="run_checks_filter_has_remark" value="1" {% if run_checks_filter_has_remark %}checked{% endif %}>
|
||||
<label class="form-check-label" for="run_checks_filter_has_remark">Only jobs with active remark</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<button class="btn btn-primary" type="submit">Save Run Checks preferences</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -2,6 +2,30 @@
|
||||
|
||||
This file documents all changes made to this project via Claude Code.
|
||||
|
||||
## [2026-02-27]
|
||||
|
||||
### Added
|
||||
- Run Checks user preferences (per user) stored on `users`:
|
||||
- `run_checks_sort_mode`
|
||||
- `run_checks_filter_statuses`
|
||||
- `run_checks_filter_has_ticket`
|
||||
- `run_checks_filter_has_remark`
|
||||
- `run_checks_filter_q`
|
||||
- DB migration `migrate_users_run_checks_preferences()` for the new user preference columns.
|
||||
- New route `POST /run-checks/preferences` to save current Run Checks filter/sort controls as user defaults.
|
||||
- User Settings page now includes a dedicated "Run Checks preferences" section (next to password self-service).
|
||||
|
||||
### Changed
|
||||
- Run Checks now supports user-configurable sorting:
|
||||
- default: Customer > Backup > Type > Job (existing behavior)
|
||||
- optional: `Critical > Missed > Warning > Success (override) > Success`
|
||||
- Run Checks now supports user-configurable filtering:
|
||||
- status filter (`Critical`, `Missed`, `Warning`, `Success (override)`, `Success`)
|
||||
- only jobs with active ticket
|
||||
- only jobs with active remark
|
||||
- search text
|
||||
- Run Checks page loads with the logged-in user's saved defaults when no explicit query parameters are provided.
|
||||
|
||||
## [2026-02-23]
|
||||
|
||||
### Added
|
||||
|
||||
Loading…
Reference in New Issue
Block a user