Forward global search filters to overview pages
This commit is contained in:
parent
f90b2bdcf6
commit
b46010dbc2
@ -63,7 +63,27 @@ def _get_or_create_settings_local():
|
|||||||
@login_required
|
@login_required
|
||||||
@roles_required("admin", "operator", "viewer")
|
@roles_required("admin", "operator", "viewer")
|
||||||
def customers():
|
def customers():
|
||||||
items = Customer.query.order_by(Customer.name.asc()).all()
|
q = (request.args.get("q") or "").strip()
|
||||||
|
|
||||||
|
def _patterns(raw: str) -> list[str]:
|
||||||
|
out = []
|
||||||
|
for tok in [t.strip() for t in (raw or "").split() if t.strip()]:
|
||||||
|
p = tok.replace("\\", "\\\\")
|
||||||
|
p = p.replace("%", "\\%").replace("_", "\\_")
|
||||||
|
p = p.replace("*", "%")
|
||||||
|
if not p.startswith("%"):
|
||||||
|
p = "%" + p
|
||||||
|
if not p.endswith("%"):
|
||||||
|
p = p + "%"
|
||||||
|
out.append(p)
|
||||||
|
return out
|
||||||
|
|
||||||
|
query = Customer.query
|
||||||
|
if q:
|
||||||
|
for pat in _patterns(q):
|
||||||
|
query = query.filter(func.coalesce(Customer.name, "").ilike(pat, escape="\\"))
|
||||||
|
|
||||||
|
items = query.order_by(Customer.name.asc()).all()
|
||||||
|
|
||||||
settings = _get_or_create_settings_local()
|
settings = _get_or_create_settings_local()
|
||||||
autotask_enabled = bool(getattr(settings, "autotask_enabled", False))
|
autotask_enabled = bool(getattr(settings, "autotask_enabled", False))
|
||||||
@ -105,6 +125,7 @@ def customers():
|
|||||||
can_manage=can_manage,
|
can_manage=can_manage,
|
||||||
autotask_enabled=autotask_enabled,
|
autotask_enabled=autotask_enabled,
|
||||||
autotask_configured=autotask_configured,
|
autotask_configured=autotask_configured,
|
||||||
|
q=q,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -600,4 +621,3 @@ def customers_import():
|
|||||||
|
|
||||||
return redirect(url_for("main.customers"))
|
return redirect(url_for("main.customers"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,21 @@ MISSED_GRACE_WINDOW = timedelta(hours=1)
|
|||||||
@login_required
|
@login_required
|
||||||
@roles_required("admin", "operator", "viewer")
|
@roles_required("admin", "operator", "viewer")
|
||||||
def daily_jobs():
|
def daily_jobs():
|
||||||
|
q = (request.args.get("q") or "").strip()
|
||||||
|
|
||||||
|
def _patterns(raw: str) -> list[str]:
|
||||||
|
out = []
|
||||||
|
for tok in [t.strip() for t in (raw or "").split() if t.strip()]:
|
||||||
|
p = tok.replace("\\", "\\\\")
|
||||||
|
p = p.replace("%", "\\%").replace("_", "\\_")
|
||||||
|
p = p.replace("*", "%")
|
||||||
|
if not p.startswith("%"):
|
||||||
|
p = "%" + p
|
||||||
|
if not p.endswith("%"):
|
||||||
|
p = p + "%"
|
||||||
|
out.append(p)
|
||||||
|
return out
|
||||||
|
|
||||||
# Determine target date (default: today) in Europe/Amsterdam
|
# Determine target date (default: today) in Europe/Amsterdam
|
||||||
date_str = request.args.get("date")
|
date_str = request.args.get("date")
|
||||||
try:
|
try:
|
||||||
@ -74,10 +89,21 @@ def daily_jobs():
|
|||||||
|
|
||||||
weekday_idx = target_date.weekday() # 0=Mon..6=Sun
|
weekday_idx = target_date.weekday() # 0=Mon..6=Sun
|
||||||
|
|
||||||
jobs = (
|
jobs_query = (
|
||||||
Job.query.join(Customer, isouter=True)
|
Job.query.join(Customer, isouter=True)
|
||||||
.filter(Job.archived.is_(False))
|
.filter(Job.archived.is_(False))
|
||||||
.filter(db.or_(Customer.id.is_(None), Customer.active.is_(True)))
|
.filter(db.or_(Customer.id.is_(None), Customer.active.is_(True)))
|
||||||
|
)
|
||||||
|
if q:
|
||||||
|
for pat in _patterns(q):
|
||||||
|
jobs_query = jobs_query.filter(
|
||||||
|
(func.coalesce(Customer.name, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(Job.backup_software, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(Job.backup_type, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(Job.job_name, "").ilike(pat, escape="\\"))
|
||||||
|
)
|
||||||
|
jobs = (
|
||||||
|
jobs_query
|
||||||
.order_by(Customer.name.asc().nullslast(), Job.backup_software.asc(), Job.backup_type.asc(), Job.job_name.asc())
|
.order_by(Customer.name.asc().nullslast(), Job.backup_software.asc(), Job.backup_type.asc(), Job.job_name.asc())
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
@ -306,7 +332,7 @@ def daily_jobs():
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_date_str = target_date.strftime("%Y-%m-%d")
|
target_date_str = target_date.strftime("%Y-%m-%d")
|
||||||
return render_template("main/daily_jobs.html", rows=rows, target_date_str=target_date_str)
|
return render_template("main/daily_jobs.html", rows=rows, target_date_str=target_date_str, q=q)
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route("/daily-jobs/details")
|
@main_bp.route("/daily-jobs/details")
|
||||||
|
|||||||
@ -9,12 +9,28 @@ from ..ticketing_utils import link_open_internal_tickets_to_run
|
|||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import html as _html
|
import html as _html
|
||||||
|
from sqlalchemy import cast, String
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route("/inbox")
|
@main_bp.route("/inbox")
|
||||||
@login_required
|
@login_required
|
||||||
@roles_required("admin", "operator", "viewer")
|
@roles_required("admin", "operator", "viewer")
|
||||||
def inbox():
|
def inbox():
|
||||||
|
q = (request.args.get("q") or "").strip()
|
||||||
|
|
||||||
|
def _patterns(raw: str) -> list[str]:
|
||||||
|
out = []
|
||||||
|
for tok in [t.strip() for t in (raw or "").split() if t.strip()]:
|
||||||
|
p = tok.replace("\\", "\\\\")
|
||||||
|
p = p.replace("%", "\\%").replace("_", "\\_")
|
||||||
|
p = p.replace("*", "%")
|
||||||
|
if not p.startswith("%"):
|
||||||
|
p = "%" + p
|
||||||
|
if not p.endswith("%"):
|
||||||
|
p = p + "%"
|
||||||
|
out.append(p)
|
||||||
|
return out
|
||||||
|
|
||||||
try:
|
try:
|
||||||
page = int(request.args.get("page", "1"))
|
page = int(request.args.get("page", "1"))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -28,6 +44,18 @@ def inbox():
|
|||||||
# Use location column if available; otherwise just return all
|
# Use location column if available; otherwise just return all
|
||||||
if hasattr(MailMessage, "location"):
|
if hasattr(MailMessage, "location"):
|
||||||
query = query.filter(MailMessage.location == "inbox")
|
query = query.filter(MailMessage.location == "inbox")
|
||||||
|
if q:
|
||||||
|
for pat in _patterns(q):
|
||||||
|
query = query.filter(
|
||||||
|
(func.coalesce(MailMessage.from_address, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(MailMessage.subject, "").ilike(pat, escape="\\"))
|
||||||
|
| (cast(MailMessage.received_at, String).ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(MailMessage.backup_software, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(MailMessage.backup_type, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(MailMessage.job_name, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(MailMessage.parse_result, "").ilike(pat, escape="\\"))
|
||||||
|
| (cast(MailMessage.parsed_at, String).ilike(pat, escape="\\"))
|
||||||
|
)
|
||||||
|
|
||||||
total_items = query.count()
|
total_items = query.count()
|
||||||
total_pages = max(1, math.ceil(total_items / per_page)) if total_items else 1
|
total_pages = max(1, math.ceil(total_items / per_page)) if total_items else 1
|
||||||
@ -79,6 +107,7 @@ def inbox():
|
|||||||
customers=customer_rows,
|
customers=customer_rows,
|
||||||
can_bulk_delete=(get_active_role() in ("admin", "operator")),
|
can_bulk_delete=(get_active_role() in ("admin", "operator")),
|
||||||
is_admin=(get_active_role() == "admin"),
|
is_admin=(get_active_role() == "admin"),
|
||||||
|
q=q,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1320,4 +1349,4 @@ def inbox_reparse_all():
|
|||||||
"info",
|
"info",
|
||||||
)
|
)
|
||||||
|
|
||||||
return redirect(url_for("main.inbox"))
|
return redirect(url_for("main.inbox"))
|
||||||
|
|||||||
@ -15,6 +15,7 @@ from .routes_shared import (
|
|||||||
def jobs():
|
def jobs():
|
||||||
selected_customer_id = None
|
selected_customer_id = None
|
||||||
selected_customer_name = ""
|
selected_customer_name = ""
|
||||||
|
q = (request.args.get("q") or "").strip()
|
||||||
customer_id_raw = (request.args.get("customer_id") or "").strip()
|
customer_id_raw = (request.args.get("customer_id") or "").strip()
|
||||||
if customer_id_raw:
|
if customer_id_raw:
|
||||||
try:
|
try:
|
||||||
@ -22,6 +23,19 @@ def jobs():
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
selected_customer_id = None
|
selected_customer_id = None
|
||||||
|
|
||||||
|
def _patterns(raw: str) -> list[str]:
|
||||||
|
out = []
|
||||||
|
for tok in [t.strip() for t in (raw or "").split() if t.strip()]:
|
||||||
|
p = tok.replace("\\", "\\\\")
|
||||||
|
p = p.replace("%", "\\%").replace("_", "\\_")
|
||||||
|
p = p.replace("*", "%")
|
||||||
|
if not p.startswith("%"):
|
||||||
|
p = "%" + p
|
||||||
|
if not p.endswith("%"):
|
||||||
|
p = p + "%"
|
||||||
|
out.append(p)
|
||||||
|
return out
|
||||||
|
|
||||||
base_query = (
|
base_query = (
|
||||||
Job.query
|
Job.query
|
||||||
.filter(Job.archived.is_(False))
|
.filter(Job.archived.is_(False))
|
||||||
@ -37,6 +51,15 @@ def jobs():
|
|||||||
# Default listing hides jobs for inactive customers.
|
# Default listing hides jobs for inactive customers.
|
||||||
base_query = base_query.filter(db.or_(Customer.id.is_(None), Customer.active.is_(True)))
|
base_query = base_query.filter(db.or_(Customer.id.is_(None), Customer.active.is_(True)))
|
||||||
|
|
||||||
|
if q:
|
||||||
|
for pat in _patterns(q):
|
||||||
|
base_query = base_query.filter(
|
||||||
|
(func.coalesce(Customer.name, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(Job.backup_software, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(Job.backup_type, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(Job.job_name, "").ilike(pat, escape="\\"))
|
||||||
|
)
|
||||||
|
|
||||||
# Join with customers for display
|
# Join with customers for display
|
||||||
jobs = (
|
jobs = (
|
||||||
base_query
|
base_query
|
||||||
@ -77,6 +100,7 @@ def jobs():
|
|||||||
can_manage_jobs=can_manage_jobs,
|
can_manage_jobs=can_manage_jobs,
|
||||||
selected_customer_id=selected_customer_id,
|
selected_customer_id=selected_customer_id,
|
||||||
selected_customer_name=selected_customer_name,
|
selected_customer_name=selected_customer_name,
|
||||||
|
q=q,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,16 @@ _OVERRIDE_DEFAULT_START_AT = datetime(1970, 1, 1)
|
|||||||
def overrides():
|
def overrides():
|
||||||
can_manage = get_active_role() in ("admin", "operator")
|
can_manage = get_active_role() in ("admin", "operator")
|
||||||
can_delete = get_active_role() == "admin"
|
can_delete = get_active_role() == "admin"
|
||||||
|
q = (request.args.get("q") or "").strip()
|
||||||
|
|
||||||
|
def _match_query(text: str, raw_query: str) -> bool:
|
||||||
|
hay = (text or "").lower()
|
||||||
|
tokens = [t.strip() for t in (raw_query or "").split() if t.strip()]
|
||||||
|
for tok in tokens:
|
||||||
|
needle = tok.lower().replace("*", "")
|
||||||
|
if needle and needle not in hay:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
overrides_q = Override.query.order_by(Override.level.asc(), Override.start_at.desc()).all()
|
overrides_q = Override.query.order_by(Override.level.asc(), Override.start_at.desc()).all()
|
||||||
|
|
||||||
@ -92,16 +102,31 @@ def overrides():
|
|||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
for ov in overrides_q:
|
for ov in overrides_q:
|
||||||
|
scope_text = _describe_scope(ov)
|
||||||
|
start_text = _format_datetime(ov.start_at)
|
||||||
|
end_text = _format_datetime(ov.end_at) if ov.end_at else ""
|
||||||
|
comment_text = ov.comment or ""
|
||||||
|
if q:
|
||||||
|
full_text = " | ".join([
|
||||||
|
ov.level or "",
|
||||||
|
scope_text,
|
||||||
|
start_text,
|
||||||
|
end_text,
|
||||||
|
comment_text,
|
||||||
|
])
|
||||||
|
if not _match_query(full_text, q):
|
||||||
|
continue
|
||||||
|
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
"id": ov.id,
|
"id": ov.id,
|
||||||
"level": ov.level or "",
|
"level": ov.level or "",
|
||||||
"scope": _describe_scope(ov),
|
"scope": scope_text,
|
||||||
"start_at": _format_datetime(ov.start_at),
|
"start_at": start_text,
|
||||||
"end_at": _format_datetime(ov.end_at) if ov.end_at else "",
|
"end_at": end_text,
|
||||||
"active": bool(ov.active),
|
"active": bool(ov.active),
|
||||||
"treat_as_success": bool(ov.treat_as_success),
|
"treat_as_success": bool(ov.treat_as_success),
|
||||||
"comment": ov.comment or "",
|
"comment": comment_text,
|
||||||
"match_status": ov.match_status or "",
|
"match_status": ov.match_status or "",
|
||||||
"match_error_contains": ov.match_error_contains or "",
|
"match_error_contains": ov.match_error_contains or "",
|
||||||
"match_error_mode": getattr(ov, "match_error_mode", None) or "",
|
"match_error_mode": getattr(ov, "match_error_mode", None) or "",
|
||||||
@ -122,6 +147,7 @@ def overrides():
|
|||||||
jobs_for_select=jobs_for_select,
|
jobs_for_select=jobs_for_select,
|
||||||
backup_software_options=backup_software_options,
|
backup_software_options=backup_software_options,
|
||||||
backup_type_options=backup_type_options,
|
backup_type_options=backup_type_options,
|
||||||
|
q=q,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -398,4 +424,3 @@ def overrides_toggle(override_id: int):
|
|||||||
|
|
||||||
flash("Override status updated.", "success")
|
flash("Override status updated.", "success")
|
||||||
return redirect(url_for("main.overrides"))
|
return redirect(url_for("main.overrides"))
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from .routes_shared import * # noqa: F401,F403
|
from .routes_shared import * # noqa: F401,F403
|
||||||
|
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text, cast, String
|
||||||
import json
|
import json
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
@ -101,12 +101,33 @@ def api_reports_list():
|
|||||||
if err is not None:
|
if err is not None:
|
||||||
return err
|
return err
|
||||||
|
|
||||||
rows = (
|
q = (request.args.get("q") or "").strip()
|
||||||
db.session.query(ReportDefinition)
|
|
||||||
.order_by(ReportDefinition.created_at.desc())
|
def _patterns(raw: str) -> list[str]:
|
||||||
.limit(200)
|
out = []
|
||||||
.all()
|
for tok in [t.strip() for t in (raw or "").split() if t.strip()]:
|
||||||
)
|
p = tok.replace("\\", "\\\\")
|
||||||
|
p = p.replace("%", "\\%").replace("_", "\\_")
|
||||||
|
p = p.replace("*", "%")
|
||||||
|
if not p.startswith("%"):
|
||||||
|
p = "%" + p
|
||||||
|
if not p.endswith("%"):
|
||||||
|
p = p + "%"
|
||||||
|
out.append(p)
|
||||||
|
return out
|
||||||
|
|
||||||
|
query = db.session.query(ReportDefinition)
|
||||||
|
if q:
|
||||||
|
for pat in _patterns(q):
|
||||||
|
query = query.filter(
|
||||||
|
(func.coalesce(ReportDefinition.name, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(ReportDefinition.report_type, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(ReportDefinition.output_format, "").ilike(pat, escape="\\"))
|
||||||
|
| (cast(ReportDefinition.period_start, String).ilike(pat, escape="\\"))
|
||||||
|
| (cast(ReportDefinition.period_end, String).ilike(pat, escape="\\"))
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = query.order_by(ReportDefinition.created_at.desc()).limit(200).all()
|
||||||
return {
|
return {
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
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, build_report_job_filters_meta
|
from .routes_reporting_api import build_report_columns_meta, build_report_job_filters_meta
|
||||||
|
from sqlalchemy import cast, String
|
||||||
|
|
||||||
def get_default_report_period():
|
def get_default_report_period():
|
||||||
"""Return default report period (last 7 days)."""
|
"""Return default report period (last 7 days)."""
|
||||||
@ -52,13 +53,33 @@ def _build_report_item(r):
|
|||||||
@main_bp.route("/reports")
|
@main_bp.route("/reports")
|
||||||
@login_required
|
@login_required
|
||||||
def reports():
|
def reports():
|
||||||
|
q = (request.args.get("q") or "").strip()
|
||||||
|
|
||||||
|
def _patterns(raw: str) -> list[str]:
|
||||||
|
out = []
|
||||||
|
for tok in [t.strip() for t in (raw or "").split() if t.strip()]:
|
||||||
|
p = tok.replace("\\", "\\\\")
|
||||||
|
p = p.replace("%", "\\%").replace("_", "\\_")
|
||||||
|
p = p.replace("*", "%")
|
||||||
|
if not p.startswith("%"):
|
||||||
|
p = "%" + p
|
||||||
|
if not p.endswith("%"):
|
||||||
|
p = p + "%"
|
||||||
|
out.append(p)
|
||||||
|
return out
|
||||||
|
|
||||||
# Pre-render items so the page is usable even if JS fails to load/execute.
|
# Pre-render items so the page is usable even if JS fails to load/execute.
|
||||||
rows = (
|
query = db.session.query(ReportDefinition)
|
||||||
db.session.query(ReportDefinition)
|
if q:
|
||||||
.order_by(ReportDefinition.created_at.desc())
|
for pat in _patterns(q):
|
||||||
.limit(200)
|
query = query.filter(
|
||||||
.all()
|
(func.coalesce(ReportDefinition.name, "").ilike(pat, escape="\\"))
|
||||||
)
|
| (func.coalesce(ReportDefinition.report_type, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(ReportDefinition.output_format, "").ilike(pat, escape="\\"))
|
||||||
|
| (cast(ReportDefinition.period_start, String).ilike(pat, escape="\\"))
|
||||||
|
| (cast(ReportDefinition.period_end, String).ilike(pat, escape="\\"))
|
||||||
|
)
|
||||||
|
rows = query.order_by(ReportDefinition.created_at.desc()).limit(200).all()
|
||||||
items = [_build_report_item(r) for r in rows]
|
items = [_build_report_item(r) for r in rows]
|
||||||
|
|
||||||
period_start, period_end = get_default_report_period()
|
period_start, period_end = get_default_report_period()
|
||||||
@ -70,6 +91,7 @@ def reports():
|
|||||||
job_filters_meta=build_report_job_filters_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(),
|
||||||
|
q=q,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -830,6 +830,21 @@ 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()
|
||||||
|
|
||||||
|
def _patterns(raw: str) -> list[str]:
|
||||||
|
out = []
|
||||||
|
for tok in [t.strip() for t in (raw or "").split() if t.strip()]:
|
||||||
|
p = tok.replace("\\", "\\\\")
|
||||||
|
p = p.replace("%", "\\%").replace("_", "\\_")
|
||||||
|
p = p.replace("*", "%")
|
||||||
|
if not p.startswith("%"):
|
||||||
|
p = "%" + p
|
||||||
|
if not p.endswith("%"):
|
||||||
|
p = p + "%"
|
||||||
|
out.append(p)
|
||||||
|
return out
|
||||||
|
|
||||||
include_reviewed = False
|
include_reviewed = False
|
||||||
if get_active_role() == "admin":
|
if get_active_role() == "admin":
|
||||||
include_reviewed = request.args.get("include_reviewed", "0") in ("1", "true", "yes", "on")
|
include_reviewed = request.args.get("include_reviewed", "0") in ("1", "true", "yes", "on")
|
||||||
@ -889,6 +904,14 @@ def run_checks_page():
|
|||||||
.outerjoin(Customer, Customer.id == Job.customer_id)
|
.outerjoin(Customer, Customer.id == Job.customer_id)
|
||||||
.filter(Job.archived.is_(False))
|
.filter(Job.archived.is_(False))
|
||||||
)
|
)
|
||||||
|
if q:
|
||||||
|
for pat in _patterns(q):
|
||||||
|
base = base.filter(
|
||||||
|
(func.coalesce(Customer.name, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(Job.backup_software, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(Job.backup_type, "").ilike(pat, escape="\\"))
|
||||||
|
| (func.coalesce(Job.job_name, "").ilike(pat, escape="\\"))
|
||||||
|
)
|
||||||
|
|
||||||
# Runs to show in the overview: unreviewed (or all if admin toggle enabled)
|
# Runs to show in the overview: unreviewed (or all if admin toggle enabled)
|
||||||
run_filter = []
|
run_filter = []
|
||||||
@ -1136,6 +1159,7 @@ def run_checks_page():
|
|||||||
is_admin=(get_active_role() == "admin"),
|
is_admin=(get_active_role() == "admin"),
|
||||||
include_reviewed=include_reviewed,
|
include_reviewed=include_reviewed,
|
||||||
autotask_enabled=autotask_enabled,
|
autotask_enabled=autotask_enabled,
|
||||||
|
q=q,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -813,6 +813,23 @@ def search_page():
|
|||||||
for s in visible_sections:
|
for s in visible_sections:
|
||||||
key = s["key"]
|
key = s["key"]
|
||||||
cur = int(s.get("current_page", 1) or 1)
|
cur = int(s.get("current_page", 1) or 1)
|
||||||
|
if query:
|
||||||
|
if key == "inbox":
|
||||||
|
s["view_all_url"] = url_for("main.inbox", q=query)
|
||||||
|
elif key == "customers":
|
||||||
|
s["view_all_url"] = url_for("main.customers", q=query)
|
||||||
|
elif key == "jobs":
|
||||||
|
s["view_all_url"] = url_for("main.jobs", q=query)
|
||||||
|
elif key == "daily_jobs":
|
||||||
|
s["view_all_url"] = url_for("main.daily_jobs", q=query)
|
||||||
|
elif key == "run_checks":
|
||||||
|
s["view_all_url"] = url_for("main.run_checks_page", q=query)
|
||||||
|
elif key == "tickets":
|
||||||
|
s["view_all_url"] = url_for("main.tickets_page", q=query)
|
||||||
|
elif key == "overrides":
|
||||||
|
s["view_all_url"] = url_for("main.overrides", q=query)
|
||||||
|
elif key == "reports":
|
||||||
|
s["view_all_url"] = url_for("main.reports", q=query)
|
||||||
if s.get("has_prev"):
|
if s.get("has_prev"):
|
||||||
prev_pages = dict(current_pages)
|
prev_pages = dict(current_pages)
|
||||||
prev_pages[key] = cur - 1
|
prev_pages[key] = cur - 1
|
||||||
|
|||||||
@ -28,17 +28,33 @@ def tickets_page():
|
|||||||
|
|
||||||
if tab == "tickets":
|
if tab == "tickets":
|
||||||
query = Ticket.query
|
query = Ticket.query
|
||||||
|
joined_scope = False
|
||||||
if active_only:
|
if active_only:
|
||||||
query = query.filter(Ticket.resolved_at.is_(None))
|
query = query.filter(Ticket.resolved_at.is_(None))
|
||||||
if q:
|
if q:
|
||||||
like_q = f"%{q}%"
|
like_q = f"%{q}%"
|
||||||
|
query = (
|
||||||
|
query
|
||||||
|
.outerjoin(TicketScope, TicketScope.ticket_id == Ticket.id)
|
||||||
|
.outerjoin(Customer, Customer.id == TicketScope.customer_id)
|
||||||
|
.outerjoin(Job, Job.id == TicketScope.job_id)
|
||||||
|
)
|
||||||
|
joined_scope = True
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
(Ticket.ticket_code.ilike(like_q))
|
(Ticket.ticket_code.ilike(like_q))
|
||||||
| (Ticket.description.ilike(like_q))
|
| (Ticket.description.ilike(like_q))
|
||||||
|
| (Customer.name.ilike(like_q))
|
||||||
|
| (TicketScope.scope_type.ilike(like_q))
|
||||||
|
| (TicketScope.backup_software.ilike(like_q))
|
||||||
|
| (TicketScope.backup_type.ilike(like_q))
|
||||||
|
| (TicketScope.job_name_match.ilike(like_q))
|
||||||
|
| (Job.job_name.ilike(like_q))
|
||||||
)
|
)
|
||||||
|
query = query.distinct()
|
||||||
|
|
||||||
if customer_id or backup_software or backup_type:
|
if customer_id or backup_software or backup_type:
|
||||||
query = query.join(TicketScope, TicketScope.ticket_id == Ticket.id)
|
if not joined_scope:
|
||||||
|
query = query.join(TicketScope, TicketScope.ticket_id == Ticket.id)
|
||||||
if customer_id:
|
if customer_id:
|
||||||
query = query.filter(TicketScope.customer_id == customer_id)
|
query = query.filter(TicketScope.customer_id == customer_id)
|
||||||
if backup_software:
|
if backup_software:
|
||||||
@ -322,4 +338,3 @@ def ticket_detail(ticket_id: int):
|
|||||||
scopes=scopes,
|
scopes=scopes,
|
||||||
runs=runs,
|
runs=runs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,9 @@
|
|||||||
<h2 class="mb-3">Daily Jobs</h2>
|
<h2 class="mb-3">Daily Jobs</h2>
|
||||||
|
|
||||||
<form method="get" class="row g-3 mb-3">
|
<form method="get" class="row g-3 mb-3">
|
||||||
|
{% if q %}
|
||||||
|
<input type="hidden" name="q" value="{{ q }}" />
|
||||||
|
{% endif %}
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="dj_date" class="form-label">Date</label>
|
<label for="dj_date" class="form-label">Date</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -14,12 +14,12 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center my-2">
|
<div class="d-flex justify-content-between align-items-center my-2">
|
||||||
<div>
|
<div>
|
||||||
{% if has_prev %}
|
{% if has_prev %}
|
||||||
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('main.inbox', page=page-1) }}">Previous</a>
|
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('main.inbox', page=page-1, q=q) }}">Previous</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="btn btn-outline-secondary btn-sm" disabled>Previous</button>
|
<button class="btn btn-outline-secondary btn-sm" disabled>Previous</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if has_next %}
|
{% if has_next %}
|
||||||
<a class="btn btn-outline-secondary btn-sm ms-2" href="{{ url_for('main.inbox', page=page+1) }}">Next</a>
|
<a class="btn btn-outline-secondary btn-sm ms-2" href="{{ url_for('main.inbox', page=page+1, q=q) }}">Next</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="btn btn-outline-secondary btn-sm ms-2" disabled>Next</button>
|
<button class="btn btn-outline-secondary btn-sm ms-2" disabled>Next</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -422,7 +422,10 @@ function loadRawData() {
|
|||||||
|
|
||||||
function loadReports() {
|
function loadReports() {
|
||||||
setTableLoading('Loading…');
|
setTableLoading('Loading…');
|
||||||
fetch('/api/reports', { credentials: 'same-origin' })
|
var params = new URLSearchParams(window.location.search || '');
|
||||||
|
var q = (params.get('q') || '').trim();
|
||||||
|
var apiUrl = '/api/reports' + (q ? ('?q=' + encodeURIComponent(q)) : '');
|
||||||
|
fetch(apiUrl, { credentials: 'same-origin' })
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
renderTable((data && data.items) ? data.items : []);
|
renderTable((data && data.items) ? data.items : []);
|
||||||
@ -521,4 +524,4 @@ function loadRawData() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -22,6 +22,9 @@ This file documents all changes made to this project via Claude Code.
|
|||||||
- Changed Daily Jobs search result links to open the same Daily Jobs modal flow via `open_job_id` (instead of only navigating to the overview page)
|
- Changed Daily Jobs search result links to open the same Daily Jobs modal flow via `open_job_id` (instead of only navigating to the overview page)
|
||||||
- Changed `docs/technical-notes-codex.md` to include search pagination query params, Daily Jobs modal-open search behavior, and latest successful test-build digest
|
- Changed `docs/technical-notes-codex.md` to include search pagination query params, Daily Jobs modal-open search behavior, and latest successful test-build digest
|
||||||
- Changed search pagination buttons to preserve scroll position by linking back to the active section anchor after page navigation
|
- Changed search pagination buttons to preserve scroll position by linking back to the active section anchor after page navigation
|
||||||
|
- Changed "Open <section>" behavior from global search to pass `q` into destination pages and apply page-level filtering, so opened overviews reflect the same search term
|
||||||
|
- Changed filtering support on Inbox, Customers, Jobs, Daily Jobs, Run Checks, Tickets, Overrides, and Reports routes to accept wildcard-enabled `q` terms from search
|
||||||
|
- Changed Reports frontend loading (`/api/reports`) to forward URL `q` so client-side refresh keeps the same filtered result set
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Fixed `/search` page crash (`TypeError: 'builtin_function_or_method' object is not iterable`) by replacing Jinja dict access from `section.items` to `section['items']` in `templates/main/search.html`
|
- Fixed `/search` page crash (`TypeError: 'builtin_function_or_method' object is not iterable`) by replacing Jinja dict access from `section.items` to `section['items']` in `templates/main/search.html`
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user