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 datetime import date, datetime, time, timedelta, timezone
|
||||||
|
|
||||||
from flask import jsonify, render_template, request, url_for
|
from flask import flash, jsonify, redirect, render_template, request, url_for
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urlencode, urljoin
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from sqlalchemy import and_, or_, func, text
|
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}
|
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:
|
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():
|
def run_checks_page():
|
||||||
"""Run Checks page: list jobs that have runs to review (including generated missed runs)."""
|
"""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]:
|
def _patterns(raw: str) -> list[str]:
|
||||||
out = []
|
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()
|
settings = _get_or_create_settings()
|
||||||
autotask_enabled = bool(getattr(settings, "autotask_enabled", False))
|
autotask_enabled = bool(getattr(settings, "autotask_enabled", False))
|
||||||
|
|
||||||
@ -1169,9 +1290,54 @@ def run_checks_page():
|
|||||||
include_reviewed=include_reviewed,
|
include_reviewed=include_reviewed,
|
||||||
autotask_enabled=autotask_enabled,
|
autotask_enabled=autotask_enabled,
|
||||||
q=q,
|
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")
|
@main_bp.route("/api/run-checks/details")
|
||||||
@login_required
|
@login_required
|
||||||
@roles_required("admin", "operator")
|
@roles_required("admin", "operator")
|
||||||
|
|||||||
@ -10,29 +10,74 @@ from .routes_shared import main_bp
|
|||||||
def user_settings():
|
def user_settings():
|
||||||
"""User self-service 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":
|
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 ""
|
current_password = request.form.get("current_password") or ""
|
||||||
new_password = (request.form.get("new_password") or "").strip()
|
new_password = (request.form.get("new_password") or "").strip()
|
||||||
confirm_password = (request.form.get("confirm_password") or "").strip()
|
confirm_password = (request.form.get("confirm_password") or "").strip()
|
||||||
|
|
||||||
if not current_user.check_password(current_password):
|
if not current_user.check_password(current_password):
|
||||||
flash("Current password is incorrect.", "danger")
|
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:
|
if not new_password:
|
||||||
flash("New password is required.", "danger")
|
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:
|
if new_password != confirm_password:
|
||||||
flash("Passwords do not match.", "danger")
|
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)
|
current_user.set_password(new_password)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Password updated.", "success")
|
flash("Password updated.", "success")
|
||||||
return redirect(url_for("main.user_settings"))
|
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.")
|
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:
|
def migrate_system_settings_eml_retention() -> None:
|
||||||
"""Add ingest_eml_retention_days to system_settings if missing.
|
"""Add ingest_eml_retention_days to system_settings if missing.
|
||||||
|
|
||||||
@ -1210,6 +1275,7 @@ def run_migrations() -> None:
|
|||||||
migrate_add_username_to_users()
|
migrate_add_username_to_users()
|
||||||
migrate_make_email_nullable()
|
migrate_make_email_nullable()
|
||||||
migrate_users_theme_preference()
|
migrate_users_theme_preference()
|
||||||
|
migrate_users_run_checks_preferences()
|
||||||
migrate_system_settings_eml_retention()
|
migrate_system_settings_eml_retention()
|
||||||
migrate_system_settings_auto_import_cutoff_date()
|
migrate_system_settings_auto_import_cutoff_date()
|
||||||
migrate_system_settings_daily_jobs_start_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")
|
role = db.Column(db.String(50), nullable=False, default="viewer")
|
||||||
# UI theme preference: 'auto' (follow OS), 'light', 'dark'
|
# UI theme preference: 'auto' (follow OS), 'light', 'dark'
|
||||||
theme_preference = db.Column(db.String(16), nullable=False, default="auto")
|
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)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
def set_password(self, password: str) -> None:
|
def set_password(self, password: str) -> None:
|
||||||
|
|||||||
@ -41,6 +41,81 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="small text-muted mb-2" id="rc_status"></div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@ -1619,4 +1694,4 @@ table.addEventListener('change', function (e) {
|
|||||||
updateButtons();
|
updateButtons();
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="h6">Change password</h2>
|
<h2 class="h6">Change password</h2>
|
||||||
<form method="post" class="row g-3">
|
<form method="post" class="row g-3">
|
||||||
|
<input type="hidden" name="form_name" value="password" />
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label" for="current_password">Current password</label>
|
<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 />
|
<input class="form-control" type="password" id="current_password" name="current_password" autocomplete="current-password" required />
|
||||||
@ -30,4 +31,77 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@ -2,6 +2,30 @@
|
|||||||
|
|
||||||
This file documents all changes made to this project via Claude Code.
|
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]
|
## [2026-02-23]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user