Compare commits

..

No commits in common. "main" and "v0.2.5" have entirely different histories.
main ... v0.2.5

8 changed files with 8 additions and 232 deletions

View File

@ -1 +1 @@
v20260402-01 25

View File

@ -3,47 +3,6 @@ Changelog data structure for Backupchecks
""" """
CHANGELOG = [ CHANGELOG = [
{
"version": "v0.2.5",
"date": "2026-04-13",
"summary": "Consolidated release since v0.2.4 with manual schedule overrides, Autotask/remark synchronization improvements, Run Checks stability fixes, and refreshed operational documentation.",
"sections": [
{
"title": "Added",
"type": "feature",
"changes": [
"Manual schedule override support in Job Details (daily/weekly/monthly) with save/clear endpoint POST /jobs/<job_id>/schedule",
"Job Details now shows First backup detected based on earliest non-missed run",
"Remarks now support source and ticket_id metadata with migration and indexes",
"Autotask resolution text can be mirrored to active internal remarks with source=autotask_resolution and deduplication",
"Documentation Integrations section added with dedicated Cove Data Protection and Veeam Cloud Connect pages"
]
},
{
"title": "Changed",
"type": "improvement",
"changes": [
"Effective schedule resolution is now manual-first across Daily Jobs, Dashboard, Search, Job Details, and Run Checks missed-run generation",
"Missed-run grace window increased from +/- 1 hour to +/- 3 hours in Run Checks and Daily Jobs",
"Schedule inference now also includes Cove API runs (source_type=cove_api) in addition to mail-based runs",
"Run Checks now suppresses repeated Cove runs on the same local day after the first complete success run for that job/day",
"Ticket API active-state logic now uses effective status from both ticket-level and scope-level resolution",
"Settings/Autotask documentation pages were rewritten from placeholder content to current operational guidance"
]
},
{
"title": "Fixed",
"type": "bugfix",
"changes": [
"Run Checks modal mail visibility no longer remains hidden after navigating from Cove runs",
"Run Checks modal responsive behavior improved for smaller viewports so scrolling/content access remains usable",
"Run Checks Link existing Autotask ticket supports cross-company shared/umbrella tickets while preserving validation checks",
"Ticket copy action in Run Checks and Job Detail hardened with improved click handling and clipboard fallback",
"Autotask unresolved-ticket propagation to new runs fixed for edge cases where internal open-ticket rows are temporarily absent"
]
}
]
},
{ {
"version": "v0.2.4", "version": "v0.2.4",
"date": "2026-03-26", "date": "2026-03-26",

View File

@ -9,7 +9,7 @@ from datetime import date, datetime, time, timedelta, timezone
from flask import flash, jsonify, redirect, render_template, request, url_for from flask import flash, jsonify, redirect, render_template, request, url_for
from urllib.parse import urlencode, urljoin from urllib.parse import urlencode, urljoin
from flask_login import current_user, login_required from flask_login import current_user, login_required
from sqlalchemy import and_, bindparam, or_, func, text from sqlalchemy import and_, or_, func, text
from .routes_shared import ( from .routes_shared import (
_apply_overrides_to_run, _apply_overrides_to_run,
@ -167,118 +167,6 @@ def _is_hidden_3cx_non_backup(backup_software: str | None, backup_type: str | No
return bs == "3cx" and bt in {"update", "ssl certificate"} return bs == "3cx" and bt in {"update", "ssl certificate"}
def _chunked(values: list[int], size: int = 500) -> list[list[int]]:
if not values:
return []
return [values[i:i + size] for i in range(0, len(values), size)]
def _get_cove_complete_success_run_ids(run_ids: list[int]) -> set[int]:
"""Return Cove run ids that have at least one object and all object statuses are Success."""
if not run_ids:
return set()
complete_success_ids: set[int] = set()
for chunk in _chunked(run_ids, size=500):
rows = db.session.execute(
text(
"""
SELECT
rol.run_id AS run_id,
COUNT(*) AS obj_count,
SUM(CASE WHEN LOWER(COALESCE(rol.status, '')) = 'success' THEN 1 ELSE 0 END) AS success_count
FROM run_object_links rol
WHERE rol.run_id IN :run_ids
GROUP BY rol.run_id
"""
).bindparams(bindparam("run_ids", expanding=True)),
{"run_ids": chunk},
).mappings().all()
for rr in rows:
run_id = int(rr.get("run_id") or 0)
obj_count = int(rr.get("obj_count") or 0)
success_count = int(rr.get("success_count") or 0)
if run_id > 0 and obj_count > 0 and success_count == obj_count:
complete_success_ids.add(run_id)
return complete_success_ids
def _collect_suppressed_cove_run_ids(
*,
job_ids: list[int] | None = None,
include_reviewed: bool = False,
) -> set[int]:
"""Suppress Cove runs that occur later on a day after the first complete success run."""
q = (
db.session.query(
JobRun.id.label("run_id"),
JobRun.job_id.label("job_id"),
func.coalesce(JobRun.run_at, JobRun.created_at).label("run_ts"),
JobRun.status.label("status"),
)
.filter(JobRun.source_type == "cove_api")
)
if job_ids:
q = q.filter(JobRun.job_id.in_(job_ids))
if not include_reviewed:
q = q.filter(JobRun.reviewed_at.is_(None))
run_rows = q.order_by(
JobRun.job_id.asc(),
func.coalesce(JobRun.run_at, JobRun.created_at).asc(),
JobRun.id.asc(),
).all()
if not run_rows:
return set()
success_candidate_ids = [
int(r.run_id)
for r in run_rows
if ((r.status or "").strip().lower() == "success")
]
complete_success_ids = _get_cove_complete_success_run_ids(success_candidate_ids)
if not complete_success_ids:
return set()
cutoff_by_job_day: dict[tuple[int, date], datetime] = {}
for r in run_rows:
run_id = int(r.run_id or 0)
run_ts = getattr(r, "run_ts", None)
if run_id <= 0 or run_ts is None or run_id not in complete_success_ids:
continue
local_day = _to_amsterdam_date(run_ts)
if local_day is None:
continue
key = (int(r.job_id), local_day)
prev_cutoff = cutoff_by_job_day.get(key)
if prev_cutoff is None or run_ts < prev_cutoff:
cutoff_by_job_day[key] = run_ts
if not cutoff_by_job_day:
return set()
suppressed: set[int] = set()
for r in run_rows:
run_id = int(r.run_id or 0)
run_ts = getattr(r, "run_ts", None)
if run_id <= 0 or run_ts is None:
continue
local_day = _to_amsterdam_date(run_ts)
if local_day is None:
continue
cutoff = cutoff_by_job_day.get((int(r.job_id), local_day))
if cutoff is None:
continue
if run_ts > cutoff:
suppressed.add(run_id)
return suppressed
def _ensure_internal_ticket_for_autotask( def _ensure_internal_ticket_for_autotask(
*, *,
ticket_number: str, ticket_number: str,
@ -1449,13 +1337,6 @@ def run_checks_page():
| (func.coalesce(Job.job_name, "").ilike(pat, escape="\\")) | (func.coalesce(Job.job_name, "").ilike(pat, escape="\\"))
) )
# Restrict Cove suppression calculation to currently relevant jobs.
candidate_job_ids = [int(x) for (x,) in base.with_entities(Job.id).limit(4000).all()]
suppressed_cove_run_ids = _collect_suppressed_cove_run_ids(
job_ids=candidate_job_ids,
include_reviewed=include_reviewed,
)
# Runs to show in the overview: unreviewed (or all if admin toggle enabled) # Runs to show in the overview: unreviewed (or all if admin toggle enabled)
run_filter = [] run_filter = []
if not include_reviewed: if not include_reviewed:
@ -1507,8 +1388,6 @@ def run_checks_page():
) )
if run_filter: if run_filter:
agg = agg.filter(*run_filter) agg = agg.filter(*run_filter)
if suppressed_cove_run_ids:
agg = agg.filter(~JobRun.id.in_(list(suppressed_cove_run_ids)))
agg = agg.subquery() agg = agg.subquery()
@ -1560,8 +1439,6 @@ def run_checks_page():
) )
if run_filter: if run_filter:
s_q = s_q.filter(*run_filter) s_q = s_q.filter(*run_filter)
if suppressed_cove_run_ids:
s_q = s_q.filter(~JobRun.id.in_(list(suppressed_cove_run_ids)))
s_q = s_q.group_by(JobRun.job_id, JobRun.status, JobRun.missed, JobRun.override_applied) s_q = s_q.group_by(JobRun.job_id, JobRun.status, JobRun.missed, JobRun.override_applied)
for jid, status, missed, override_applied, cnt in s_q.all(): for jid, status, missed, override_applied, cnt in s_q.all():
@ -1821,13 +1698,6 @@ def run_checks_details():
if not include_reviewed: if not include_reviewed:
q = q.filter(JobRun.reviewed_at.is_(None)) q = q.filter(JobRun.reviewed_at.is_(None))
suppressed_cove_run_ids = _collect_suppressed_cove_run_ids(
job_ids=[int(job.id)],
include_reviewed=include_reviewed,
)
if suppressed_cove_run_ids:
q = q.filter(~JobRun.id.in_(list(suppressed_cove_run_ids)))
runs = q.order_by(func.coalesce(JobRun.run_at, JobRun.created_at).desc(), JobRun.id.desc()).limit(400).all() runs = q.order_by(func.coalesce(JobRun.run_at, JobRun.created_at).desc(), JobRun.id.desc()).limit(400).all()
# Prefetch internal ticket resolution info for Autotask-linked runs (Phase 2 UI). # Prefetch internal ticket resolution info for Autotask-linked runs (Phase 2 UI).

View File

@ -193,6 +193,9 @@ def link_open_internal_tickets_to_run(*, run: JobRun, job: Job) -> None:
except Exception: except Exception:
rows = [] rows = []
if not rows:
return
# Link all open tickets to this run (idempotent) # Link all open tickets to this run (idempotent)
for tid, code, t_resolved, ts_resolved 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(): if not TicketJobRun.query.filter_by(ticket_id=int(tid), job_run_id=int(run.id)).first():

View File

@ -892,16 +892,10 @@ table.addEventListener('change', function (e) {
opts = opts || {}; opts = opts || {};
opts.headers = opts.headers || {}; opts.headers = opts.headers || {};
opts.headers['Content-Type'] = 'application/json'; opts.headers['Content-Type'] = 'application/json';
opts.headers['X-Requested-With'] = 'XMLHttpRequest';
if (!opts.credentials) opts.credentials = 'same-origin';
return fetch(url, opts).then(function (r) { return fetch(url, opts).then(function (r) {
return r.text().then(function (txt) { return r.json().then(function (j) {
var j = null;
try { j = txt ? JSON.parse(txt) : null; } catch (_) { j = null; }
if (!r.ok || !j || j.status !== 'ok') { if (!r.ok || !j || j.status !== 'ok') {
var msg = (j && j.message) var msg = (j && j.message) ? j.message : ('Request failed (' + r.status + ')');
? j.message
: ((txt && txt.trim()) ? txt.trim() : ('Request failed (' + r.status + ')'));
throw new Error(msg); throw new Error(msg);
} }
return j; return j;

View File

@ -2,17 +2,6 @@
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-04-13]
### Fixed
- Run Checks now suppresses repeated Cove runs within the same local day once a **complete success run** has occurred:
- A complete success run is defined as `JobRun.status = Success` with at least one persisted run object and all object statuses equal to `Success`.
- For each Cove job/day, the first complete success run is treated as the cutoff; newer runs on that same day are hidden from Run Checks (both overview aggregation and modal details), regardless of whether they are `Success`, `Warning`, or `Failed/Error`.
- Sorting remains unchanged (`newest -> oldest`).
### Validation
- Test build executed with `./build-and-push.sh t` on 2026-04-13 and pushed `gitea.oskamp.info/ivooskamp/backupchecks:dev` (digest `sha256:520778f4b72643c1cd1815fa424317ee2dce182ccfcbea687f4ac711b3d00fb0`).
## [2026-04-02] ## [2026-04-02]
### Added ### Added

View File

@ -1,29 +1,3 @@
## v0.2.5
This release bundles all changes made since `v0.2.4`, including schedule management improvements, Autotask/remark synchronization, Run Checks stability updates, and a full documentation refresh.
### Added
- **Manual schedule overrides per job** — Job Details now supports saving and clearing manual schedules (`Daily`, `Weekly`, `Monthly`) via `POST /jobs/<job_id>/schedule`.
- **First backup detected in Job Details** — shows the earliest non-missed run timestamp for operational context.
- **Autotask resolution remark metadata** — remarks now support `source` and optional `ticket_id`, with migration and indexes.
- **Autotask resolution mirroring** — PSA ticket resolution text can be mirrored into internal active remarks (`source=autotask_resolution`) with deduplication.
- **Documentation Integrations section** — added dedicated pages for Cove Data Protection and Veeam Cloud Connect.
### Changed
- **Effective schedule resolution (manual-first)** — Daily Jobs, Dashboard, Search, Job Details and Run Checks missed-run logic now use effective schedule resolution (`manual` override first, inferred fallback).
- **Missed-run grace window widened** — tolerance changed from `±1 hour` to `±3 hours` in Daily Jobs and Run Checks.
- **Schedule inference coverage** — inference now also considers Cove API runs (`source_type='cove_api'`) next to mail-based runs.
- **Run Checks Cove deduplication in-day** — once a complete Cove success run is detected for a job/day, newer runs that day are suppressed in Run Checks overview/modal.
- **Tickets API active-state semantics** — effective active state now considers both ticket-level and scope-level resolution.
- **Documentation refresh** — Settings and Autotask documentation pages were replaced with current operational guidance; outdated TODO audit/Cove documents were archived.
### Fixed
- **Run Checks modal mail visibility** — navigating from Cove runs no longer leaves regular mail runs hidden.
- **Run Checks responsive behavior on smaller screens** — modal layout/scroll behavior improved so content and footer remain reachable.
- **Autotask link-existing cross-company support** — shared/umbrella tickets can be linked across companies while terminal/incomplete validations remain enforced.
- **Ticket copy action robustness** — click/copy handling improved in Run Checks and Job Details.
- **Autotask propagation to new runs** — fixed a propagation path where an open Autotask ticket could disappear on a next-day run if internal open-ticket rows were temporarily absent; unresolved ticket links are now propagated consistently.
## v0.2.4 ## v0.2.4
### Fixed ### Fixed

View File

@ -331,11 +331,6 @@ Cove run rows in the job detail history table are clickable even without a mail
- `routes_run_checks.py` returns `cove_summary` in the run payload for `source_type="cove_api"` runs - `routes_run_checks.py` returns `cove_summary` in the run payload for `source_type="cove_api"` runs
- Includes: account_name, computer_name, customer_name, readable datasource labels, last_run_at, status - Includes: account_name, computer_name, customer_name, readable datasource labels, last_run_at, status
- `run_checks.html` shows the Cove summary panel and hides the mail section - `run_checks.html` shows the Cove summary panel and hides the mail section
- Duplicate-day suppression for Cove runs:
- Runs are grouped per job per local day (Europe/Amsterdam date derived from run timestamp).
- A run is considered a "complete success" when `JobRun.status == Success` and persisted run objects exist with all object statuses equal to `Success`.
- Once the first complete success exists on that day, all newer Cove runs for the same day are hidden in Run Checks (overview aggregation + details modal), regardless of status (`Success`, `Warning`, `Failed/Error`).
- Sort order in the modal remains unchanged (`newest -> oldest`).
### Migrations ### Migrations
- `migrate_cove_integration()` — adds 8 columns to `system_settings`, `cove_account_id` to `jobs`, `source_type` + `external_id` to `job_runs`, dedup index on `job_runs.external_id` - `migrate_cove_integration()` — adds 8 columns to `system_settings`, `cove_account_id` to `jobs`, `source_type` + `external_id` to `job_runs`, dedup index on `job_runs.external_id`
@ -735,15 +730,6 @@ File: `build-and-push.sh`
## Recent Changes ## Recent Changes
### 2026-04-13
- **Run Checks Cove daily suppression** (`main/routes_run_checks.py`):
- Added Cove-specific filtering to suppress repeated same-day runs after the first complete success run.
- Complete success criteria: run status `Success`, object set present, all object statuses `Success`.
- Applied consistently to both Run Checks overview aggregation and details modal query.
- Local-day grouping uses the existing Amsterdam date helper for run timestamps.
- **Validation**:
- Test build executed with `./build-and-push.sh t`; pushed `gitea.oskamp.info/ivooskamp/backupchecks:dev` with digest `sha256:520778f4b72643c1cd1815fa424317ee2dce182ccfcbea687f4ac711b3d00fb0`.
### 2026-03-23 ### 2026-03-23
- **Synology ABB parser fix** (`parsers/synology.py`): ABB completion regex now also matches `has been completed` phrasing. - **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. - **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.
@ -833,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.