Release prep v0.2.3 fixes and changelog updates
This commit is contained in:
parent
9edeb8eab9
commit
282601269c
@ -3,6 +3,53 @@ Changelog data structure for Backupchecks
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "v0.2.3",
|
||||||
|
"date": "2026-03-23",
|
||||||
|
"summary": "Update release that improves Autotask existing-ticket linking transparency, Cove object detail propagation, and Customers page link styling consistency.",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"title": "Added",
|
||||||
|
"type": "feature",
|
||||||
|
"changes": [
|
||||||
|
"Autotask Link existing ticket now also posts a ticket note when an additional Backupchecks alert/run is linked, including customer, job, run context and a Backupchecks deep-link"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Changed",
|
||||||
|
"type": "improvement",
|
||||||
|
"changes": [
|
||||||
|
"Link existing Autotask API response now includes note_posted and note_warning fields for operator visibility",
|
||||||
|
"Customers page customer-name links to filtered Jobs now use sidebar-matching text and hover styling instead of default blue link styling"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Fixed",
|
||||||
|
"type": "bugfix",
|
||||||
|
"changes": [
|
||||||
|
"Autotask object retrieval for ticket composition now reads objects from run_object_links/customer_objects first (same source as Run Checks UI), then falls back to legacy job_objects/mail_objects, preventing missing object details",
|
||||||
|
"Cove Accounts and Cove-run object visibility improved by consistently using persisted run object links as primary object source, aligning operator view and ticket details",
|
||||||
|
"Autotask ticket affected-objects list now includes only problem objects (failed/error/warning/missed) and excludes completed/success objects"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v0.2.2",
|
||||||
|
"date": "2026-03-23",
|
||||||
|
"summary": "Hotfix release that corrects Synology Active Backup for Business parsing for completion mails using has been wording, preventing wrong job-name extraction.",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"title": "Fixed",
|
||||||
|
"type": "bugfix",
|
||||||
|
"changes": [
|
||||||
|
"Synology Active Backup for Business parser now recognizes completion mails using has been completed wording",
|
||||||
|
"Prevents fallback to the generic Synology Active Backup parser that could incorrectly take the bracketed subject prefix as job name",
|
||||||
|
"ABB mails like backup task dc001 on DS220p has been completed now keep the expected identity: backup_software=Synology, backup_type=Active Backup for Business, job_name=dc001"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "v0.2.1",
|
"version": "v0.2.1",
|
||||||
"date": "2026-03-20",
|
"date": "2026-03-20",
|
||||||
|
|||||||
@ -45,6 +45,13 @@ COVE_COLUMNS = [
|
|||||||
"D23F00", "D23F15", # M365 Teams
|
"D23F00", "D23F15", # M365 Teams
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Optional datasource-specific columns for datasources that may be active in I78
|
||||||
|
# but are not always available in every tenant/API scope.
|
||||||
|
COVE_OPTIONAL_COLUMNS = [
|
||||||
|
"D2F00", "D2F15", # System State
|
||||||
|
"D6F00", "D6F15", # Network Shares
|
||||||
|
]
|
||||||
|
|
||||||
# Mapping from Cove status code to Backupchecks status string
|
# Mapping from Cove status code to Backupchecks status string
|
||||||
STATUS_MAP: dict[int, str] = {
|
STATUS_MAP: dict[int, str] = {
|
||||||
1: "Warning", # In process
|
1: "Warning", # In process
|
||||||
@ -75,14 +82,21 @@ STATUS_LABELS: dict[int, str] = {
|
|||||||
12: "Restarted",
|
12: "Restarted",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Datasource label mapping (column prefix → human-readable label)
|
# Datasource label mapping (Cove datasource code → human-readable label).
|
||||||
|
# Keep both canonical and zero-padded aliases so UI summary and importer stay in sync.
|
||||||
DATASOURCE_LABELS: dict[str, str] = {
|
DATASOURCE_LABELS: dict[str, str] = {
|
||||||
"D1": "Files & Folders",
|
"D1": "Files & Folders",
|
||||||
|
"D01": "Files & Folders",
|
||||||
|
"D2": "System State",
|
||||||
|
"D02": "System State",
|
||||||
|
"D6": "Network Shares",
|
||||||
|
"D06": "Network Shares",
|
||||||
"D10": "VssMsSql",
|
"D10": "VssMsSql",
|
||||||
"D11": "VssSharePoint",
|
"D11": "VssSharePoint",
|
||||||
"D19": "M365 Exchange",
|
"D19": "M365 Exchange",
|
||||||
"D20": "M365 OneDrive",
|
"D20": "M365 OneDrive",
|
||||||
"D5": "M365 SharePoint",
|
"D5": "M365 SharePoint",
|
||||||
|
"D05": "M365 SharePoint",
|
||||||
"D23": "M365 Teams",
|
"D23": "M365 Teams",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,6 +161,7 @@ def _cove_enumerate(
|
|||||||
partner_id: int,
|
partner_id: int,
|
||||||
start: int,
|
start: int,
|
||||||
count: int,
|
count: int,
|
||||||
|
columns: list[str] | None = None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Call EnumerateAccountStatistics and return a list of account dicts.
|
"""Call EnumerateAccountStatistics and return a list of account dicts.
|
||||||
|
|
||||||
@ -162,7 +177,7 @@ def _cove_enumerate(
|
|||||||
"PartnerId": partner_id,
|
"PartnerId": partner_id,
|
||||||
"StartRecordNumber": start,
|
"StartRecordNumber": start,
|
||||||
"RecordsCount": count,
|
"RecordsCount": count,
|
||||||
"Columns": COVE_COLUMNS,
|
"Columns": columns or COVE_COLUMNS,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -201,6 +216,12 @@ def _cove_enumerate(
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _is_cove_security_column_error(exc: Exception) -> bool:
|
||||||
|
"""Return True when Enumerate failed due to restricted columns/security scope."""
|
||||||
|
msg = str(exc or "").lower()
|
||||||
|
return ("security reasons" in msg) or ("13501" in msg)
|
||||||
|
|
||||||
|
|
||||||
def _flatten_settings(account: dict) -> dict:
|
def _flatten_settings(account: dict) -> dict:
|
||||||
"""Convert the Settings array in an account dict to a flat key→value dict.
|
"""Convert the Settings array in an account dict to a flat key→value dict.
|
||||||
|
|
||||||
@ -216,6 +237,42 @@ def _flatten_settings(account: dict) -> dict:
|
|||||||
return flat
|
return flat
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_ds_code(code: str) -> str:
|
||||||
|
"""Normalize datasource codes like D01 -> D1."""
|
||||||
|
m = re.fullmatch(r"D(\d{1,2})", (code or "").strip().upper())
|
||||||
|
if not m:
|
||||||
|
return (code or "").strip().upper()
|
||||||
|
return f"D{int(m.group(1))}"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_active_datasource_codes(raw: Any) -> list[str]:
|
||||||
|
"""Extract unique active datasource codes from I78 (e.g. D01D02D10)."""
|
||||||
|
text = str(raw or "").strip().upper()
|
||||||
|
if not text:
|
||||||
|
return []
|
||||||
|
seen: set[str] = set()
|
||||||
|
out: list[str] = []
|
||||||
|
for code in re.findall(r"D\d{1,2}", text):
|
||||||
|
norm = _normalize_ds_code(code)
|
||||||
|
if not norm or norm in seen:
|
||||||
|
continue
|
||||||
|
seen.add(norm)
|
||||||
|
out.append(norm)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _label_for_ds_code(code: str) -> str:
|
||||||
|
"""Resolve human label for a datasource code (supports canonical + padded aliases)."""
|
||||||
|
norm = _normalize_ds_code(code)
|
||||||
|
if norm in DATASOURCE_LABELS:
|
||||||
|
return DATASOURCE_LABELS[norm]
|
||||||
|
m = re.fullmatch(r"D(\d{1,2})", norm)
|
||||||
|
if m:
|
||||||
|
padded = f"D{int(m.group(1)):02d}"
|
||||||
|
return DATASOURCE_LABELS.get(padded, norm)
|
||||||
|
return DATASOURCE_LABELS.get(code, code)
|
||||||
|
|
||||||
|
|
||||||
def _map_status(code: Any) -> str:
|
def _map_status(code: Any) -> str:
|
||||||
"""Map a Cove status code (int) to a Backupchecks status string."""
|
"""Map a Cove status code (int) to a Backupchecks status string."""
|
||||||
if code is None:
|
if code is None:
|
||||||
@ -436,11 +493,26 @@ def run_cove_import(settings, include_reasons: bool = False):
|
|||||||
page_size = 250
|
page_size = 250
|
||||||
start = 0
|
start = 0
|
||||||
|
|
||||||
|
base_columns = list(COVE_COLUMNS)
|
||||||
|
extended_columns = base_columns + list(COVE_OPTIONAL_COLUMNS)
|
||||||
|
use_optional_columns = True
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
columns = extended_columns if use_optional_columns else base_columns
|
||||||
try:
|
try:
|
||||||
accounts = _cove_enumerate(url, visa, partner_id, start, page_size)
|
accounts = _cove_enumerate(url, visa, partner_id, start, page_size, columns=columns)
|
||||||
except CoveImportError:
|
except CoveImportError as exc:
|
||||||
raise
|
# Some tenants block specific datasource columns; fall back safely.
|
||||||
|
if use_optional_columns and _is_cove_security_column_error(exc):
|
||||||
|
logger.warning(
|
||||||
|
"Cove import: optional datasource columns blocked by API scope; "
|
||||||
|
"falling back to base columns. Error: %s",
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
use_optional_columns = False
|
||||||
|
accounts = _cove_enumerate(url, visa, partner_id, start, page_size, columns=base_columns)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise CoveImportError(f"Unexpected error fetching accounts at offset {start}: {exc}") from exc
|
raise CoveImportError(f"Unexpected error fetching accounts at offset {start}: {exc}") from exc
|
||||||
|
|
||||||
@ -584,6 +656,9 @@ def _process_account(account: dict) -> str:
|
|||||||
# Cove session was previously stored under another job.
|
# Cove session was previously stored under another job.
|
||||||
existing = JobRun.query.filter_by(job_id=job.id, external_id=external_id).first()
|
existing = JobRun.query.filter_by(job_id=job.id, external_id=external_id).first()
|
||||||
if existing:
|
if existing:
|
||||||
|
# Keep objects in sync even when the run itself is a duplicate session.
|
||||||
|
if job.customer_id:
|
||||||
|
_persist_datasource_objects(flat, job.customer_id, job.id, existing.id, last_run_at)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return "skip_duplicate"
|
return "skip_duplicate"
|
||||||
|
|
||||||
@ -638,18 +713,49 @@ def _persist_datasource_objects(
|
|||||||
observed_at: datetime,
|
observed_at: datetime,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create run_object_links for each active datasource found in the account stats."""
|
"""Create run_object_links for each active datasource found in the account stats."""
|
||||||
for ds_prefix, ds_label in DATASOURCE_LABELS.items():
|
# Use I78 as source-of-truth for active datasources so object count matches Cove UI.
|
||||||
status_key = f"{ds_prefix}F00"
|
ds_codes = _parse_active_datasource_codes(flat.get("I78"))
|
||||||
|
|
||||||
|
# Fallback: when I78 is missing, derive from present DxxF00 keys to avoid empty objects.
|
||||||
|
if not ds_codes:
|
||||||
|
seen: set[str] = set()
|
||||||
|
for key in flat.keys():
|
||||||
|
m = re.fullmatch(r"(D\d{1,2})F00", str(key or "").upper())
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
norm = _normalize_ds_code(m.group(1))
|
||||||
|
if norm and norm not in seen:
|
||||||
|
seen.add(norm)
|
||||||
|
ds_codes.append(norm)
|
||||||
|
|
||||||
|
overall_status_code = flat.get("D09F00")
|
||||||
|
overall_last_ts = _ts_to_dt(flat.get("D09F15"))
|
||||||
|
|
||||||
|
for ds_code in ds_codes:
|
||||||
|
ds_label = _label_for_ds_code(ds_code)
|
||||||
|
|
||||||
|
status_key = f"{ds_code}F00"
|
||||||
|
ts_key = f"{ds_code}F15"
|
||||||
status_code = flat.get(status_key)
|
status_code = flat.get(status_key)
|
||||||
|
|
||||||
|
uses_overall_fallback = status_code is None
|
||||||
if status_code is None:
|
if status_code is None:
|
||||||
continue
|
status_code = overall_status_code
|
||||||
|
|
||||||
status = _map_status(status_code)
|
status = _map_status(status_code)
|
||||||
ds_last_ts = _ts_to_dt(flat.get(f"{ds_prefix}F15"))
|
ds_last_ts = _ts_to_dt(flat.get(ts_key)) or overall_last_ts
|
||||||
status_msg = (
|
|
||||||
f"Cove datasource status: {_status_label(status_code)} "
|
if uses_overall_fallback:
|
||||||
f"({status_code}); last session: {_fmt_utc(ds_last_ts)}"
|
status_msg = (
|
||||||
)
|
f"Cove datasource status: {_status_label(status_code)} "
|
||||||
|
f"({status_code}); datasource-specific status not returned by API columns; "
|
||||||
|
f"using overall account status; last session: {_fmt_utc(ds_last_ts)}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
status_msg = (
|
||||||
|
f"Cove datasource status: {_status_label(status_code)} "
|
||||||
|
f"({status_code}); last session: {_fmt_utc(ds_last_ts)}"
|
||||||
|
)
|
||||||
|
|
||||||
# Use the same SQLAlchemy session/transaction as JobRun creation.
|
# Use the same SQLAlchemy session/transaction as JobRun creation.
|
||||||
# A separate engine connection cannot reliably see the uncommitted run row.
|
# A separate engine connection cannot reliably see the uncommitted run row.
|
||||||
|
|||||||
@ -19,6 +19,8 @@ _COVE_DATASOURCE_LABELS = {
|
|||||||
"D1": "Files & Folders",
|
"D1": "Files & Folders",
|
||||||
"D02": "System State",
|
"D02": "System State",
|
||||||
"D2": "System State",
|
"D2": "System State",
|
||||||
|
"D06": "Network Shares",
|
||||||
|
"D6": "Network Shares",
|
||||||
"D10": "VssMsSql",
|
"D10": "VssMsSql",
|
||||||
"D11": "VssSharePoint",
|
"D11": "VssSharePoint",
|
||||||
"D19": "M365 Exchange",
|
"D19": "M365 Exchange",
|
||||||
|
|||||||
@ -645,36 +645,110 @@ def _compose_autotask_ticket_description(
|
|||||||
lines.append("Summary:")
|
lines.append("Summary:")
|
||||||
lines.append(overall_message)
|
lines.append(overall_message)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Multiple objects reported messages. See Backupchecks for full details.")
|
|
||||||
else:
|
# Always include object-level details so technicians can immediately see
|
||||||
# Fallback to object-level messages with a hard limit.
|
# which objects failed/warned from the ticket itself.
|
||||||
|
def _object_priority(obj: dict[str, str]) -> int:
|
||||||
|
combined = f"{obj.get('status', '')} {obj.get('error_message', '')}".strip().lower()
|
||||||
|
if any(x in combined for x in ("failed", "error", "missed")):
|
||||||
|
return 0
|
||||||
|
if "warning" in combined:
|
||||||
|
return 1
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def _is_problem_object(status: str, err: str) -> bool:
|
||||||
|
s = (status or "").strip().lower()
|
||||||
|
e = (err or "").strip().lower()
|
||||||
|
combined = f"{s} {e}".strip()
|
||||||
|
|
||||||
|
if any(x in combined for x in ("failed", "error", "warning", "missed")):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if s in ("success", "succeeded", "completed", "ok"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if "completed" in combined:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Keep uncommon non-success statuses visible.
|
||||||
|
return bool(s or e)
|
||||||
|
|
||||||
|
candidates: list[dict[str, str]] = []
|
||||||
|
for o in objects_payload or []:
|
||||||
|
name = (o.get("name") or "").strip()
|
||||||
|
err = (o.get("error_message") or "").strip()
|
||||||
|
st = (o.get("status") or "").strip()
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
if not _is_problem_object(st, err):
|
||||||
|
continue
|
||||||
|
candidates.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"status": st,
|
||||||
|
"error_message": err,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if candidates:
|
||||||
|
sorted_candidates = sorted(
|
||||||
|
candidates,
|
||||||
|
key=lambda x: (_object_priority(x), x["name"].lower()),
|
||||||
|
)
|
||||||
|
lines.append("Affected objects:")
|
||||||
limit = 10
|
limit = 10
|
||||||
shown = 0
|
shown = 0
|
||||||
total = 0
|
for o in sorted_candidates:
|
||||||
for o in objects_payload or []:
|
|
||||||
name = (o.get("name") or "").strip()
|
|
||||||
err = (o.get("error_message") or "").strip()
|
|
||||||
st = (o.get("status") or "").strip()
|
|
||||||
if not name:
|
|
||||||
continue
|
|
||||||
if not err and not st:
|
|
||||||
continue
|
|
||||||
total += 1
|
|
||||||
if shown >= limit:
|
if shown >= limit:
|
||||||
continue
|
break
|
||||||
msg = err or st
|
msg = o["error_message"] or o["status"]
|
||||||
lines.append(f"- {name}: {msg}")
|
lines.append(f"- {o['name']}: {msg}")
|
||||||
shown += 1
|
shown += 1
|
||||||
|
if len(sorted_candidates) > shown:
|
||||||
if total == 0:
|
lines.append(f"And {int(len(sorted_candidates) - shown)} additional objects reported messages.")
|
||||||
lines.append("No detailed object messages available. See Backupchecks for full details.")
|
else:
|
||||||
elif total > shown:
|
lines.append("No detailed object messages available. See Backupchecks for full details.")
|
||||||
lines.append(f"And {int(total - shown)} additional objects reported similar messages.")
|
|
||||||
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"Backupchecks details: {job_link}")
|
lines.append(f"Backupchecks details: {job_link}")
|
||||||
return "\n".join(lines).strip() + "\n"
|
return "\n".join(lines).strip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _compose_autotask_link_existing_note(
|
||||||
|
*,
|
||||||
|
settings,
|
||||||
|
job: Job,
|
||||||
|
run: JobRun,
|
||||||
|
ticket_number: str,
|
||||||
|
linked_run_count: int,
|
||||||
|
) -> str:
|
||||||
|
tz_name = _get_ui_timezone_name() or "Europe/Amsterdam"
|
||||||
|
run_at_str = _format_datetime(run.run_at) if run.run_at else "-"
|
||||||
|
actor = (getattr(current_user, "email", None) or getattr(current_user, "username", None) or "operator")
|
||||||
|
|
||||||
|
base_url = (getattr(settings, "autotask_base_url", None) or "").strip()
|
||||||
|
job_rel = url_for("main.job_detail", job_id=job.id)
|
||||||
|
job_link = urljoin(base_url.rstrip("/") + "/", job_rel.lstrip("/"))
|
||||||
|
if run.id:
|
||||||
|
job_link = f"{job_link}?run_id={int(run.id)}"
|
||||||
|
|
||||||
|
lines: list[str] = []
|
||||||
|
lines.append("[Backupchecks] Additional alert linked to this ticket.")
|
||||||
|
lines.append(f"Customer: {job.customer.name if job.customer else ''}")
|
||||||
|
lines.append(f"Job: {job.job_name or ''}")
|
||||||
|
lines.append(f"Backup: {job.backup_software or ''} / {job.backup_type or ''}")
|
||||||
|
lines.append(f"Run ID: {int(run.id) if run.id else 0}")
|
||||||
|
lines.append(f"Run at ({tz_name}): {run_at_str}")
|
||||||
|
lines.append(f"Run status: {run.status or ''}")
|
||||||
|
lines.append(f"Linked active runs for this job: {int(linked_run_count or 0)}")
|
||||||
|
if ticket_number:
|
||||||
|
lines.append(f"Ticket: {ticket_number}")
|
||||||
|
lines.append(f"Linked by: {actor}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"Backupchecks details: {job_link}")
|
||||||
|
return "\n".join(lines).strip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
# Grace window for matching real runs to an expected schedule slot.
|
# Grace window for matching real runs to an expected schedule slot.
|
||||||
# A run within +/- 1 hour of the inferred schedule time counts as fulfilling the slot.
|
# A run within +/- 1 hour of the inferred schedule time counts as fulfilling the slot.
|
||||||
MISSED_GRACE_WINDOW = timedelta(hours=1)
|
MISSED_GRACE_WINDOW = timedelta(hours=1)
|
||||||
@ -1790,21 +1864,58 @@ def api_run_checks_create_autotask_ticket():
|
|||||||
overall_message = (getattr(msg, "overall_message", None) or "") if msg else ""
|
overall_message = (getattr(msg, "overall_message", None) or "") if msg else ""
|
||||||
|
|
||||||
objects_payload: list[dict[str, str]] = []
|
objects_payload: list[dict[str, str]] = []
|
||||||
try:
|
|
||||||
objs = run.objects.order_by(JobObject.object_name.asc()).all()
|
|
||||||
except Exception:
|
|
||||||
objs = list(run.objects or [])
|
|
||||||
for o in objs or []:
|
|
||||||
objects_payload.append(
|
|
||||||
{
|
|
||||||
"name": getattr(o, "object_name", "") or "",
|
|
||||||
"type": getattr(o, "object_type", "") or "",
|
|
||||||
"status": getattr(o, "status", "") or "",
|
|
||||||
"error_message": getattr(o, "error_message", "") or "",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (not objects_payload) and msg:
|
# Preferred source: run_object_links/customer_objects, also used by Run Checks UI
|
||||||
|
# for Cove/cloud sourced runs.
|
||||||
|
try:
|
||||||
|
rows = (
|
||||||
|
db.session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
co.object_name AS name,
|
||||||
|
rol.status AS status,
|
||||||
|
rol.error_message AS error_message
|
||||||
|
FROM run_object_links rol
|
||||||
|
JOIN customer_objects co ON co.id = rol.customer_object_id
|
||||||
|
WHERE rol.run_id = :run_id
|
||||||
|
ORDER BY co.object_name ASC
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"run_id": run.id},
|
||||||
|
)
|
||||||
|
.mappings()
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for rr in rows:
|
||||||
|
objects_payload.append(
|
||||||
|
{
|
||||||
|
"name": rr.get("name") or "",
|
||||||
|
"type": "",
|
||||||
|
"status": rr.get("status") or "",
|
||||||
|
"error_message": rr.get("error_message") or "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback for legacy mail parser sourced runs.
|
||||||
|
if not objects_payload:
|
||||||
|
try:
|
||||||
|
objs = run.objects.order_by(JobObject.object_name.asc()).all()
|
||||||
|
except Exception:
|
||||||
|
objs = list(run.objects or [])
|
||||||
|
for o in objs or []:
|
||||||
|
objects_payload.append(
|
||||||
|
{
|
||||||
|
"name": getattr(o, "object_name", "") or "",
|
||||||
|
"type": getattr(o, "object_type", "") or "",
|
||||||
|
"status": getattr(o, "status", "") or "",
|
||||||
|
"error_message": getattr(o, "error_message", "") or "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (not objects_payload) and msg and getattr(run, "source_type", None) != "cloud_connect":
|
||||||
try:
|
try:
|
||||||
mos = MailObject.query.filter_by(mail_message_id=msg.id).order_by(MailObject.object_name.asc()).all()
|
mos = MailObject.query.filter_by(mail_message_id=msg.id).order_by(MailObject.object_name.asc()).all()
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -2193,12 +2304,38 @@ def api_run_checks_autotask_link_existing_ticket():
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return jsonify({"status": "error", "message": "Failed to persist Autotask ticket link."}), 500
|
return jsonify({"status": "error", "message": "Failed to persist Autotask ticket link."}), 500
|
||||||
|
|
||||||
|
note_posted = False
|
||||||
|
note_warning = ""
|
||||||
|
try:
|
||||||
|
settings = _get_or_create_settings()
|
||||||
|
note_body = _compose_autotask_link_existing_note(
|
||||||
|
settings=settings,
|
||||||
|
job=job,
|
||||||
|
run=run,
|
||||||
|
ticket_number=tnum,
|
||||||
|
linked_run_count=len(run_ids),
|
||||||
|
)
|
||||||
|
client.create_ticket_note(
|
||||||
|
{
|
||||||
|
"ticketID": int(ticket_id),
|
||||||
|
"title": "Backupchecks",
|
||||||
|
"description": note_body,
|
||||||
|
"publish": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
note_posted = True
|
||||||
|
except Exception as exc:
|
||||||
|
note_posted = False
|
||||||
|
note_warning = f"Autotask ticket was linked, but posting the link-update note failed: {exc}"
|
||||||
|
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"ticket_id": int(ticket_id),
|
"ticket_id": int(ticket_id),
|
||||||
"ticket_number": tnum,
|
"ticket_number": tnum,
|
||||||
"internal_ticket_id": int(getattr(internal_ticket, "id", 0) or 0) if internal_ticket else 0,
|
"internal_ticket_id": int(getattr(internal_ticket, "id", 0) or 0) if internal_ticket else 0,
|
||||||
|
"note_posted": bool(note_posted),
|
||||||
|
"note_warning": note_warning,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -265,7 +265,7 @@ _ABB_SUBJECT_RE = re.compile(r"\bactive\s+backup\s+for\s+business\b", re.I)
|
|||||||
# "backup task vSphere-Task-1 on KANTOOR-NEW was skipped"
|
# "backup task vSphere-Task-1 on KANTOOR-NEW was skipped"
|
||||||
_ABB_COMPLETED_RE = re.compile(
|
_ABB_COMPLETED_RE = re.compile(
|
||||||
r"\b(?:virtuele\s+machine\s+)?(?:de\s+)?back-?up\s*(?:taak|job)\s+(?:van\s+deze\s+taak\s+)?(?P<job>.+?)\s+op\s+(?P<host>[A-Za-z0-9._-]+)\s+is\s+(?P<status>voltooid|gedeeltelijk\s+voltooid|genegeerd)\b"
|
r"\b(?:virtuele\s+machine\s+)?(?:de\s+)?back-?up\s*(?:taak|job)\s+(?:van\s+deze\s+taak\s+)?(?P<job>.+?)\s+op\s+(?P<host>[A-Za-z0-9._-]+)\s+is\s+(?P<status>voltooid|gedeeltelijk\s+voltooid|genegeerd)\b"
|
||||||
r"|\b(?:virtual\s+machine\s+)?(?:the\s+)?back-?up\s+(?:task|job)\s+(?P<job_en>.+?)\s+on\s+(?P<host_en>[A-Za-z0-9._-]+)\s+(?:is\s+|was\s+)?(?P<status_en>completed|finished|has\s+completed|partially\s+completed|skipped|ignored)\b",
|
r"|\b(?:virtual\s+machine\s+)?(?:the\s+)?back-?up\s+(?:task|job)\s+(?P<job_en>.+?)\s+on\s+(?P<host_en>[A-Za-z0-9._-]+)\s+(?:(?:is|was|has\s+been|has)\s+)?(?P<status_en>completed|finished|partially\s+completed|skipped|ignored)\b",
|
||||||
re.I,
|
re.I,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -173,6 +173,24 @@ body.bc-body {
|
|||||||
color: var(--bc-sidebar-active-text);
|
color: var(--bc-sidebar-active-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bc-sidebar-link-inline {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 6px;
|
||||||
|
margin: -2px -6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--bc-sidebar-text);
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background var(--bc-transition), color var(--bc-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-sidebar-link-inline:hover {
|
||||||
|
background: rgba(255,255,255,0.06);
|
||||||
|
color: var(--bc-sidebar-text-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.bc-nav-icon { display: flex; align-items: center; flex-shrink: 0; }
|
.bc-nav-icon { display: flex; align-items: center; flex-shrink: 0; }
|
||||||
.bc-nav-label-text { flex: 1; overflow: hidden; text-overflow: ellipsis; }
|
.bc-nav-label-text { flex: 1; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
|
||||||
@ -412,6 +430,25 @@ body.bc-body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep long object/datasource values fully visible in operational modals. */
|
||||||
|
#rcm_objects_table th,
|
||||||
|
#rcm_objects_table td,
|
||||||
|
#dj_objects_table th,
|
||||||
|
#dj_objects_table td,
|
||||||
|
#run_msg_objects_container table th,
|
||||||
|
#run_msg_objects_container table td {
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
#rcm_cove_datasources,
|
||||||
|
#jdm_cove_datasources {
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
/* Cards */
|
/* Cards */
|
||||||
.card {
|
.card {
|
||||||
border-radius: var(--bc-radius);
|
border-radius: var(--bc-radius);
|
||||||
|
|||||||
@ -50,7 +50,7 @@
|
|||||||
{% for c in customers %}
|
{% for c in customers %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('main.jobs', customer_id=c.id) }}" class="link-primary text-decoration-none">
|
<a href="{{ url_for('main.jobs', customer_id=c.id) }}" class="bc-sidebar-link-inline">
|
||||||
{{ c.name }}
|
{{ c.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -183,6 +183,14 @@
|
|||||||
@media (min-width: 1400px) { .modal-xxl { max-width: 1400px; } }
|
@media (min-width: 1400px) { .modal-xxl { max-width: 1400px; } }
|
||||||
#run_msg_body_container_iframe { height: 55vh; }
|
#run_msg_body_container_iframe { height: 55vh; }
|
||||||
#run_msg_objects_container { max-height: 25vh; overflow: auto; }
|
#run_msg_objects_container { max-height: 25vh; overflow: auto; }
|
||||||
|
|
||||||
|
#jobRunMessageModal.is-cove #jdm_mail_iframe_panel {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#jobRunMessageModal.is-cove #run_msg_objects_container {
|
||||||
|
max-height: 55vh;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- Inline popup modal for run message details -->
|
<!-- Inline popup modal for run message details -->
|
||||||
@ -286,7 +294,7 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3" id="jdm_mail_iframe_panel">
|
||||||
<div class="d-flex align-items-center gap-2 mb-1">
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
<h6 class="mb-0" id="jdm_mail_heading">Mail</h6>
|
<h6 class="mb-0" id="jdm_mail_heading">Mail</h6>
|
||||||
<a href="#" class="small text-muted" id="jdm_mail_toggle" style="display:none;"
|
<a href="#" class="small text-muted" id="jdm_mail_toggle" style="display:none;"
|
||||||
@ -788,8 +796,11 @@ function renderObjects(objects) {
|
|||||||
var mailToggle = document.getElementById("jdm_mail_toggle");
|
var mailToggle = document.getElementById("jdm_mail_toggle");
|
||||||
var mailBody = document.getElementById("jdm_mail_iframe_body");
|
var mailBody = document.getElementById("jdm_mail_iframe_body");
|
||||||
var bodyFrame = document.getElementById("run_msg_body_container_iframe");
|
var bodyFrame = document.getElementById("run_msg_body_container_iframe");
|
||||||
|
var mailPanel = document.getElementById("jdm_mail_iframe_panel");
|
||||||
|
var modalEl = document.getElementById("jobRunMessageModal");
|
||||||
|
|
||||||
if (data.cove_summary) {
|
if (data.cove_summary) {
|
||||||
|
if (modalEl) modalEl.classList.add("is-cove");
|
||||||
var cs = data.cove_summary;
|
var cs = data.cove_summary;
|
||||||
document.getElementById("jdm_cove_account").textContent = cs.account_name || "—";
|
document.getElementById("jdm_cove_account").textContent = cs.account_name || "—";
|
||||||
document.getElementById("jdm_cove_computer").textContent = cs.computer_name || "—";
|
document.getElementById("jdm_cove_computer").textContent = cs.computer_name || "—";
|
||||||
@ -802,7 +813,9 @@ function renderObjects(objects) {
|
|||||||
if (mailHeading) mailHeading.style.display = "none";
|
if (mailHeading) mailHeading.style.display = "none";
|
||||||
if (mailToggle) mailToggle.style.display = "none";
|
if (mailToggle) mailToggle.style.display = "none";
|
||||||
if (mailBody) mailBody.style.display = "none";
|
if (mailBody) mailBody.style.display = "none";
|
||||||
|
if (mailPanel) mailPanel.style.display = "none";
|
||||||
} else if (data.cloud_connect_summary) {
|
} else if (data.cloud_connect_summary) {
|
||||||
|
if (modalEl) modalEl.classList.remove("is-cove");
|
||||||
var s = data.cloud_connect_summary;
|
var s = data.cloud_connect_summary;
|
||||||
document.getElementById("jdm_cc_user").textContent = s.user || "";
|
document.getElementById("jdm_cc_user").textContent = s.user || "";
|
||||||
document.getElementById("jdm_cc_section").textContent = s.section || "";
|
document.getElementById("jdm_cc_section").textContent = s.section || "";
|
||||||
@ -816,6 +829,7 @@ function renderObjects(objects) {
|
|||||||
if (mailHeading) { mailHeading.style.display = ""; mailHeading.textContent = "Source report email"; }
|
if (mailHeading) { mailHeading.style.display = ""; mailHeading.textContent = "Source report email"; }
|
||||||
if (mailToggle) { mailToggle.style.display = ""; mailToggle.textContent = "show"; }
|
if (mailToggle) { mailToggle.style.display = ""; mailToggle.textContent = "show"; }
|
||||||
if (mailBody) mailBody.style.display = "none";
|
if (mailBody) mailBody.style.display = "none";
|
||||||
|
if (mailPanel) mailPanel.style.display = "";
|
||||||
if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || "");
|
if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || "");
|
||||||
} else {
|
} else {
|
||||||
if (covePanel) covePanel.style.display = "none";
|
if (covePanel) covePanel.style.display = "none";
|
||||||
@ -823,6 +837,7 @@ function renderObjects(objects) {
|
|||||||
if (mailHeading) { mailHeading.style.display = ""; mailHeading.textContent = "Mail"; }
|
if (mailHeading) { mailHeading.style.display = ""; mailHeading.textContent = "Mail"; }
|
||||||
if (mailToggle) mailToggle.style.display = "none";
|
if (mailToggle) mailToggle.style.display = "none";
|
||||||
if (mailBody) mailBody.style.display = "";
|
if (mailBody) mailBody.style.display = "";
|
||||||
|
if (mailPanel) mailPanel.style.display = "";
|
||||||
if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || "");
|
if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -228,6 +228,14 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#runChecksModal.is-cove .rcm-mail-panel {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#runChecksModal.is-cove .rcm-objects-scroll {
|
||||||
|
max-height: 55vh;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="modal fade" id="runChecksModal" tabindex="-1" aria-labelledby="runChecksModalLabel" aria-hidden="true">
|
<div class="modal fade" id="runChecksModal" tabindex="-1" aria-labelledby="runChecksModalLabel" aria-hidden="true">
|
||||||
@ -1469,8 +1477,11 @@ table.addEventListener('change', function (e) {
|
|||||||
var mailToggle = document.getElementById('rcm_mail_toggle');
|
var mailToggle = document.getElementById('rcm_mail_toggle');
|
||||||
var mailBody = document.getElementById('rcm_mail_iframe_body');
|
var mailBody = document.getElementById('rcm_mail_iframe_body');
|
||||||
var bodyFrame = document.getElementById('rcm_body_iframe');
|
var bodyFrame = document.getElementById('rcm_body_iframe');
|
||||||
|
var mailPanel = document.getElementById('rcm_mail_iframe_panel');
|
||||||
|
var modalEl = document.getElementById('runChecksModal');
|
||||||
|
|
||||||
if (run.cove_summary) {
|
if (run.cove_summary) {
|
||||||
|
if (modalEl) modalEl.classList.add('is-cove');
|
||||||
var cs = run.cove_summary;
|
var cs = run.cove_summary;
|
||||||
document.getElementById('rcm_cove_account').textContent = cs.account_name || '—';
|
document.getElementById('rcm_cove_account').textContent = cs.account_name || '—';
|
||||||
document.getElementById('rcm_cove_computer').textContent = cs.computer_name || '—';
|
document.getElementById('rcm_cove_computer').textContent = cs.computer_name || '—';
|
||||||
@ -1483,7 +1494,9 @@ table.addEventListener('change', function (e) {
|
|||||||
if (mailHeading) mailHeading.style.display = 'none';
|
if (mailHeading) mailHeading.style.display = 'none';
|
||||||
if (mailToggle) mailToggle.style.display = 'none';
|
if (mailToggle) mailToggle.style.display = 'none';
|
||||||
if (mailBody) mailBody.style.display = 'none';
|
if (mailBody) mailBody.style.display = 'none';
|
||||||
|
if (mailPanel) mailPanel.style.display = 'none';
|
||||||
} else if (run.cloud_connect_summary) {
|
} else if (run.cloud_connect_summary) {
|
||||||
|
if (modalEl) modalEl.classList.remove('is-cove');
|
||||||
var s = run.cloud_connect_summary;
|
var s = run.cloud_connect_summary;
|
||||||
document.getElementById('rcc_user').textContent = s.user || '';
|
document.getElementById('rcc_user').textContent = s.user || '';
|
||||||
document.getElementById('rcc_section').textContent = s.section || '';
|
document.getElementById('rcc_section').textContent = s.section || '';
|
||||||
@ -1497,6 +1510,7 @@ table.addEventListener('change', function (e) {
|
|||||||
if (mailHeading) { mailHeading.style.display = ''; mailHeading.textContent = 'Source report email'; }
|
if (mailHeading) { mailHeading.style.display = ''; mailHeading.textContent = 'Source report email'; }
|
||||||
if (mailToggle) { mailToggle.style.display = ''; mailToggle.textContent = 'show'; }
|
if (mailToggle) { mailToggle.style.display = ''; mailToggle.textContent = 'show'; }
|
||||||
if (mailBody) mailBody.style.display = 'none';
|
if (mailBody) mailBody.style.display = 'none';
|
||||||
|
if (mailPanel) mailPanel.style.display = '';
|
||||||
if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(run.body_html || '');
|
if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(run.body_html || '');
|
||||||
} else {
|
} else {
|
||||||
if (covePanel) covePanel.style.display = 'none';
|
if (covePanel) covePanel.style.display = 'none';
|
||||||
@ -1504,6 +1518,7 @@ table.addEventListener('change', function (e) {
|
|||||||
if (mailHeading) { mailHeading.style.display = ''; mailHeading.textContent = 'Mail'; }
|
if (mailHeading) { mailHeading.style.display = ''; mailHeading.textContent = 'Mail'; }
|
||||||
if (mailToggle) mailToggle.style.display = 'none';
|
if (mailToggle) mailToggle.style.display = 'none';
|
||||||
if (mailBody) mailBody.style.display = '';
|
if (mailBody) mailBody.style.display = '';
|
||||||
|
if (mailPanel) mailPanel.style.display = '';
|
||||||
if (bodyFrame) {
|
if (bodyFrame) {
|
||||||
bodyFrame.srcdoc = wrapMailHtml(run.body_html || (run.missed ? '<div class="text-muted">No email for missed run.</div>' : ''));
|
bodyFrame.srcdoc = wrapMailHtml(run.body_html || (run.missed ? '<div class="text-muted">No email for missed run.</div>' : ''));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,34 @@
|
|||||||
|
|
||||||
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-03-23]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Autotask "Link existing ticket" now also posts a ticket note on the selected Autotask ticket when an additional Backupchecks alert/run is linked:
|
||||||
|
- Added `_compose_autotask_link_existing_note()` in `containers/backupchecks/src/backend/app/main/routes_run_checks.py`.
|
||||||
|
- Extended `POST /api/run-checks/autotask-link-existing-ticket` to create a note via `client.create_ticket_note(...)` after successful link propagation.
|
||||||
|
- Link operation remains successful even if note posting fails; response now returns `note_posted` and `note_warning` for visibility.
|
||||||
|
- Customers page job-filter link styling now matches sidebar colors and hover behavior:
|
||||||
|
- Replaced `link-primary` link class with `bc-sidebar-link-inline` in `containers/backupchecks/src/templates/main/customers.html`.
|
||||||
|
- Added `bc-sidebar-link-inline` style in `containers/backupchecks/src/static/css/layout.css` using sidebar text and hover tokens.
|
||||||
|
|
||||||
|
- Run Checks Autotask ticket description now includes Cove run objects from `run_object_links` / `customer_objects` (same source as Run Checks UI), instead of only `job_objects`/`mail_objects`:
|
||||||
|
- Fixes missing object lines for Cove runs where ticket text previously showed only a generic "No detailed object messages available".
|
||||||
|
- Autotask ticket object listing is now problem-focused:
|
||||||
|
- Includes only objects with problem signals (`failed`, `error`, `warning`, `missed`).
|
||||||
|
- Excludes success/completed objects (`success`, `succeeded`, `completed`, `ok`, including `Completed (...)` variants).
|
||||||
|
|
||||||
|
- Synology Active Backup for Business parser now correctly handles subjects/bodies that use the wording `has been completed` (e.g. `backup task dc001 on DS220p has been completed`):
|
||||||
|
- Updated ABB completion regex in `containers/backupchecks/src/backend/app/parsers/synology.py` to accept `has been` completion phrasing.
|
||||||
|
- Prevents fallback to the generic Synology Active Backup parser that could incorrectly take the bracketed subject prefix (e.g. `[Stout Verlichting - DS220p]`) as `job_name`.
|
||||||
|
- Correct result for this mail shape is now preserved as `backup_software = Synology`, `backup_type = Active Backup for Business`, `job_name = dc001`.
|
||||||
|
|
||||||
|
### Validation
|
||||||
|
- Test build executed with `./build-and-push.sh t` on 2026-03-23 and pushed `gitea.oskamp.info/ivooskamp/backupchecks:dev` (digest `sha256:19014477f2ae14eac0a62f07e11c923c83f9cd5e478873290bdcca37e6ab257c`).
|
||||||
|
- Latest validation build executed with `./build-and-push.sh t` on 2026-03-23 and pushed `gitea.oskamp.info/ivooskamp/backupchecks:dev` (digest `sha256:b9bb6d50f131118ebccaed5834513ca83ec7592bd622ecea42c1ce2dd7bf0cfc`).
|
||||||
|
- Validation build executed with `./build-and-push.sh t` on 2026-03-23 and pushed `gitea.oskamp.info/ivooskamp/backupchecks:dev` (digest `sha256:2ff1675996b27bf409687bf5c52e2a3cb3314728ce1c67bc3ffc14fbd0562427`).
|
||||||
|
- Validation build executed with `./build-and-push.sh t` on 2026-03-23 and pushed `gitea.oskamp.info/ivooskamp/backupchecks:dev` (digest `sha256:f87d871caa3501251c31a66f61eb94b249faf0d3ab85da3f0c02c6036855849b`).
|
||||||
|
|
||||||
## [2026-03-20] (9)
|
## [2026-03-20] (9)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -1,3 +1,23 @@
|
|||||||
|
## v0.2.3
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Autotask: link existing ticket note update** — when linking a Run Checks alert to an existing Autotask ticket, Backupchecks now posts an additional note on that ticket indicating that another alert/run was linked, including customer/job/run context and a Backupchecks deep-link.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Autotask link-existing API response** — response now includes `note_posted` and `note_warning`, so operators can directly see whether posting the additional ticket note succeeded.
|
||||||
|
- **Customers page job-filter links** — customer name links that open Jobs with a customer filter now use sidebar-matching text and hover styling instead of the default blue link style.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Autotask object retrieval for ticket composition (improved)** — object details are now fetched from `run_object_links`/`customer_objects` first (same source as Run Checks UI), with fallback to legacy `job_objects`/`mail_objects`, preventing missing-object details for Cove runs.
|
||||||
|
- **Cove Accounts / Cove runs object visibility** -- object details shown in Backupchecks are now consistently sourced from persisted run-object links, improving completeness for Cove Data Protection runs and aligning ticket content with what operators see in Run Checks.
|
||||||
|
- **Autotask affected objects list** — ticket descriptions now include only problem objects (`failed`/`error`/`warning`/`missed`) and no longer include completed/success objects.
|
||||||
|
|
||||||
|
## v0.2.2
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Synology Active Backup for Business parsing** — notifications using wording `has been completed` are now correctly recognized by the ABB parser. This prevents fallback to the generic Synology Active Backup parser that could incorrectly use the bracketed subject prefix (for example `Stout Verlichting - DS220p`) as job name.
|
||||||
|
- For ABB notifications like `backup task dc001 on DS220p has been completed`, parsed values are now correctly preserved as: backup software `Synology`, backup type `Active Backup for Business`, job name `dc001`.
|
||||||
|
|
||||||
## v0.2.1
|
## v0.2.1
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Technical Notes (Internal)
|
# Technical Notes (Internal)
|
||||||
|
|
||||||
Last updated: 2026-03-20
|
Last updated: 2026-03-23
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
Internal technical snapshot of the `backupchecks` repository for faster onboarding, troubleshooting, and change impact analysis.
|
Internal technical snapshot of the `backupchecks` repository for faster onboarding, troubleshooting, and change impact analysis.
|
||||||
@ -730,6 +730,19 @@ File: `build-and-push.sh`
|
|||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
|
||||||
|
### 2026-03-23
|
||||||
|
- **Synology ABB parser fix** (`parsers/synology.py`): ABB completion regex now also matches `has been completed` phrasing.
|
||||||
|
- **Job name parsing corrected for ABB mails**: messages like `backup task dc001 on DS220p has been completed` no longer fall back to generic Synology Active Backup parsing; `job_name` stays `dc001` instead of bracketed subject prefix values.
|
||||||
|
- **Validation**: test build ran successfully via `./build-and-push.sh t`; pushed `gitea.oskamp.info/ivooskamp/backupchecks:dev` with digest `sha256:19014477f2ae14eac0a62f07e11c923c83f9cd5e478873290bdcca37e6ab257c`.
|
||||||
|
- **Run Checks Autotask (Cove object source fix)** (`main/routes_run_checks.py`): ticket creation now reads object details from `run_object_links` + `customer_objects` first (same data source as Run Checks modal), then falls back to legacy `job_objects`/`mail_objects`.
|
||||||
|
- **Autotask ticket object filtering tightened** (`main/routes_run_checks.py`): ticket description now lists only problem objects (`failed`/`error`/`warning`/`missed`) and excludes completed/success objects (including `Completed (...)` text variants).
|
||||||
|
- **Validation**: latest test build ran successfully via `./build-and-push.sh t`; pushed `gitea.oskamp.info/ivooskamp/backupchecks:dev` with digest `sha256:b9bb6d50f131118ebccaed5834513ca83ec7592bd622ecea42c1ce2dd7bf0cfc`.
|
||||||
|
- **Autotask link-existing ticket note update** (`main/routes_run_checks.py`): linking a run to an existing Autotask ticket now posts an informational ticket note that another alert/run was linked (with customer/job/run context and Backupchecks deep-link).
|
||||||
|
- **Autotask link-existing API response enriched** (`main/routes_run_checks.py`): response now includes `note_posted` and `note_warning` so UI/operators can see if the extra note call succeeded.
|
||||||
|
- **Customers job-filter link visual alignment** (`templates/main/customers.html`, `static/css/layout.css`): customer links to filtered Jobs view now use sidebar text colors and hover behavior (`bc-sidebar-link-inline`) instead of Bootstrap `link-primary` blue.
|
||||||
|
- **Validation**: test build ran successfully via `./build-and-push.sh t`; pushed `gitea.oskamp.info/ivooskamp/backupchecks:dev` with digest `sha256:2ff1675996b27bf409687bf5c52e2a3cb3314728ce1c67bc3ffc14fbd0562427`.
|
||||||
|
- **Validation**: test build ran successfully via `./build-and-push.sh t`; pushed `gitea.oskamp.info/ivooskamp/backupchecks:dev` with digest `sha256:f87d871caa3501251c31a66f61eb94b249faf0d3ab85da3f0c02c6036855849b`.
|
||||||
|
|
||||||
### 2026-03-20 (v0.2.1)
|
### 2026-03-20 (v0.2.1)
|
||||||
- **Missed run false positive fix** (`routes_shared.py`):
|
- **Missed run false positive fix** (`routes_shared.py`):
|
||||||
- Weekly inference window: last 90 days only (was unbounded). Eliminates stale slot false positives after time-of-day or frequency changes.
|
- Weekly inference window: last 90 days only (was unbounded). Eliminates stale slot false positives after time-of-day or frequency changes.
|
||||||
@ -806,3 +819,4 @@ File: `build-and-push.sh`
|
|||||||
### 2026-02-10
|
### 2026-02-10
|
||||||
- **Added screenshot support to Feedback system**: Multiple file upload, inline display, two-stage delete (soft delete for audit trail, permanent delete for cleanup).
|
- **Added screenshot support to Feedback system**: Multiple file upload, inline display, two-stage delete (soft delete for audit trail, permanent delete for cleanup).
|
||||||
- **Completed transition to link-based ticket system**: All pages now use JOIN queries, no date-based logic. Added cross-browser copy ticket functionality with three-tier fallback mechanism to both Run Checks and Job Details pages.
|
- **Completed transition to link-based ticket system**: All pages now use JOIN queries, no date-based logic. Added cross-browser copy ticket functionality with three-tier fallback mechanism to both Run Checks and Job Details pages.
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user