Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 588f788e31 | |||
| a919610d68 | |||
| da9ed8402e | |||
| 8bef63c18a | |||
| 7385ecf94c | |||
| f62c19ddf8 | |||
| 1064bc8d86 | |||
| 5e617cb6a9 | |||
| d467c060dc | |||
| 0d9159ef6f | |||
| 5b940e34f2 | |||
| 43502ae6f3 | |||
| a9cae0f8f5 | |||
| 1b5effc5d2 | |||
| c1aeee2a8c | |||
| aea6a866c9 | |||
| c228d6db19 | |||
| 88b267b8bd | |||
| 4f208aedd0 | |||
| caff435f96 | |||
| f3b1b56b6a | |||
| 596fc94e69 | |||
| 49f24595c3 | |||
| fd3f3765c3 | |||
| 2a03ff0764 |
@ -1 +1 @@
|
||||
v20260209-08-veeam-vbo365-not-started
|
||||
main
|
||||
|
||||
@ -3,6 +3,50 @@ Changelog data structure for Backupchecks
|
||||
"""
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "v0.1.26",
|
||||
"date": "2026-02-10",
|
||||
"summary": "This critical bug fix release resolves ticket system display issues where resolved tickets were incorrectly appearing on new runs across multiple pages. The ticket system has been completely transitioned from date-based logic to explicit link-based queries, ensuring resolved tickets stop appearing immediately after resolution while preserving audit trail for historical runs.",
|
||||
"sections": [
|
||||
{
|
||||
"title": "Bug Fixes",
|
||||
"type": "bugfix",
|
||||
"subsections": [
|
||||
{
|
||||
"subtitle": "Ticket System - Resolved Ticket Display Issues",
|
||||
"changes": [
|
||||
"Root Cause: Multiple pages used legacy date-based logic (active_from_date <= run_date AND resolved_at >= run_date) instead of checking explicit ticket_job_runs links",
|
||||
"Impact: Resolved tickets kept appearing on ALL runs between active_from_date and resolved_at, even runs created after resolution",
|
||||
"Fixed: Ticket Linking (ticketing_utils.py) - Autotask tickets now propagate to new runs using independent strategy that checks for most recent non-deleted and non-resolved Autotask ticket",
|
||||
"Fixed: Internal tickets no longer link to new runs after resolution - removed date-based 'open' logic, now only links if COALESCE(ts.resolved_at, t.resolved_at) IS NULL",
|
||||
"Fixed: Job Details Page - Implemented two-source ticket display: direct links (ticket_job_runs) always shown for audit trail, active window (ticket_scopes) only shown if unresolved",
|
||||
"Fixed: Run Checks Main Page - Ticket/remark indicators (🎫/💬) now only show for genuinely unresolved tickets, removed date-based logic from existence queries",
|
||||
"Fixed: Run Checks Popup Modal - Replaced date-based queries in /api/job-runs/<run_id>/alerts with explicit JOIN queries (ticket_job_runs, remark_job_runs)",
|
||||
"Fixed: Run Checks Popup - Removed unused parameters (run_date, job_id, ui_tz) as they are no longer needed with link-based queries",
|
||||
"Testing: Temporarily added debug logging to link_open_internal_tickets_to_run (wrote to AuditLog with event_type 'ticket_link_debug'), removed after successful resolution",
|
||||
"Result: Resolved tickets stop appearing immediately after resolution, consistent behavior across all pages, audit trail preserved for historical runs",
|
||||
"Result: All queries now use explicit link-based logic with no date comparisons"
|
||||
]
|
||||
},
|
||||
{
|
||||
"subtitle": "Test Email Generation",
|
||||
"changes": [
|
||||
"Reduced test email generation from 3 emails per status to 1 email per status for simpler testing",
|
||||
"Each button now creates exactly 1 test mail instead of 3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"subtitle": "User Interface",
|
||||
"changes": [
|
||||
"Updated Settings → Maintenance page text for test email generation to match actual behavior",
|
||||
"Changed description from '3 emails simulating Veeam, Synology, and NAKIVO' to '1 Veeam Backup Job email'",
|
||||
"Updated button labels from '(3)' to '(1)' on all test email generation buttons"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "v0.1.25",
|
||||
"date": "2026-02-09",
|
||||
|
||||
@ -16,33 +16,27 @@ def api_job_run_alerts(run_id: int):
|
||||
tickets = []
|
||||
remarks = []
|
||||
|
||||
# Tickets active for this job on this run date (including resolved-on-day)
|
||||
# Tickets linked to this specific run
|
||||
# Only show tickets that were explicitly linked via ticket_job_runs
|
||||
try:
|
||||
rows = (
|
||||
db.session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT t.id,
|
||||
SELECT DISTINCT t.id,
|
||||
t.ticket_code,
|
||||
t.description,
|
||||
t.start_date,
|
||||
COALESCE(ts.resolved_at, t.resolved_at) AS resolved_at,
|
||||
t.resolved_at,
|
||||
t.active_from_date
|
||||
FROM tickets t
|
||||
JOIN ticket_scopes ts ON ts.ticket_id = t.id
|
||||
WHERE ts.job_id = :job_id
|
||||
AND t.active_from_date <= :run_date
|
||||
AND (
|
||||
COALESCE(ts.resolved_at, t.resolved_at) IS NULL
|
||||
OR ((COALESCE(ts.resolved_at, t.resolved_at) AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :run_date
|
||||
)
|
||||
JOIN ticket_job_runs tjr ON tjr.ticket_id = t.id
|
||||
WHERE tjr.job_run_id = :run_id
|
||||
ORDER BY t.start_date DESC
|
||||
"""
|
||||
),
|
||||
{
|
||||
"job_id": job.id if job else None,
|
||||
"run_date": run_date,
|
||||
"ui_tz": _get_ui_timezone_name(),
|
||||
"run_id": run_id,
|
||||
},
|
||||
)
|
||||
.mappings()
|
||||
@ -71,31 +65,22 @@ def api_job_run_alerts(run_id: int):
|
||||
except Exception as exc:
|
||||
return jsonify({"status": "error", "message": str(exc) or "Failed to load tickets."}), 500
|
||||
|
||||
# Remarks active for this job on this run date (including resolved-on-day)
|
||||
# Remarks linked to this specific run
|
||||
# Only show remarks that were explicitly linked via remark_job_runs
|
||||
try:
|
||||
rows = (
|
||||
db.session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT r.id, r.body, r.start_date, r.resolved_at, r.active_from_date
|
||||
SELECT DISTINCT r.id, r.body, r.start_date, r.resolved_at, r.active_from_date
|
||||
FROM remarks r
|
||||
JOIN remark_scopes rs ON rs.remark_id = r.id
|
||||
WHERE rs.job_id = :job_id
|
||||
AND COALESCE(
|
||||
r.active_from_date,
|
||||
((r.start_date AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date)
|
||||
) <= :run_date
|
||||
AND (
|
||||
r.resolved_at IS NULL
|
||||
OR ((r.resolved_at AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :run_date
|
||||
)
|
||||
JOIN remark_job_runs rjr ON rjr.remark_id = r.id
|
||||
WHERE rjr.job_run_id = :run_id
|
||||
ORDER BY r.start_date DESC
|
||||
"""
|
||||
),
|
||||
{
|
||||
"job_id": job.id if job else None,
|
||||
"run_date": run_date,
|
||||
"ui_tz": _get_ui_timezone_name(),
|
||||
"run_id": run_id,
|
||||
},
|
||||
)
|
||||
.mappings()
|
||||
|
||||
@ -168,23 +168,61 @@ def job_detail(job_id: int):
|
||||
.all()
|
||||
)
|
||||
|
||||
# Tickets: mark runs that fall within the ticket active window
|
||||
# Tickets: mark runs that fall within the ticket active window OR have direct links
|
||||
ticket_rows = []
|
||||
ticket_open_count = 0
|
||||
ticket_total_count = 0
|
||||
|
||||
# Map of run_id -> list of directly linked ticket codes (for audit trail)
|
||||
direct_ticket_links = {}
|
||||
|
||||
remark_rows = []
|
||||
remark_open_count = 0
|
||||
remark_total_count = 0
|
||||
|
||||
run_dates = []
|
||||
run_date_map = {}
|
||||
run_ids = []
|
||||
for r in runs:
|
||||
rd = _to_amsterdam_date(r.run_at) or _to_amsterdam_date(datetime.utcnow())
|
||||
run_date_map[r.id] = rd
|
||||
run_ids.append(r.id)
|
||||
if rd:
|
||||
run_dates.append(rd)
|
||||
|
||||
# Get directly linked tickets for these runs (audit trail - show even if resolved)
|
||||
if run_ids:
|
||||
try:
|
||||
rows = (
|
||||
db.session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT tjr.job_run_id, t.ticket_code, t.resolved_at
|
||||
FROM ticket_job_runs tjr
|
||||
JOIN tickets t ON t.id = tjr.ticket_id
|
||||
WHERE tjr.job_run_id = ANY(:run_ids)
|
||||
"""
|
||||
),
|
||||
{"run_ids": run_ids},
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
)
|
||||
for rr in rows:
|
||||
run_id = rr.get("job_run_id")
|
||||
code = (rr.get("ticket_code") or "").strip()
|
||||
resolved_at = rr.get("resolved_at")
|
||||
if run_id not in direct_ticket_links:
|
||||
direct_ticket_links[run_id] = []
|
||||
direct_ticket_links[run_id].append({
|
||||
"ticket_code": code,
|
||||
"resolved_at": resolved_at,
|
||||
"is_direct_link": True
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Get active (unresolved) tickets for future runs
|
||||
if run_dates:
|
||||
min_date = min(run_dates)
|
||||
max_date = max(run_dates)
|
||||
@ -198,14 +236,10 @@ def job_detail(job_id: int):
|
||||
JOIN ticket_scopes ts ON ts.ticket_id = t.id
|
||||
WHERE ts.job_id = :job_id
|
||||
AND t.active_from_date <= :max_date
|
||||
AND (
|
||||
COALESCE(ts.resolved_at, t.resolved_at) IS NULL
|
||||
OR ((COALESCE(ts.resolved_at, t.resolved_at) AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :min_date
|
||||
)
|
||||
AND COALESCE(ts.resolved_at, t.resolved_at) IS NULL
|
||||
"""
|
||||
),
|
||||
{"job_id": job.id, "min_date": min_date,
|
||||
"ui_tz": _get_ui_timezone_name(), "max_date": max_date},
|
||||
{"job_id": job.id, "max_date": max_date},
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
@ -214,7 +248,12 @@ def job_detail(job_id: int):
|
||||
active_from = rr.get("active_from_date")
|
||||
resolved_at = rr.get("resolved_at")
|
||||
resolved_date = _to_amsterdam_date(resolved_at) if resolved_at else None
|
||||
ticket_rows.append({"active_from_date": active_from, "resolved_date": resolved_date, "ticket_code": rr.get("ticket_code")})
|
||||
ticket_rows.append({
|
||||
"active_from_date": active_from,
|
||||
"resolved_date": resolved_date,
|
||||
"ticket_code": rr.get("ticket_code"),
|
||||
"is_direct_link": False
|
||||
})
|
||||
except Exception:
|
||||
ticket_rows = []
|
||||
|
||||
@ -240,14 +279,10 @@ def job_detail(job_id: int):
|
||||
r.active_from_date,
|
||||
((r.start_date AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date)
|
||||
) <= :max_date
|
||||
AND (
|
||||
r.resolved_at IS NULL
|
||||
OR ((r.resolved_at AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :min_date
|
||||
)
|
||||
AND r.resolved_at IS NULL
|
||||
"""
|
||||
),
|
||||
{"job_id": job.id, "min_date": min_date,
|
||||
"ui_tz": _get_ui_timezone_name(), "max_date": max_date},
|
||||
{"job_id": job.id, "max_date": max_date},
|
||||
)
|
||||
.mappings()
|
||||
.all()
|
||||
@ -341,11 +376,22 @@ def job_detail(job_id: int):
|
||||
ticket_codes = []
|
||||
remark_items = []
|
||||
|
||||
# First: add directly linked tickets (audit trail - always show)
|
||||
if r.id in direct_ticket_links:
|
||||
for tlink in direct_ticket_links[r.id]:
|
||||
code = tlink.get("ticket_code", "")
|
||||
if code and code not in ticket_codes:
|
||||
ticket_codes.append(code)
|
||||
has_ticket = True
|
||||
|
||||
# Second: add active window tickets (only unresolved)
|
||||
if rd and ticket_rows:
|
||||
for tr in ticket_rows:
|
||||
if tr.get("is_direct_link"):
|
||||
continue # Skip, already added above
|
||||
af = tr.get("active_from_date")
|
||||
resd = tr.get("resolved_date")
|
||||
if af and af <= rd and (resd is None or resd >= rd):
|
||||
# Only check active_from, resolved tickets already filtered by query
|
||||
if af and af <= rd:
|
||||
has_ticket = True
|
||||
code = (tr.get("ticket_code") or "").strip()
|
||||
if code and code not in ticket_codes:
|
||||
|
||||
@ -1068,14 +1068,11 @@ def run_checks_page():
|
||||
JOIN ticket_scopes ts ON ts.ticket_id = t.id
|
||||
WHERE ts.job_id = :job_id
|
||||
AND t.active_from_date <= :run_date
|
||||
AND (
|
||||
COALESCE(ts.resolved_at, t.resolved_at) IS NULL
|
||||
OR ((COALESCE(ts.resolved_at, t.resolved_at) AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :run_date
|
||||
)
|
||||
AND COALESCE(ts.resolved_at, t.resolved_at) IS NULL
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
{"job_id": job_id, "run_date": today_local, "ui_tz": ui_tz},
|
||||
{"job_id": job_id, "run_date": today_local},
|
||||
).first()
|
||||
has_active_ticket = bool(t_exists)
|
||||
|
||||
@ -1090,10 +1087,7 @@ def run_checks_page():
|
||||
r.active_from_date,
|
||||
((r.start_date AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date)
|
||||
) <= :run_date
|
||||
AND (
|
||||
r.resolved_at IS NULL
|
||||
OR ((r.resolved_at AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :run_date
|
||||
)
|
||||
AND r.resolved_at IS NULL
|
||||
LIMIT 1
|
||||
"""
|
||||
),
|
||||
|
||||
@ -310,8 +310,7 @@ def settings_generate_test_emails(status_type):
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Fixed test email sets per status type (Veeam only for consistent testing)
|
||||
# All emails use the same job name "Test-Backup-Job" so they appear as different
|
||||
# runs of the same job for proper status testing
|
||||
# Single email per status for simpler testing
|
||||
email_sets = {
|
||||
"success": [
|
||||
{
|
||||
@ -334,50 +333,6 @@ Success
|
||||
Processing VM-WEB01
|
||||
Success
|
||||
|
||||
All backup operations completed without issues.""",
|
||||
},
|
||||
{
|
||||
"from_address": "veeam@test.local",
|
||||
"subject": 'Veeam Backup Job "Test-Backup-Job" finished with Success',
|
||||
"body": """Backup job: Test-Backup-Job
|
||||
|
||||
Session details:
|
||||
Start time: 2026-02-08 01:00:00
|
||||
End time: 2026-02-08 02:10:00
|
||||
Total size: 145 GB
|
||||
Duration: 01:10:00
|
||||
|
||||
Processing VM-APP01
|
||||
Success
|
||||
|
||||
Processing VM-DB01
|
||||
Success
|
||||
|
||||
Processing VM-WEB01
|
||||
Success
|
||||
|
||||
All backup operations completed without issues.""",
|
||||
},
|
||||
{
|
||||
"from_address": "veeam@test.local",
|
||||
"subject": 'Veeam Backup Job "Test-Backup-Job" finished with Success',
|
||||
"body": """Backup job: Test-Backup-Job
|
||||
|
||||
Session details:
|
||||
Start time: 2026-02-07 01:00:00
|
||||
End time: 2026-02-07 02:20:00
|
||||
Total size: 152 GB
|
||||
Duration: 01:20:00
|
||||
|
||||
Processing VM-APP01
|
||||
Success
|
||||
|
||||
Processing VM-DB01
|
||||
Success
|
||||
|
||||
Processing VM-WEB01
|
||||
Success
|
||||
|
||||
All backup operations completed without issues.""",
|
||||
},
|
||||
],
|
||||
@ -405,52 +360,6 @@ Success
|
||||
|
||||
Backup completed but some files were skipped.""",
|
||||
},
|
||||
{
|
||||
"from_address": "veeam@test.local",
|
||||
"subject": 'Veeam Backup Job "Test-Backup-Job" finished with WARNING',
|
||||
"body": """Backup job: Test-Backup-Job
|
||||
|
||||
Session details:
|
||||
Start time: 2026-02-08 01:00:00
|
||||
End time: 2026-02-08 02:25:00
|
||||
Total size: 142 GB
|
||||
Duration: 01:25:00
|
||||
|
||||
Processing VM-APP01
|
||||
Warning
|
||||
Warning: Retry was successful after initial failure
|
||||
|
||||
Processing VM-DB01
|
||||
Success
|
||||
|
||||
Processing VM-WEB01
|
||||
Success
|
||||
|
||||
Backup completed with warnings.""",
|
||||
},
|
||||
{
|
||||
"from_address": "veeam@test.local",
|
||||
"subject": 'Veeam Backup Job "Test-Backup-Job" finished with WARNING',
|
||||
"body": """Backup job: Test-Backup-Job
|
||||
|
||||
Session details:
|
||||
Start time: 2026-02-07 01:00:00
|
||||
End time: 2026-02-07 02:35:00
|
||||
Total size: 140 GB
|
||||
Duration: 01:35:00
|
||||
|
||||
Processing VM-APP01
|
||||
Success
|
||||
|
||||
Processing VM-DB01
|
||||
Success
|
||||
|
||||
Processing VM-WEB01
|
||||
Warning
|
||||
Warning: Some files were locked and skipped
|
||||
|
||||
Backup completed with warnings.""",
|
||||
},
|
||||
],
|
||||
"error": [
|
||||
{
|
||||
@ -476,52 +385,6 @@ Success
|
||||
|
||||
Backup failed. Please check the logs for details.""",
|
||||
},
|
||||
{
|
||||
"from_address": "veeam@test.local",
|
||||
"subject": 'Veeam Backup Job "Test-Backup-Job" finished with Failed',
|
||||
"body": """Backup job: Test-Backup-Job
|
||||
|
||||
Session details:
|
||||
Start time: 2026-02-08 01:00:00
|
||||
End time: 2026-02-08 01:10:00
|
||||
Total size: 0 GB
|
||||
Duration: 00:10:00
|
||||
|
||||
Processing VM-APP01
|
||||
Success
|
||||
|
||||
Processing VM-DB01
|
||||
Failed
|
||||
Error: Disk space exhausted on backup repository
|
||||
|
||||
Processing VM-WEB01
|
||||
Success
|
||||
|
||||
Backup failed due to storage issue.""",
|
||||
},
|
||||
{
|
||||
"from_address": "veeam@test.local",
|
||||
"subject": 'Veeam Backup Job "Test-Backup-Job" finished with Failed',
|
||||
"body": """Backup job: Test-Backup-Job
|
||||
|
||||
Session details:
|
||||
Start time: 2026-02-07 01:00:00
|
||||
End time: 2026-02-07 01:05:00
|
||||
Total size: 0 GB
|
||||
Duration: 00:05:00
|
||||
|
||||
Processing VM-APP01
|
||||
Success
|
||||
|
||||
Processing VM-DB01
|
||||
Success
|
||||
|
||||
Processing VM-WEB01
|
||||
Failed
|
||||
Error: Network connection lost to ESXi host
|
||||
|
||||
Backup failed. Network issue detected.""",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@ -170,27 +170,23 @@ def link_open_internal_tickets_to_run(*, run: JobRun, job: Job) -> None:
|
||||
ui_tz = _get_ui_timezone_name()
|
||||
run_date = _to_ui_date(getattr(run, "run_at", None)) or _to_ui_date(datetime.utcnow())
|
||||
|
||||
# Find open tickets scoped to this job for the run date window.
|
||||
# This matches the logic used by Job Details and Run Checks indicators.
|
||||
# Find open (unresolved) tickets scoped to this job.
|
||||
rows = []
|
||||
try:
|
||||
rows = (
|
||||
db.session.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT t.id, t.ticket_code
|
||||
SELECT t.id, t.ticket_code, t.resolved_at, ts.resolved_at as scope_resolved_at
|
||||
FROM tickets t
|
||||
JOIN ticket_scopes ts ON ts.ticket_id = t.id
|
||||
WHERE ts.job_id = :job_id
|
||||
AND t.active_from_date <= :run_date
|
||||
AND (
|
||||
COALESCE(ts.resolved_at, t.resolved_at) IS NULL
|
||||
OR ((COALESCE(ts.resolved_at, t.resolved_at) AT TIME ZONE 'UTC' AT TIME ZONE :ui_tz)::date) >= :run_date
|
||||
)
|
||||
AND COALESCE(ts.resolved_at, t.resolved_at) IS NULL
|
||||
ORDER BY t.start_date DESC, t.id DESC
|
||||
"""
|
||||
),
|
||||
{"job_id": int(job.id), "run_date": run_date, "ui_tz": ui_tz},
|
||||
{"job_id": int(job.id), "run_date": run_date},
|
||||
)
|
||||
.fetchall()
|
||||
)
|
||||
@ -201,7 +197,7 @@ def link_open_internal_tickets_to_run(*, run: JobRun, job: Job) -> None:
|
||||
return
|
||||
|
||||
# Link all open tickets to this run (idempotent)
|
||||
for tid, _code in rows:
|
||||
for tid, code, t_resolved, ts_resolved in rows:
|
||||
if not TicketJobRun.query.filter_by(ticket_id=int(tid), job_run_id=int(run.id)).first():
|
||||
db.session.add(TicketJobRun(ticket_id=int(tid), job_run_id=int(run.id), link_source="inherit"))
|
||||
|
||||
@ -213,12 +209,13 @@ def link_open_internal_tickets_to_run(*, run: JobRun, job: Job) -> None:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Strategy 1: Use internal ticket code to find matching Autotask-linked run
|
||||
# The query above only returns unresolved tickets, so we can safely propagate.
|
||||
try:
|
||||
# Use the newest ticket code to find a matching prior Autotask-linked run.
|
||||
newest_code = (rows[0][1] or "").strip()
|
||||
if not newest_code:
|
||||
return
|
||||
|
||||
# rows format: (tid, code, t_resolved, ts_resolved)
|
||||
newest_code = (rows[0][1] or "").strip() if rows else ""
|
||||
if newest_code:
|
||||
prior = (
|
||||
JobRun.query.filter(JobRun.job_id == job.id)
|
||||
.filter(JobRun.autotask_ticket_id.isnot(None))
|
||||
@ -231,5 +228,33 @@ def link_open_internal_tickets_to_run(*, run: JobRun, job: Job) -> None:
|
||||
run.autotask_ticket_number = prior.autotask_ticket_number
|
||||
run.autotask_ticket_created_at = getattr(prior, "autotask_ticket_created_at", None)
|
||||
run.autotask_ticket_created_by_user_id = getattr(prior, "autotask_ticket_created_by_user_id", None)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Strategy 2: Direct Autotask propagation (independent of internal ticket status)
|
||||
# Find the most recent non-deleted, non-resolved Autotask ticket for this job.
|
||||
try:
|
||||
prior = (
|
||||
JobRun.query.filter(JobRun.job_id == job.id)
|
||||
.filter(JobRun.autotask_ticket_id.isnot(None))
|
||||
.filter(JobRun.autotask_ticket_deleted_at.is_(None))
|
||||
.order_by(JobRun.id.desc())
|
||||
.first()
|
||||
)
|
||||
if prior and getattr(prior, "autotask_ticket_id", None):
|
||||
# Check if the internal ticket is resolved (Autotask tickets are resolved via internal Ticket)
|
||||
ticket_number = (getattr(prior, "autotask_ticket_number", None) or "").strip()
|
||||
if ticket_number:
|
||||
internal_ticket = Ticket.query.filter_by(ticket_code=ticket_number).first()
|
||||
if internal_ticket and getattr(internal_ticket, "resolved_at", None):
|
||||
# Ticket is resolved, don't propagate
|
||||
return
|
||||
|
||||
# Ticket is not deleted and not resolved, propagate it
|
||||
run.autotask_ticket_id = prior.autotask_ticket_id
|
||||
run.autotask_ticket_number = prior.autotask_ticket_number
|
||||
run.autotask_ticket_created_at = getattr(prior, "autotask_ticket_created_at", None)
|
||||
run.autotask_ticket_created_by_user_id = getattr(prior, "autotask_ticket_created_by_user_id", None)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
@ -563,16 +563,16 @@
|
||||
<div class="card h-100 border-info">
|
||||
<div class="card-header bg-info text-white">Generate test emails</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-3">Generate fixed test email sets in the inbox for testing parsers and maintenance operations. Each set contains 3 emails simulating Veeam, Synology, and NAKIVO backups.</p>
|
||||
<p class="mb-3">Generate Veeam test emails in the inbox for testing parsers and maintenance operations. Each button creates 1 Veeam Backup Job email with the specified status.</p>
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<form method="post" action="{{ url_for('main.settings_generate_test_emails', status_type='success') }}">
|
||||
<button type="submit" class="btn btn-success w-100">Generate success emails (3)</button>
|
||||
<button type="submit" class="btn btn-success w-100">Generate success email (1)</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('main.settings_generate_test_emails', status_type='warning') }}">
|
||||
<button type="submit" class="btn btn-warning w-100">Generate warning emails (3)</button>
|
||||
<button type="submit" class="btn btn-warning w-100">Generate warning email (1)</button>
|
||||
</form>
|
||||
<form method="post" action="{{ url_for('main.settings_generate_test_emails', status_type='error') }}">
|
||||
<button type="submit" class="btn btn-danger w-100">Generate error emails (3)</button>
|
||||
<button type="submit" class="btn btn-danger w-100">Generate error email (1)</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,27 @@
|
||||
|
||||
This file documents all changes made to this project via Claude Code.
|
||||
|
||||
## [2026-02-10]
|
||||
|
||||
### Fixed
|
||||
- Fixed Autotask ticket not being automatically linked to new runs when internal ticket is resolved by implementing independent Autotask propagation strategy (now checks for most recent non-deleted and non-resolved Autotask ticket on job regardless of internal ticket status, ensuring PSA ticket reference persists across runs until explicitly resolved or deleted)
|
||||
- Fixed internal and Autotask tickets being linked to new runs even after being resolved by removing date-based "open" logic from ticket query (tickets now only link to new runs if they are genuinely unresolved, not based on run date comparisons)
|
||||
- Fixed Job Details page showing resolved tickets for ALL runs by implementing two-source ticket display: directly linked tickets (via ticket_job_runs) are always shown for audit trail, while active window tickets (via scope query) are only shown if unresolved, preserving historical ticket links while preventing resolved tickets from appearing on new runs
|
||||
- Fixed Run Checks page showing resolved ticket indicators by removing date-based logic from ticket/remark existence queries (tickets and remarks now only show indicators if genuinely unresolved)
|
||||
- Fixed Run Checks popup showing resolved tickets for runs where they were never linked by replacing date-based ticket/remark queries in `/api/job-runs/<run_id>/alerts` endpoint with explicit link-based queries (now only shows tickets/remarks that were actually linked to the specific run via ticket_job_runs/remark_job_runs tables, completing the transition from date-based to explicit-link ticket system)
|
||||
- **HOTFIX**: Fixed Run Checks popup showing duplicate tickets (same ticket repeated multiple times) by removing unnecessary JOIN with ticket_scopes/remark_scopes tables and adding DISTINCT to prevent duplicate rows (root cause: tickets with multiple scopes created multiple result rows for same ticket via Cartesian product)
|
||||
|
||||
### Changed
|
||||
- Added debug logging to ticket linking function to troubleshoot resolved ticket propagation issues (writes to AuditLog table with event_type "ticket_link_debug", visible on Logging page, logs EVERY run import to show whether tickets were found and their resolved_at status, uses commit instead of flush to ensure persistence) - **LATER REMOVED** after ticket system was fixed
|
||||
- Reduced test email generation from 3 emails per status to 1 email per status for simpler testing (each button now creates exactly 1 test mail instead of 3)
|
||||
- Updated Settings Maintenance page text to reflect that test emails are Veeam only and 1 per button (changed from "3 emails simulating Veeam, Synology, and NAKIVO" to "1 Veeam Backup Job email" per status button)
|
||||
|
||||
### Removed
|
||||
- Removed debug logging from ticket linking function after successfully resolving all ticket propagation issues (the logging was temporarily added to troubleshoot why resolved tickets kept appearing on new runs, wrote to AuditLog with event_type "ticket_link_debug" showing ticket_id, code, resolved_at status for every run import, debug code preserved in backupchecks-system.md documentation for future use if similar issues arise)
|
||||
|
||||
### Release
|
||||
- **v0.1.26** - Official release consolidating all ticket system bug fixes from 2026-02-10 (see docs/changelog.md and changelog.py for customer-facing release notes)
|
||||
|
||||
## [2026-02-09]
|
||||
|
||||
### Added
|
||||
|
||||
@ -1,3 +1,67 @@
|
||||
## v0.1.26
|
||||
|
||||
This critical bug fix release resolves ticket system display issues where resolved tickets were incorrectly appearing on new runs across multiple pages. The ticket system has been completely transitioned from date-based logic to explicit link-based queries, ensuring resolved tickets stop appearing immediately after resolution while preserving audit trail for historical runs.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
**Ticket System - Resolved Ticket Display Issues:**
|
||||
|
||||
*Root Cause:*
|
||||
- Multiple pages used legacy date-based logic to determine if tickets should be displayed
|
||||
- Queries checked if `active_from_date <= run_date` and `resolved_at >= run_date` instead of checking explicit `ticket_job_runs` links
|
||||
- Result: Resolved tickets kept appearing on ALL runs between active_from_date and resolved_at, even runs created after resolution
|
||||
- Impact: Users saw resolved tickets on new runs, creating confusion about which issues were actually active
|
||||
|
||||
*Fixed Pages and Queries:*
|
||||
|
||||
1. **Ticket Linking (ticketing_utils.py)**
|
||||
- Fixed Autotask tickets not propagating to new runs after internal ticket resolution
|
||||
- Implemented independent Autotask propagation strategy: checks for most recent non-deleted and non-resolved Autotask ticket on job regardless of internal ticket status
|
||||
- Fixed internal tickets being linked to new runs after resolution by removing date-based "open" logic from ticket query
|
||||
- Tickets now only link to new runs if `COALESCE(ts.resolved_at, t.resolved_at) IS NULL` (genuinely unresolved)
|
||||
|
||||
2. **Job Details Page (routes_job_details.py)**
|
||||
- Fixed resolved tickets appearing on ALL runs for a job
|
||||
- Implemented two-source ticket display for proper audit trail:
|
||||
- Direct links via `ticket_job_runs` → always shown (preserves historical context)
|
||||
- Active window via `ticket_scopes` → only shown if unresolved
|
||||
- Result: Old runs keep their ticket references, new runs don't get resolved tickets
|
||||
|
||||
3. **Run Checks Main Page (routes_run_checks.py)**
|
||||
- Fixed ticket/remark indicators (🎫/💬) showing for jobs with resolved tickets
|
||||
- Removed date-based logic from indicator existence queries
|
||||
- Now only shows indicators if `COALESCE(ts.resolved_at, t.resolved_at) IS NULL` (genuinely unresolved)
|
||||
|
||||
4. **Run Checks Popup Modal (routes_api.py)**
|
||||
- Fixed popup showing resolved tickets for runs where they were never linked
|
||||
- Replaced date-based queries in `/api/job-runs/<run_id>/alerts` endpoint with explicit JOIN queries
|
||||
- Tickets query: Now uses `JOIN ticket_job_runs WHERE job_run_id = :run_id`
|
||||
- Remarks query: Now uses `JOIN remark_job_runs WHERE job_run_id = :run_id`
|
||||
- Removed unused parameters: `run_date`, `job_id`, `ui_tz` (no longer needed)
|
||||
- Result: Only shows tickets/remarks that were actually linked to that specific run
|
||||
|
||||
*Testing & Troubleshooting:*
|
||||
- Temporarily added debug logging to `link_open_internal_tickets_to_run` function
|
||||
- Wrote to AuditLog table with event_type "ticket_link_debug" for troubleshooting
|
||||
- Logged ticket_id, code, resolved_at status for every run import
|
||||
- Debug logging removed after successful resolution (code preserved in documentation)
|
||||
|
||||
**Test Email Generation:**
|
||||
- Reduced test email generation from 3 emails per status to 1 email per status
|
||||
- Each button now creates exactly 1 test mail instead of 3 for simpler testing
|
||||
|
||||
**User Interface:**
|
||||
- Updated Settings → Maintenance page text for test email generation
|
||||
- Changed description from "3 emails simulating Veeam, Synology, and NAKIVO" to "1 Veeam Backup Job email"
|
||||
- Updated button labels from "(3)" to "(1)" to match actual behavior
|
||||
|
||||
*Result:*
|
||||
- ✅ Resolved tickets stop appearing immediately after resolution
|
||||
- ✅ Consistent behavior across all pages (Job Details, Run Checks, Run Checks popup)
|
||||
- ✅ Audit trail preserved: old runs keep their historical ticket links
|
||||
- ✅ Clear distinction: new runs only show currently active (unresolved) tickets
|
||||
- ✅ All queries now use explicit link-based logic (no date comparisons)
|
||||
|
||||
## v0.1.25
|
||||
|
||||
This release focuses on parser improvements and maintenance enhancements, adding support for new notification types across Synology and Veeam backup systems while improving system usability with orphaned job cleanup and test email generation features.
|
||||
|
||||
@ -1 +1 @@
|
||||
v0.1.25
|
||||
v0.1.26
|
||||
|
||||
Loading…
Reference in New Issue
Block a user