Compare commits
No commits in common. "main" and "v0.2.5" have entirely different histories.
@ -1 +1 @@
|
|||||||
v20260402-01
|
25
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -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():
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user