Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cb608cb6b | |||
| d095a23944 |
@ -1 +1 @@
|
||||
25
|
||||
v20260402-01
|
||||
|
||||
@ -3,6 +3,47 @@ Changelog data structure for Backupchecks
|
||||
"""
|
||||
|
||||
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",
|
||||
"date": "2026-03-26",
|
||||
|
||||
@ -9,7 +9,7 @@ from datetime import date, datetime, time, timedelta, timezone
|
||||
from flask import flash, jsonify, redirect, render_template, request, url_for
|
||||
from urllib.parse import urlencode, urljoin
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy import and_, or_, func, text
|
||||
from sqlalchemy import and_, bindparam, or_, func, text
|
||||
|
||||
from .routes_shared import (
|
||||
_apply_overrides_to_run,
|
||||
@ -167,6 +167,118 @@ def _is_hidden_3cx_non_backup(backup_software: str | None, backup_type: str | No
|
||||
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(
|
||||
*,
|
||||
ticket_number: str,
|
||||
@ -1337,6 +1449,13 @@ def run_checks_page():
|
||||
| (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)
|
||||
run_filter = []
|
||||
if not include_reviewed:
|
||||
@ -1388,6 +1507,8 @@ def run_checks_page():
|
||||
)
|
||||
if 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()
|
||||
|
||||
@ -1439,6 +1560,8 @@ def run_checks_page():
|
||||
)
|
||||
if 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)
|
||||
|
||||
for jid, status, missed, override_applied, cnt in s_q.all():
|
||||
@ -1698,6 +1821,13 @@ def run_checks_details():
|
||||
if not include_reviewed:
|
||||
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()
|
||||
|
||||
# Prefetch internal ticket resolution info for Autotask-linked runs (Phase 2 UI).
|
||||
|
||||
@ -193,9 +193,6 @@ def link_open_internal_tickets_to_run(*, run: JobRun, job: Job) -> None:
|
||||
except Exception:
|
||||
rows = []
|
||||
|
||||
if not rows:
|
||||
return
|
||||
|
||||
# Link all open tickets to this run (idempotent)
|
||||
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():
|
||||
|
||||
@ -892,10 +892,16 @@ table.addEventListener('change', function (e) {
|
||||
opts = opts || {};
|
||||
opts.headers = opts.headers || {};
|
||||
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 r.json().then(function (j) {
|
||||
return r.text().then(function (txt) {
|
||||
var j = null;
|
||||
try { j = txt ? JSON.parse(txt) : null; } catch (_) { j = null; }
|
||||
if (!r.ok || !j || j.status !== 'ok') {
|
||||
var msg = (j && j.message) ? j.message : ('Request failed (' + r.status + ')');
|
||||
var msg = (j && j.message)
|
||||
? j.message
|
||||
: ((txt && txt.trim()) ? txt.trim() : ('Request failed (' + r.status + ')'));
|
||||
throw new Error(msg);
|
||||
}
|
||||
return j;
|
||||
|
||||
@ -2,6 +2,17 @@
|
||||
|
||||
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]
|
||||
|
||||
### Added
|
||||
|
||||
@ -1,3 +1,29 @@
|
||||
## 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
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -331,6 +331,11 @@ 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
|
||||
- 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
|
||||
- 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
|
||||
- `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`
|
||||
@ -730,6 +735,15 @@ File: `build-and-push.sh`
|
||||
|
||||
## 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
|
||||
- **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.
|
||||
@ -819,4 +833,3 @@ 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