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 = [
|
||||
{
|
||||
"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",
|
||||
"date": "2026-03-20",
|
||||
|
||||
@ -45,6 +45,13 @@ COVE_COLUMNS = [
|
||||
"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
|
||||
STATUS_MAP: dict[int, str] = {
|
||||
1: "Warning", # In process
|
||||
@ -75,14 +82,21 @@ STATUS_LABELS: dict[int, str] = {
|
||||
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] = {
|
||||
"D1": "Files & Folders",
|
||||
"D01": "Files & Folders",
|
||||
"D2": "System State",
|
||||
"D02": "System State",
|
||||
"D6": "Network Shares",
|
||||
"D06": "Network Shares",
|
||||
"D10": "VssMsSql",
|
||||
"D11": "VssSharePoint",
|
||||
"D19": "M365 Exchange",
|
||||
"D20": "M365 OneDrive",
|
||||
"D5": "M365 SharePoint",
|
||||
"D05": "M365 SharePoint",
|
||||
"D23": "M365 Teams",
|
||||
}
|
||||
|
||||
@ -147,6 +161,7 @@ def _cove_enumerate(
|
||||
partner_id: int,
|
||||
start: int,
|
||||
count: int,
|
||||
columns: list[str] | None = None,
|
||||
) -> list[dict]:
|
||||
"""Call EnumerateAccountStatistics and return a list of account dicts.
|
||||
|
||||
@ -162,7 +177,7 @@ def _cove_enumerate(
|
||||
"PartnerId": partner_id,
|
||||
"StartRecordNumber": start,
|
||||
"RecordsCount": count,
|
||||
"Columns": COVE_COLUMNS,
|
||||
"Columns": columns or COVE_COLUMNS,
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -201,6 +216,12 @@ def _cove_enumerate(
|
||||
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:
|
||||
"""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
|
||||
|
||||
|
||||
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:
|
||||
"""Map a Cove status code (int) to a Backupchecks status string."""
|
||||
if code is None:
|
||||
@ -436,10 +493,25 @@ def run_cove_import(settings, include_reasons: bool = False):
|
||||
page_size = 250
|
||||
start = 0
|
||||
|
||||
base_columns = list(COVE_COLUMNS)
|
||||
extended_columns = base_columns + list(COVE_OPTIONAL_COLUMNS)
|
||||
use_optional_columns = True
|
||||
|
||||
while True:
|
||||
columns = extended_columns if use_optional_columns else base_columns
|
||||
try:
|
||||
accounts = _cove_enumerate(url, visa, partner_id, start, page_size)
|
||||
except CoveImportError:
|
||||
accounts = _cove_enumerate(url, visa, partner_id, start, page_size, columns=columns)
|
||||
except CoveImportError as exc:
|
||||
# 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:
|
||||
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.
|
||||
existing = JobRun.query.filter_by(job_id=job.id, external_id=external_id).first()
|
||||
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()
|
||||
return "skip_duplicate"
|
||||
|
||||
@ -638,14 +713,45 @@ def _persist_datasource_objects(
|
||||
observed_at: datetime,
|
||||
) -> None:
|
||||
"""Create run_object_links for each active datasource found in the account stats."""
|
||||
for ds_prefix, ds_label in DATASOURCE_LABELS.items():
|
||||
status_key = f"{ds_prefix}F00"
|
||||
status_code = flat.get(status_key)
|
||||
if status_code is None:
|
||||
# Use I78 as source-of-truth for active datasources so object count matches Cove UI.
|
||||
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)
|
||||
|
||||
uses_overall_fallback = status_code is None
|
||||
if status_code is None:
|
||||
status_code = overall_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
|
||||
|
||||
if uses_overall_fallback:
|
||||
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)}"
|
||||
|
||||
@ -19,6 +19,8 @@ _COVE_DATASOURCE_LABELS = {
|
||||
"D1": "Files & Folders",
|
||||
"D02": "System State",
|
||||
"D2": "System State",
|
||||
"D06": "Network Shares",
|
||||
"D6": "Network Shares",
|
||||
"D10": "VssMsSql",
|
||||
"D11": "VssSharePoint",
|
||||
"D19": "M365 Exchange",
|
||||
|
||||
@ -645,36 +645,110 @@ def _compose_autotask_ticket_description(
|
||||
lines.append("Summary:")
|
||||
lines.append(overall_message)
|
||||
lines.append("")
|
||||
lines.append("Multiple objects reported messages. See Backupchecks for full details.")
|
||||
else:
|
||||
# Fallback to object-level messages with a hard limit.
|
||||
limit = 10
|
||||
shown = 0
|
||||
total = 0
|
||||
|
||||
# Always include object-level details so technicians can immediately see
|
||||
# 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 err and not st:
|
||||
if not _is_problem_object(st, err):
|
||||
continue
|
||||
total += 1
|
||||
if shown >= limit:
|
||||
continue
|
||||
msg = err or st
|
||||
lines.append(f"- {name}: {msg}")
|
||||
shown += 1
|
||||
candidates.append(
|
||||
{
|
||||
"name": name,
|
||||
"status": st,
|
||||
"error_message": err,
|
||||
}
|
||||
)
|
||||
|
||||
if total == 0:
|
||||
if candidates:
|
||||
sorted_candidates = sorted(
|
||||
candidates,
|
||||
key=lambda x: (_object_priority(x), x["name"].lower()),
|
||||
)
|
||||
lines.append("Affected objects:")
|
||||
limit = 10
|
||||
shown = 0
|
||||
for o in sorted_candidates:
|
||||
if shown >= limit:
|
||||
break
|
||||
msg = o["error_message"] or o["status"]
|
||||
lines.append(f"- {o['name']}: {msg}")
|
||||
shown += 1
|
||||
if len(sorted_candidates) > shown:
|
||||
lines.append(f"And {int(len(sorted_candidates) - shown)} additional objects reported messages.")
|
||||
else:
|
||||
lines.append("No detailed object messages available. See Backupchecks for full details.")
|
||||
elif total > shown:
|
||||
lines.append(f"And {int(total - shown)} additional objects reported similar messages.")
|
||||
|
||||
lines.append("")
|
||||
lines.append(f"Backupchecks details: {job_link}")
|
||||
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.
|
||||
# A run within +/- 1 hour of the inferred schedule time counts as fulfilling the slot.
|
||||
MISSED_GRACE_WINDOW = timedelta(hours=1)
|
||||
@ -1790,6 +1864,43 @@ def api_run_checks_create_autotask_ticket():
|
||||
overall_message = (getattr(msg, "overall_message", None) or "") if msg else ""
|
||||
|
||||
objects_payload: list[dict[str, str]] = []
|
||||
|
||||
# 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:
|
||||
@ -1804,7 +1915,7 @@ def api_run_checks_create_autotask_ticket():
|
||||
}
|
||||
)
|
||||
|
||||
if (not objects_payload) and msg:
|
||||
if (not objects_payload) and msg and getattr(run, "source_type", None) != "cloud_connect":
|
||||
try:
|
||||
mos = MailObject.query.filter_by(mail_message_id=msg.id).order_by(MailObject.object_name.asc()).all()
|
||||
except Exception:
|
||||
@ -2193,12 +2304,38 @@ def api_run_checks_autotask_link_existing_ticket():
|
||||
db.session.rollback()
|
||||
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(
|
||||
{
|
||||
"status": "ok",
|
||||
"ticket_id": int(ticket_id),
|
||||
"ticket_number": tnum,
|
||||
"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"
|
||||
_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(?: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,
|
||||
)
|
||||
|
||||
|
||||
@ -173,6 +173,24 @@ body.bc-body {
|
||||
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-label-text { flex: 1; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
@ -412,6 +430,25 @@ body.bc-body {
|
||||
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 */
|
||||
.card {
|
||||
border-radius: var(--bc-radius);
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
{% for c in customers %}
|
||||
<tr>
|
||||
<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 }}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@ -183,6 +183,14 @@
|
||||
@media (min-width: 1400px) { .modal-xxl { max-width: 1400px; } }
|
||||
#run_msg_body_container_iframe { height: 55vh; }
|
||||
#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>
|
||||
|
||||
<!-- Inline popup modal for run message details -->
|
||||
@ -286,7 +294,7 @@
|
||||
</dl>
|
||||
</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">
|
||||
<h6 class="mb-0" id="jdm_mail_heading">Mail</h6>
|
||||
<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 mailBody = document.getElementById("jdm_mail_iframe_body");
|
||||
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 (modalEl) modalEl.classList.add("is-cove");
|
||||
var cs = data.cove_summary;
|
||||
document.getElementById("jdm_cove_account").textContent = cs.account_name || "—";
|
||||
document.getElementById("jdm_cove_computer").textContent = cs.computer_name || "—";
|
||||
@ -802,7 +813,9 @@ function renderObjects(objects) {
|
||||
if (mailHeading) mailHeading.style.display = "none";
|
||||
if (mailToggle) mailToggle.style.display = "none";
|
||||
if (mailBody) mailBody.style.display = "none";
|
||||
if (mailPanel) mailPanel.style.display = "none";
|
||||
} else if (data.cloud_connect_summary) {
|
||||
if (modalEl) modalEl.classList.remove("is-cove");
|
||||
var s = data.cloud_connect_summary;
|
||||
document.getElementById("jdm_cc_user").textContent = s.user || "";
|
||||
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 (mailToggle) { mailToggle.style.display = ""; mailToggle.textContent = "show"; }
|
||||
if (mailBody) mailBody.style.display = "none";
|
||||
if (mailPanel) mailPanel.style.display = "";
|
||||
if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || "");
|
||||
} else {
|
||||
if (covePanel) covePanel.style.display = "none";
|
||||
@ -823,6 +837,7 @@ function renderObjects(objects) {
|
||||
if (mailHeading) { mailHeading.style.display = ""; mailHeading.textContent = "Mail"; }
|
||||
if (mailToggle) mailToggle.style.display = "none";
|
||||
if (mailBody) mailBody.style.display = "";
|
||||
if (mailPanel) mailPanel.style.display = "";
|
||||
if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(data.body_html || "");
|
||||
}
|
||||
|
||||
|
||||
@ -228,6 +228,14 @@
|
||||
overflow: auto;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
#runChecksModal.is-cove .rcm-mail-panel {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#runChecksModal.is-cove .rcm-objects-scroll {
|
||||
max-height: 55vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
<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 mailBody = document.getElementById('rcm_mail_iframe_body');
|
||||
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 (modalEl) modalEl.classList.add('is-cove');
|
||||
var cs = run.cove_summary;
|
||||
document.getElementById('rcm_cove_account').textContent = cs.account_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 (mailToggle) mailToggle.style.display = 'none';
|
||||
if (mailBody) mailBody.style.display = 'none';
|
||||
if (mailPanel) mailPanel.style.display = 'none';
|
||||
} else if (run.cloud_connect_summary) {
|
||||
if (modalEl) modalEl.classList.remove('is-cove');
|
||||
var s = run.cloud_connect_summary;
|
||||
document.getElementById('rcc_user').textContent = s.user || '';
|
||||
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 (mailToggle) { mailToggle.style.display = ''; mailToggle.textContent = 'show'; }
|
||||
if (mailBody) mailBody.style.display = 'none';
|
||||
if (mailPanel) mailPanel.style.display = '';
|
||||
if (bodyFrame) bodyFrame.srcdoc = wrapMailHtml(run.body_html || '');
|
||||
} else {
|
||||
if (covePanel) covePanel.style.display = 'none';
|
||||
@ -1504,6 +1518,7 @@ table.addEventListener('change', function (e) {
|
||||
if (mailHeading) { mailHeading.style.display = ''; mailHeading.textContent = 'Mail'; }
|
||||
if (mailToggle) mailToggle.style.display = 'none';
|
||||
if (mailBody) mailBody.style.display = '';
|
||||
if (mailPanel) mailPanel.style.display = '';
|
||||
if (bodyFrame) {
|
||||
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.
|
||||
|
||||
## [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)
|
||||
|
||||
### 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
|
||||
|
||||
### Added
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# Technical Notes (Internal)
|
||||
|
||||
Last updated: 2026-03-20
|
||||
Last updated: 2026-03-23
|
||||
|
||||
## Purpose
|
||||
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
|
||||
|
||||
### 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)
|
||||
- **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.
|
||||
@ -806,3 +819,4 @@ File: `build-and-push.sh`
|
||||
### 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).
|
||||
- **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