backupchecks/containers/backupchecks/src/backend/app/main/routes_tickets.py

336 lines
12 KiB
Python

from .routes_shared import * # noqa: F401,F403
from .routes_shared import _format_datetime
@main_bp.route("/tickets")
@login_required
@roles_required("admin", "operator", "viewer")
def tickets_page():
tab = (request.args.get("tab") or "tickets").strip().lower()
if tab not in ("tickets", "remarks"):
tab = "tickets"
active = (request.args.get("active") or "1").strip()
active_only = active != "0"
q = (request.args.get("q") or "").strip()
try:
customer_id = int(request.args.get("customer_id") or 0)
except Exception:
customer_id = 0
backup_software = (request.args.get("backup_software") or "").strip()
backup_type = (request.args.get("backup_type") or "").strip()
customers = Customer.query.order_by(Customer.name.asc()).all()
tickets = []
remarks = []
if tab == "tickets":
query = Ticket.query
if active_only:
query = query.filter(Ticket.resolved_at.is_(None))
if q:
like_q = f"%{q}%"
query = query.filter(
(Ticket.ticket_code.ilike(like_q))
| (Ticket.description.ilike(like_q))
)
if customer_id or backup_software or backup_type:
query = query.join(TicketScope, TicketScope.ticket_id == Ticket.id)
if customer_id:
query = query.filter(TicketScope.customer_id == customer_id)
if backup_software:
query = query.filter(TicketScope.backup_software == backup_software)
if backup_type:
query = query.filter(TicketScope.backup_type == backup_type)
query = query.order_by(Ticket.resolved_at.isnot(None), Ticket.start_date.desc())
tickets_raw = query.limit(500).all()
ticket_ids = [t.id for t in tickets_raw]
customer_map = {}
run_count_map = {}
if ticket_ids:
try:
rows = (
db.session.execute(
text(
"""
SELECT ts.ticket_id, c.name
FROM ticket_scopes ts
JOIN customers c ON c.id = ts.customer_id
WHERE ts.ticket_id = ANY(:ids)
AND ts.customer_id IS NOT NULL
"""
),
{"ids": ticket_ids},
)
.fetchall()
)
for tid, cname in rows:
customer_map.setdefault(int(tid), [])
if cname and cname not in customer_map[int(tid)]:
customer_map[int(tid)].append(cname)
except Exception:
customer_map = {}
try:
rows = (
db.session.execute(
text(
"""
SELECT ticket_id, COUNT(*)
FROM ticket_job_runs
WHERE ticket_id = ANY(:ids)
GROUP BY ticket_id
"""
),
{"ids": ticket_ids},
)
.fetchall()
)
for tid, cnt in rows:
run_count_map[int(tid)] = int(cnt or 0)
except Exception:
run_count_map = {}
for t in tickets_raw:
customers_for_ticket = customer_map.get(t.id) or []
if customers_for_ticket:
customer_display = customers_for_ticket[0]
if len(customers_for_ticket) > 1:
customer_display += f" +{len(customers_for_ticket)-1}"
else:
customer_display = "-"
# Scope summary: best-effort from first scope
scope_summary = "-"
first_job_id = None
try:
s = TicketScope.query.filter(TicketScope.ticket_id == t.id).order_by(TicketScope.id.asc()).first()
if s:
parts = []
if s.backup_software:
parts.append(s.backup_software)
if s.backup_type:
parts.append(s.backup_type)
if s.job_id:
first_job_id = int(s.job_id)
job = Job.query.get(s.job_id)
if job and job.job_name:
parts.append(job.job_name)
scope_summary = " / ".join([p for p in parts if p]) or "-"
except Exception:
scope_summary = "-"
tickets.append(
{
"id": t.id,
"ticket_code": t.ticket_code,
"description": t.description or "",
"active_from_date": str(getattr(t, "active_from_date", "") or ""),
"start_date": _format_datetime(t.start_date),
"resolved_at": _format_datetime(t.resolved_at) if t.resolved_at else "",
"active": t.resolved_at is None,
"customers": customer_display,
"scope_summary": scope_summary,
"linked_runs": run_count_map.get(t.id, 0),
"job_id": first_job_id,
}
)
else:
query = Remark.query
if active_only:
query = query.filter(Remark.resolved_at.is_(None))
if q:
like_q = f"%{q}%"
query = query.filter(Remark.body.ilike(like_q))
if customer_id or backup_software or backup_type:
query = query.join(RemarkScope, RemarkScope.remark_id == Remark.id)
if customer_id:
query = query.filter(RemarkScope.customer_id == customer_id)
if backup_software:
query = query.filter(RemarkScope.backup_software == backup_software)
if backup_type:
query = query.filter(RemarkScope.backup_type == backup_type)
query = query.order_by(Remark.resolved_at.isnot(None), Remark.start_date.desc())
remarks_raw = query.limit(500).all()
remark_ids = [r.id for r in remarks_raw]
customer_map = {}
run_count_map = {}
if remark_ids:
try:
rows = (
db.session.execute(
text(
"""
SELECT rs.remark_id, c.name
FROM remark_scopes rs
JOIN customers c ON c.id = rs.customer_id
WHERE rs.remark_id = ANY(:ids)
AND rs.customer_id IS NOT NULL
"""
),
{"ids": remark_ids},
)
.fetchall()
)
for rid, cname in rows:
customer_map.setdefault(int(rid), [])
if cname and cname not in customer_map[int(rid)]:
customer_map[int(rid)].append(cname)
except Exception:
customer_map = {}
try:
rows = (
db.session.execute(
text(
"""
SELECT remark_id, COUNT(*)
FROM remark_job_runs
WHERE remark_id = ANY(:ids)
GROUP BY remark_id
"""
),
{"ids": remark_ids},
)
.fetchall()
)
for rid, cnt in rows:
run_count_map[int(rid)] = int(cnt or 0)
except Exception:
run_count_map = {}
for r in remarks_raw:
customers_for_remark = customer_map.get(r.id) or []
if customers_for_remark:
customer_display = customers_for_remark[0]
if len(customers_for_remark) > 1:
customer_display += f" +{len(customers_for_remark)-1}"
else:
customer_display = "-"
scope_summary = "-"
first_job_id = None
try:
s = RemarkScope.query.filter(RemarkScope.remark_id == r.id).order_by(RemarkScope.id.asc()).first()
if s:
parts = []
if s.backup_software:
parts.append(s.backup_software)
if s.backup_type:
parts.append(s.backup_type)
if s.job_id:
first_job_id = int(s.job_id)
job = Job.query.get(s.job_id)
if job and job.job_name:
parts.append(job.job_name)
scope_summary = " / ".join([p for p in parts if p]) or "-"
except Exception:
scope_summary = "-"
preview = (r.body or "")
if len(preview) > 80:
preview = preview[:77] + "..."
remarks.append(
{
"id": r.id,
"preview": preview,
"start_date": _format_datetime(r.start_date) if r.start_date else "-",
"resolved_at": _format_datetime(r.resolved_at) if r.resolved_at else "",
"active": r.resolved_at is None,
"customers": customer_display,
"scope_summary": scope_summary,
"linked_runs": run_count_map.get(r.id, 0),
"job_id": first_job_id,
}
)
return render_template(
"main/tickets.html",
tab=tab,
active_only=active_only,
q=q,
customer_id=customer_id,
backup_software=backup_software,
backup_type=backup_type,
customers=customers,
tickets=tickets,
remarks=remarks,
)
@main_bp.route("/tickets/<int:ticket_id>", methods=["GET", "POST"])
@login_required
@roles_required("admin", "operator", "viewer")
def ticket_detail(ticket_id: int):
ticket = Ticket.query.get_or_404(ticket_id)
if request.method == "POST":
if get_active_role() not in ("admin", "operator"):
abort(403)
ticket.description = (request.form.get("description") or "").strip() or None
try:
db.session.commit()
flash("Ticket updated.", "success")
except Exception as exc:
db.session.rollback()
flash(f"Failed to update ticket: {exc}", "danger")
return redirect(url_for("main.ticket_detail", ticket_id=ticket.id))
# Scopes
scopes = TicketScope.query.filter(TicketScope.ticket_id == ticket.id).order_by(TicketScope.id.asc()).all()
# Linked runs
runs = []
try:
rows = (
db.session.execute(
text(
"""
SELECT jr.id, jr.run_at, jr.status, j.job_name, c.name AS customer_name
FROM ticket_job_runs tjr
JOIN job_runs jr ON jr.id = tjr.job_run_id
JOIN jobs j ON j.id = jr.job_id
LEFT JOIN customers c ON c.id = j.customer_id
WHERE tjr.ticket_id = :ticket_id
ORDER BY jr.run_at DESC
LIMIT 20
"""
),
{"ticket_id": ticket.id},
)
.mappings()
.all()
)
for r in rows:
runs.append(
{
"id": r.get("id"),
"run_at": _format_datetime(r.get("run_at")),
"status": r.get("status") or "",
"job_name": r.get("job_name") or "",
"customer_name": r.get("customer_name") or "",
}
)
except Exception:
runs = []
return render_template(
"main/ticket_detail.html",
ticket=ticket,
scopes=scopes,
runs=runs,
)