35 KiB
Technical Notes (Internal)
Last updated: 2026-02-23 (late)
Purpose
Internal technical snapshot of the backupchecks repository for faster onboarding, troubleshooting, and change impact analysis.
Repository Overview
- Application: Flask web app with SQLAlchemy and Flask-Migrate.
- Runtime: Containerized (Docker), deployed via Docker Compose stack.
- Primary source code location:
containers/backupchecks/src. - The project also contains extensive functional documentation in
docs/and multiple roadmap TODO files at repository root.
Main Structure
containers/backupchecks/Dockerfile: Python 3.12-slim image, startsgunicornwithbackend.app:create_app().containers/backupchecks/requirements.txt: Flask stack + PostgreSQL driver + reporting libraries (reportlab,Markdown).containers/backupchecks/src/backend/app: backend domain logic, routes, parsers, models, migrations.containers/backupchecks/src/templates: Jinja templates for auth/main/documentation pages.containers/backupchecks/src/static: CSS, images, favicon.deploy/backupchecks-stack.yml: compose stack withbackupchecks,postgres,adminer.build-and-push.sh: release/test build script with version bumping, tags, and image push.docs/: functional design, changelogs, migration notes, API notes.
Application Architecture (Current Observation)
- Factory pattern:
create_app()incontainers/backupchecks/src/backend/app/__init__.py. - Blueprints:
auth_bpfor authentication.main_bpfor core functionality.doc_bpfor internal documentation pages.
- Database initialization at startup:
db.create_all()run_migrations()
- Background tasks:
start_auto_importer(app)starts the automatic mail importer thread.start_cove_importer(app)starts the Cove Data Protection polling thread (started only whencove_import_enabledis set).
- Health endpoint:
GET /healthreturns{ "status": "ok" }.
Functional Processing Flow
- Import:
- Email is fetched via Microsoft Graph API.
- Parse:
- Parser selection through registry + software-specific parser implementations.
- Approve:
- New jobs first appear in Inbox for initial customer assignment.
- Auto-process:
- Subsequent emails for known jobs automatically create
JobRunrecords.
- Subsequent emails for known jobs automatically create
- Monitor:
- Runs appear in Daily Jobs and Run Checks.
- Review:
- Manual review removes items from the unreviewed operational queue.
Configuration and Runtime
- Config is built from environment variables in
containers/backupchecks/src/backend/app/config.py. - Important variables:
APP_SECRET_KEYAPP_ENVAPP_PORTPOSTGRES_DBPOSTGRES_USERPOSTGRES_PASSWORDDB_HOSTDB_PORT
- Database URI pattern:
postgresql+psycopg2://<user>:<pass>@<host>:<port>/<db>
- Default timezone in config:
Europe/Amsterdam.
Data Model (High-level)
File: containers/backupchecks/src/backend/app/models.py
- Auth/users:
Userwith role(s), active role in session.
- System settings:
SystemSettingswith Graph/mail settings, import settings, UI timezone, dashboard policy, sandbox flag.- Autotask configuration and cache fields are present.
- Cove Data Protection fields:
cove_enabled,cove_api_url,cove_api_username,cove_api_password,cove_import_enabled,cove_import_interval_minutes,cove_partner_id,cove_last_import_at. - Microsoft Entra SSO fields:
entra_sso_enabled,entra_tenant_id,entra_client_id,entra_client_secret,entra_redirect_uri,entra_allowed_domain,entra_allowed_group_ids,entra_auto_provision_users.
- Logging:
AuditLog(legacy aliasAdminLog).
- Domain:
Customer,Job,JobRun,OverrideMailMessage,MailObjectCoveAccount(Cove staging table — see Cove integration section)Ticket,TicketScope,TicketJobRunRemark,RemarkScope,RemarkJobRunFeedbackItem,FeedbackVote,FeedbackReply,FeedbackAttachment
Foreign Key Relationships & Deletion Order
Critical deletion order to avoid constraint violations (used in "Delete all jobs" maintenance route):
- Unlink staging accounts:
UPDATE cove_accounts SET job_id = NULL,UPDATE cloud_connect_accounts SET job_id = NULL - Unlink mails:
UPDATE mail_messages SET job_id = NULL, location = 'inbox' - Delete FK tables referencing
job_runs:remark_job_runs,ticket_job_runs,run_object_links,job_run_review_events - Delete FK tables referencing
jobs:job_object_links,ticket_scopes,remark_scopes,overrides DELETE FROM job_runsDELETE FROM jobs
Note: always use direct SQL (DELETE FROM) for bulk deletions — ORM-level deletes load all objects into Python memory and time out on large datasets.
Key Model Fields
MailMessage model:
from_address(NOTsender!) - sender emailsubject- email subjecttext_body- plain text contenthtml_body- HTML contentreceived_at- timestamplocation- inbox/processed/deletedjob_id- link to Job (nullable)
Job model:
customer_id- FK to Customerjob_name- parsed from emailbackup_software- e.g., "Veeam", "Synology", "Cove Data Protection"backup_type- e.g., "Backup Job", "Active Backup"cove_account_id- (nullable int) links this job to a Cove AccountId
JobRun model:
source_type- NULL = email (backwards compat),"cove_api"for Cove-imported runsexternal_id- deduplication key for Cove runs:"cove-{account_id}-{run_ts}"
Parser Architecture
- Folder:
containers/backupchecks/src/backend/app/parsers/ - Two layers:
registry.py:- matching/documentation/visibility on
/parsers. - examples must stay generic (no customer names).
- matching/documentation/visibility on
- parser files (
veeam.py,synology.py, etc.):- actual detection and parsing logic.
- return structured output: software, type, job name, status, objects.
- Practical rule:
- extend patterns by adding, not replacing (backward compatibility).
Parser Types
Informational Parsers:
- DSM Updates, Account Protection, Firmware Updates
- Set appropriate backup_type (e.g., "Updates", "Firmware Update")
- Do NOT participate in schedule learning
- Usually still visible in Run Checks for awareness
- Exception: non-backup 3CX informational types (
Update,SSL Certificate) are hidden from Run Checks
Regular Parsers:
- Backup jobs (Veeam, Synology Active Backup, NAKIVO, etc.)
- Participate in schedule learning (daily/weekly/monthly detection)
- Generate missed runs when expected runs don't occur
Example: Synology Updates Parser (synology.py)
- Handles multiple update notification types under same job:
- DSM automatic update cancelled
- Packages out-of-date
- Combined notifications (DSM + packages)
- Detection patterns:
- DSM: "Automatische DSM-update", "DSM-update op", "automatic DSM update"
- Packages: "Packages on", "out-of-date", "Package Center"
- Hostname extraction from multiple patterns
- Returns: backup_type "Updates", job_name "Synology Automatic Update"
Cove Data Protection Integration
Overview
Cove (N-able) Data Protection is a cloud backup platform. Backupchecks integrates with it via the Cove JSON-RPC API, following the same inbox-style staging flow as email imports.
Files
containers/backupchecks/src/backend/app/cove_importer.py– API client, account processing, JobRun creationcontainers/backupchecks/src/backend/app/cove_importer_service.py– background polling threadcontainers/backupchecks/src/backend/app/main/routes_cove.py–/cove/accountsroutescontainers/backupchecks/src/templates/main/cove_accounts.html– inbox-style accounts page
API Details
- Endpoint:
https://api.backup.management/jsonapi(JSON-RPC 2.0) - Login:
POSTwith{"jsonrpc":"2.0","id":"jsonrpc","method":"Login","params":{"username":"...","password":"..."}}- Returns
visaat top level (data["visa"]), not insideresult - Returns
PartnerIdinsideresult
- Returns
- EnumerateAccountStatistics:
POSTwith visa in payload,query(lowercase) withPartnerId,StartRecordNumber,RecordsCount,Columns - Settings format per account:
[{"D09F00": "5"}, {"I1": "device name"}, ...]— list of single-key dicts, flatten withdict.update(item)
Column Codes
| Code | Meaning |
|---|---|
I1 |
Account/device name |
I18 |
Computer name |
I8 |
Customer/partner name |
I78 |
Active datasource label |
D09F00 |
Overall last session status code |
D09F09 |
Last successful session timestamp (Unix) |
D09F15 |
Last session end timestamp (Unix) |
D09F08 |
28-day colorbar string |
D1F00/F15 |
Files & Folders status/timestamp |
D10F00/F15 |
VssMsSql |
D11F00/F15 |
VssSharePoint |
D19F00/F15 |
M365 Exchange |
D20F00/F15 |
M365 OneDrive |
D5F00/F15 |
M365 SharePoint |
D23F00/F15 |
M365 Teams |
Status Code Mapping
| Cove code | Meaning | Backupchecks status |
|---|---|---|
| 1 | In process | Warning |
| 2 | Failed | Error |
| 3 | Aborted | Error |
| 5 | Completed | Success |
| 6 | Interrupted | Error |
| 7 | Not started | Warning |
| 8 | Completed with errors | Warning |
| 9 | In progress with faults | Warning |
| 10 | Over quota | Error |
| 11 | No selection | Warning |
| 12 | Restarted | Warning |
Inbox-Style Flow (mirrors email import)
- Cove importer fetches all accounts via paginated
EnumerateAccountStatistics(250/page). - Every account is upserted into the
cove_accountsstaging table (always, regardless of job link). - Accounts without a
job_idappear on/cove/accounts("Cove Accounts" page) for admin action. - Admin can:
- Create new job – creates a
Jobwithbackup_software="Cove Data Protection"and links it. - Link to existing job – sets
job.cove_account_idandcove_acc.job_id.
- Create new job – creates a
- Linking an account triggers an immediate import attempt; linked accounts then generate
JobRunrecords (deduplicated per job viajob_id + external_id). - Per-datasource objects are persisted to
customer_objects,job_object_links,run_object_links.
CoveAccount Model
class CoveAccount(db.Model):
__tablename__ = "cove_accounts"
id # PK
account_id # Cove AccountId (unique)
account_name # I1
computer_name # I18
customer_name # I8
datasource_types # I78
last_status_code # D09F00 (int)
last_run_at # D09F15 (datetime)
colorbar_28d # D09F08
job_id # FK → jobs.id (nullable — None = unmatched)
first_seen_at
last_seen_at
job # relationship → Job
Deduplication
external_id = f"cove-{account_id}-{run_ts}" where run_ts is Unix timestamp from D09F15 (fallback to D09F09 when needed).
Deduplication is enforced per linked job:
- check
JobRun.query.filter_by(job_id=job.id, external_id=external_id).first() - this prevents cross-job collisions when accounts are relinked.
Historical (colorbar) runs use external_id = f"cove-colorbar-{account_id}-{date_str}" (e.g. cove-colorbar-4378343-2026-03-15).
Historical Backfill (28-day colorbar)
When a new run is created for a job, _backfill_colorbar_runs() is called to reconstruct up to 27 additional days of history from the D09F08 colorbar field.
- Each character in the colorbar = one day's status (oldest first, position 0 = 27 days ago, last position = today)
- Status 0 = no backup that day → skipped
run_at= same time-of-day as the real run, but on the historical date- Idempotent:
external_iddeduplication prevents duplicates on subsequent imports - Only creates runs for days where a backup actually ran (non-zero status code)
Run Enrichment
- Cove-created
JobRun.remarkcontains account/computer/customer and last status/timestamp summary. - Per-datasource run object records include:
- mapped Backupchecks status
- readable status details in
error_message - datasource-level session timestamp in
observed_at
Cove Accounts UI Notes
/cove/accountsderives display fields to align with existing job logic:backup_software:Cove Data Protectionbackup_type:Server,Workstation, orMicrosoft 365job_name: based on Cove account/computer fallback- readable datasource labels instead of raw
I78code stream
computer_nameis shown in both unmatched and matched account tables.
Background Thread
cove_importer_service.py — same pattern as auto_importer_service.py:
- Thread name:
"cove_importer" - Checks
settings.cove_import_enabled - Interval:
settings.cove_import_interval_minutes(default 30) - Calls
run_cove_import(settings)which returns(total, created, skipped, errors)
Settings UI
Settings → Integrations → Cove section:
- Enable toggle, API URL, username, password (masked, only overwritten if non-empty)
- Import enabled + interval
- "Test Connection" button (AJAX →
POST /settings/cove/test-connection) returns{ok, partner_id, message} - "Run import now" button (→
POST /settings/cove/run-now) triggers manual import
Routes
| Route | Method | Description |
|---|---|---|
/cove/accounts |
GET | Inbox-style page: unmatched + matched accounts |
/cove/accounts/<id>/link |
POST | action=create or action=link |
/cove/accounts/<id>/unlink |
POST | Removes job link, puts account back in unmatched |
/settings/cove/test-connection |
POST | AJAX: verify credentials, save partner_id |
/settings/cove/run-now |
POST | Manual import trigger |
Migrations
migrate_cove_integration()— adds 8 columns tosystem_settings,cove_account_idtojobs,source_type+external_idtojob_runs, dedup index onjob_runs.external_idmigrate_cove_accounts_table()— createscove_accountstable with indexes
Veeam Cloud Connect Integration
Overview
Veeam Cloud Connect sends a daily HTML report email (one email per provider, covering all tenants). The importer parses the HTML table and upserts each tenant row into the cloud_connect_accounts staging table. Linked accounts create JobRun records; unlinked accounts appear on the Cloud Connect Accounts review page.
Files
app/cloud_connect_importer.py— HTML parser + upsert logicapp/main/routes_cloud_connect.py—/cloud-connect/accountspage, link/unlink/scan-inbox routestemplates/main/cloud_connect_accounts.html— accounts review page
Email Structure
- One email covers all tenants (unlike Cove, which sends one email per account)
- Sections:
Backup,Replication,Agent(detected from<p>tags withfont-size: 18px) - Each section has a table with columns: User, #VM / #WS+#Server (Agent), Repo Name, Repository, Total quota, Used space, Free space, Last active, Expiry
Status Mapping (row background colour)
| Colour | Status |
|---|---|
#fb9895 / #ff9999 / #f4cccc / #ffb3b3 |
Failed |
#ffd96c / #fff2cc / #ffe599 / #f9cb9c |
Warning |
| white / no background | Success |
⚠ TODO — Last active detection logic: The importer currently trusts Veeam's row colour as the sole status indicator (white = Success). An earlier version downgraded white rows to Warning when "Last active" exceeded 3 days, but this was removed because Veeam itself determines row colour. It is still an open question whether backupchecks should apply its own independent "last active" threshold on top of Veeam's colour — e.g. to catch cases where Veeam shows a white row for a backup that hasn't run in a long time. Needs review before production use.
Staging Table: cloud_connect_accounts
Unique key: (user, section, repo_name) — one row per tenant × section × repository.
A single user can have multiple repositories (e.g. a standard repo + an immutable repo), each stored as a separate account row and each linkable to a separate Backupchecks job.
Deduplication
external_id = f"vcc-{user}-{section}-{repo_slug}-{report_date}" — one JobRun per job per repository per report date. Re-importing the same email updates the run status and refreshes run_object_links.
Run Enrichment
source_type = "cloud_connect"on everyJobRuncreated by the importer_persist_cc_objects()upserts the repository as acustomer_object(typecloud_connect_repo) and links it viarun_object_links— mirrors the Cove datasource pattern and enables per-run reporting- The
run_atis set to the mail'sreceived_at(not today) so historical re-imports land on the correct date
Job Detail Popup (job_detail.html)
For CC runs the popup shows a structured Cloud Connect summary panel (User/Section/Repository/Used/Quota/Free/Last active/Status) instead of the raw report email. The report email is still accessible via a collapsible "Source report email" toggle. Only the single per-run repository object is shown (from run_object_links), not all tenants from the shared mail.
Scan Inbox
POST /cloud-connect/accounts/scan-inbox — re-processes all stored CC report emails (location ≠ deleted). Safe to run multiple times; deduplication prevents duplicate runs.
Migrations
migrate_cloud_connect_accounts_table()— createscloud_connect_accountstable with(user, section)unique keymigrate_cc_accounts_repo_unique_key()— extends unique key to(user, section, repo_name), makesrepo_nameNOT NULL DEFAULT''
Ticketing and Autotask (Critical Rules)
Two Ticket Types
-
Internal Tickets (tickets table)
- Created manually or via Autotask integration
- Stored in
ticketstable withticket_code(e.g., "T20250123.0001") - Linked to runs via
ticket_job_runsmany-to-many table - Scoped to jobs via
ticket_scopestable - Have
resolved_atfield for resolution tracking - Auto-propagation: Automatically linked to new runs via
link_open_internal_tickets_to_run
-
Autotask Tickets (job_runs columns)
- Created via Run Checks modal → "Create Autotask Ticket"
- Stored directly in JobRun columns:
autotask_ticket_id,autotask_ticket_number, etc.
- When created, also creates matching internal ticket for legacy UI compatibility
Microsoft Entra SSO (Current State)
Status
- Implemented but marked Untested in Backupchecks.
Routes
GET /auth/entra/login– starts Entra auth code flow.GET /auth/entra/callback– exchanges code, maps/provisions local user, logs in session./auth/logout– Entra-aware logout redirect when user authenticated via Entra.
Access Controls
- Optional tenant/domain restriction (
entra_allowed_domain). - Optional Entra security-group allowlist (
entra_allowed_group_ids) based on group object IDs. - Group overage / missing groups claim blocks login intentionally when group gate is enabled.
Local User Mapping
- Primary mapping by
preferred_username/UPN/email. - Optional auto-provision (
entra_auto_provision_users) creates local Viewer users for unknown identities.
Documentation
- Built-in docs page:
/documentation/settings/entra-sso - Includes configuration steps and explicit untested warning.
Navbar Notes (Latest)
- To reduce split-screen overflow, nav is compacted by grouping:
- admin-only links under
Admindropdown - secondary non-admin links under
Moredropdown
- admin-only links under
- Primary operational links remain visible (notably
Run Checks). - Viewer role now exposes
CustomersandJobsdirectly in navbar.- Have
autotask_ticket_deleted_atfield for deletion tracking - Resolution tracked via matching internal ticket's
resolved_atfield - Auto-propagation: Linked to new runs via two-strategy approach
- Have
Ticket Propagation to New Runs
When a new JobRun is created (via email import OR missed run generation), link_open_internal_tickets_to_run ensures:
Strategy 1: Internal ticket linking
- Query finds tickets where:
COALESCE(ts.resolved_at, t.resolved_at) IS NULL - Creates
ticket_job_runslinks automatically - Tickets remain visible until explicitly resolved
- NO date-based logic - resolved = immediately hidden from new runs
Strategy 2: Autotask ticket propagation (independent)
- Check if internal ticket code exists → find matching Autotask run → copy ticket info
- If no match, directly search for most recent Autotask ticket on job where:
autotask_ticket_deleted_at IS NULL(not deleted in PSA)- Internal ticket
resolved_at IS NULL(not resolved in PSA)
- Copy
autotask_ticket_id,autotask_ticket_number,created_at,created_by_user_idto new run
Where Ticket Linking is Called
link_open_internal_tickets_to_run is invoked in three locations:
- Email-based runs:
routes_inbox.pyandmail_importer.py- after creating JobRun from parsed email - Missed runs:
routes_run_checks.pyin_ensure_missed_runs_for_job- after creating missed JobRun records- Weekly schedule: After creating weekly missed run (with flush to get run.id)
- Monthly schedule: After creating monthly missed run (with flush to get run.id)
- Critical: Without this call, missed runs don't get ticket propagation!
Display Logic - Link-Based System
All pages use explicit link-based queries (no date-based logic):
Job Details Page:
- Two sources for ticket display:
- Direct links (
ticket_job_runs WHERE job_run_id = X) → always show (audit trail) - Active window (
ticket_scopes WHERE job_id = Y AND resolved_at IS NULL) → only unresolved
- Direct links (
- Result: Old runs keep their ticket references, new runs don't get resolved tickets
Run Checks Main Page (Indicators 🎫):
- Query:
ticket_scopes JOIN tickets WHERE job_id = X AND resolved_at IS NULL - Only shows indicator if unresolved tickets exist for the job
Run Checks Popup Modal:
- API:
/api/job-runs/<run_id>/alerts - Two-source ticket display:
- Direct links:
tickets JOIN ticket_job_runs WHERE job_run_id = X - Job-level scope:
tickets JOIN ticket_scopes WHERE job_id = Y AND resolved_at IS NULL AND active_from_date <= run_date
- Direct links:
- Prevents duplicates by tracking seen ticket IDs
- Shows newly created tickets immediately (via scope) without waiting for resolve action
- Two-source remark display:
- Direct links:
remarks JOIN remark_job_runs WHERE job_run_id = X - Job-level scope:
remarks JOIN remark_scopes WHERE job_id = Y AND resolved_at IS NULL AND active_from_date <= run_date(with timezone-safe fallback fromstart_date)
- Direct links:
- Prevents duplicates by tracking seen remark IDs
Debug Logging for Ticket Linking (Reference)
If you need to debug ticket linking issues, add this to link_open_internal_tickets_to_run in ticketing_utils.py after the rows query:
try:
from .models import AuditLog
details = []
if rows:
for tid, code, t_resolved, ts_resolved in rows:
details.append(f"ticket_id={tid}, code={code}, t.resolved_at={t_resolved}, ts.resolved_at={ts_resolved}")
else:
details.append("No open tickets found for this job")
audit = AuditLog(
user="system", event_type="ticket_link_debug",
message=f"link_open_internal_tickets_to_run called: run_id={run.id}, job_id={job.id}, found={len(rows)} ticket(s)",
details="\n".join(details)
)
db.session.add(audit)
db.session.commit()
except Exception:
pass
Visible on Logging page under event_type = "ticket_link_debug". Remove after debugging.
Resolved vs Deleted
- Resolved: Ticket completed in Autotask (tracked in internal
tickets.resolved_at)- Stops propagating to new runs
- Ticket still exists in PSA
- Synced via PSA polling
- Deleted: Ticket removed from Autotask (tracked in
job_runs.autotask_ticket_deleted_at)- Also stops propagating
- Ticket no longer exists in PSA
- Rare operation
Critical Rules
- ❌ NEVER use date-based resolved logic:
resolved_at >= run_dateORactive_from_date <= run_date - ✅ Only show tickets that are ACTUALLY LINKED via
ticket_job_runstable - ✅ Resolved tickets stop linking immediately when resolved
- ✅ Old links preserved for audit trail (visible on old runs)
- ✅ All queries must use explicit JOIN to link tables
- ✅ Consistency: All pages use same "resolved = NULL" logic
- ✅ CRITICAL: Preserve description field during Autotask updates - must include "description" in optional_fields list
UI and UX Notes
Navbar
- Fixed-top positioning
- Collapses on mobile (hamburger menu)
- Dynamic padding adjustment via JavaScript (measures navbar height, adjusts main content padding-top)
- Role-based menu items (Admin sees more than Operator/Viewer)
Status Badges
- Success: Green
- Warning: Yellow/Orange
- Failed/Error: Red
- Override applied: Blue badge
- Reviewed: Checkmark indicator
Ticket Copy Functionality
- Copy button (⧉) available on both Run Checks and Job Details pages
- Allows quick copying of ticket numbers to clipboard
- Cross-browser compatible with three-tier fallback mechanism:
- Modern Clipboard API:
navigator.clipboard.writeText()- works in modern browsers with HTTPS - Legacy execCommand:
document.execCommand('copy')- fallback for older browsers and Edge - Prompt fallback:
window.prompt()- last resort if clipboard access fails
- Modern Clipboard API:
- Visual feedback: button changes to ✓ checkmark for 800ms after successful copy
- Implementation uses hidden textarea for execCommand method to ensure compatibility
- No user interaction required in modern browsers (direct copy)
Checkbox Behavior
- All checkboxes on Inbox and Run Checks pages use
autocomplete="off" - Prevents browser from auto-selecting checkboxes after page reload
- Fixes issue where deleting items would cause same number of new items to be selected
Customers to Jobs Navigation (2026-02-16)
- Customers page links each customer name to filtered Jobs view:
GET /jobs?customer_id=<customer_id>
- Jobs route behavior:
- Accepts optional
customer_idquery parameter inroutes_jobs.py. - If set: returns jobs for that customer only.
- If not set: keeps default filter that hides jobs linked to inactive customers.
- Accepts optional
- Jobs UI behavior:
- Shows active filter banner with selected customer name.
- Provides "Clear filter" action back to unfiltered
/jobs.
- Templates touched:
templates/main/customers.htmltemplates/main/jobs.html
Global Grouped Search (2026-02-16)
- New route:
GET /searchinmain/routes_search.py
- New UI:
- Navbar search form in
templates/layout/base.html - Grouped result page in
templates/main/search.html
- Navbar search form in
- Search behavior:
- Case-insensitive matching (
ILIKE). *wildcard is supported and translated to SQL%.- Automatic contains behavior is applied per term (
*term*) when wildcard not explicitly set. - Multi-term queries use AND across terms and OR across configured columns within each section.
- Per-section pagination is supported via query params:
p_inbox,p_customers,p_jobs,p_daily_jobs,p_run_checks,p_tickets,p_remarks,p_overrides,p_reports. - Pagination keeps search state for all sections while browsing one section.
- "Open
" links pass qto destination overview pages so page-level filtering matches the search term.
- Case-insensitive matching (
- Grouped sections:
- Inbox, Customers, Jobs, Daily Jobs, Run Checks, Tickets, Remarks, Existing overrides, Reports.
- Daily Jobs search result details:
- Meta now includes expected run time, success indicator, and run count for the selected day.
- Link now opens Daily Jobs with modal auto-open using
open_job_idquery parameter (same modal flow as clicking a row in Daily Jobs).
- Access control:
- Search results are role-aware and only show sections/data the active role can access.
run_checksresults are restricted toadmin/operator.reportssupportsadmin/operator/viewer/reporter.
- Current performance strategy:
- Per-section limit (
SEARCH_LIMIT_PER_SECTION = 10), with total count per section. - No schema migration required for V1.
- Per-section limit (
Feedback Module with Screenshots
- Models:
FeedbackItem,FeedbackVote,FeedbackReply,FeedbackAttachment. - Attachments:
- multiple uploads, type validation, per-file size limits, storage in database (BYTEA).
Validation Snapshot
- 2026-02-16: Test build + push succeeded via
update-and-build.sh t. - Pushed image:
gitea.oskamp.info/ivooskamp/backupchecks:dev. - 2026-02-16: Test build + push succeeded on branch
v20260216-02-global-search. - Pushed image digest:
sha256:6996675b9529426fe2ad58b5f353479623f3ebe24b34552c17ad0421d8a7ee0f. - 2026-02-16: Additional test build + push cycles succeeded on
v20260216-02-global-search. - Latest pushed image digest:
sha256:8ec8bfcbb928e282182fa223ce8bf7f92112d20e79f4a8602d015991700df5d7. - 2026-02-16: Additional test build + push cycles succeeded after search enhancements.
- Latest pushed image digest:
sha256:b36b5cdd4bc7c4dadedca0534f1904a6e12b5b97abc4f12bc51e42921976f061. - Delete strategy:
- soft delete by default,
- permanent delete only for admins and only after soft delete.
Deployment and Operations
- Stack exposes:
- app on
8080 - adminer on
8081
- app on
- PostgreSQL persistent volume:
/docker/appdata/backupchecks/backupchecks-postgres:/var/lib/postgresql/data
deploy/backupchecks-stack.ymlalso contains example.envvariables at the bottom.
Build/Release Flow
File: build-and-push.sh
- Bump options:
1patch,2minor,3major,ttest.
- Release build:
- update
version.txt - commit + tag + push
- docker push of
:<version>,:dev,:latest
- update
- Test build:
- only
:dev - no commit/tag.
- only
- Services are discovered under
containers/*with Dockerfile-per-service.
Technical Observations / Attention Points
README.mdis currently empty; quick-start entry context is missing.LICENSEis currently empty.docs/architecture.mdis currently empty.deploy/backupchecks-stack.ymlcontains hardcoded example values (Changeme), with risk if used without proper secrets management.- The app performs DB initialization + migrations at startup; for larger schema changes this can impact startup time/robustness.
- There is significant parser and ticketing complexity; route changes carry regression risk without targeted testing.
- For Autotask update calls, the
descriptionfield must be explicitly preserved to prevent unintended NULL overwrite. - Security hygiene remains important:
- no customer names in parser examples/source,
- no hardcoded credentials.
Quick References
- App entrypoint:
containers/backupchecks/src/backend/app/main.py - App factory:
containers/backupchecks/src/backend/app/__init__.py - Config:
containers/backupchecks/src/backend/app/config.py - Models:
containers/backupchecks/src/backend/app/models.py - Parsers:
containers/backupchecks/src/backend/app/parsers/registry.py - Ticketing utilities:
containers/backupchecks/src/backend/app/ticketing_utils.py - Run Checks routes:
containers/backupchecks/src/backend/app/main/routes_run_checks.py - Cove importer:
containers/backupchecks/src/backend/app/cove_importer.py - Cove routes:
containers/backupchecks/src/backend/app/main/routes_cove.py - Compose stack:
deploy/backupchecks-stack.yml - Build script:
build-and-push.sh
Recent Changes
2026-02-23
- Cove Data Protection full integration:
cove_importer.py– Cove API client (login, paginated enumeration, status mapping, deduplication, per-datasource object persistence)cove_importer_service.py– background polling thread (same pattern asauto_importer_service.py)CoveAccountstaging model +migrate_cove_accounts_table()migrationSystemSettings– 8 new Cove fields,Job–cove_account_id,JobRun–source_type+external_idroutes_cove.py– inbox-style/cove/accountswith link/unlink routescove_accounts.html– unmatched accounts shown first with Bootstrap modals (create job / link to existing), matched accounts with Unlink- Settings > Integrations: Cove section with test connection (AJAX) and manual import trigger
- Navbar: "Cove Accounts" link for admin/operator when
cove_enabled
- Cove API key findings (from test script + N-able support):
- Visa is returned at top level of login response, not inside
result - Settings per account are a list of single-key dicts
[{"D09F00":"5"}, ...]— flatten withflat.update(item) - EnumerateAccountStatistics params must use lowercase
querykey andRecordsCount(notRecordCount) - Login params must use lowercase
username/password - D02/D03 are legacy; use D10/D11 or D09 (Total) instead
- Visa is returned at top level of login response, not inside
2026-02-19
- Added 3CX Update parser support:
threecx.pynow recognizes subject3CX Notification: Update Successful - <host>and stores it as informational with:backup_software = 3CXbackup_type = Updateoverall_status = Success
- 3CX informational schedule behavior:
3CX / Updateand3CX / SSL Certificateare excluded from schedule inference inroutes_shared.py(no Expected/Missed generation).
- Run Checks visibility scope (3CX-only):
- Run Checks now hides only non-backup 3CX informational jobs (
Update,SSL Certificate). - Other backup software/types remain visible and unchanged.
- Run Checks now hides only non-backup 3CX informational jobs (
- Fixed remark visibility mismatch:
/api/job-runs/<run_id>/alertsnow loads remarks from both:remark_job_runs(explicit run links),remark_scopes(active job-scoped remarks),
- with duplicate prevention by remark ID.
- This resolves cases where the remark indicator appeared but remarks were not shown in Run Checks modal or Job Details modal.
2026-02-13
- Fixed missed runs ticket propagation: Added
link_open_internal_tickets_to_runcalls in_ensure_missed_runs_for_job(routes_run_checks.py) after creating both weekly and monthly missed JobRun records. Previously only email-based runs got ticket linking, causing missed runs to not show internal tickets or Autotask tickets. Requireddb.session.flush()before linking to ensure run.id is available. - Fixed checkbox auto-selection: Added
autocomplete="off"to all checkboxes on Inbox and Run Checks pages. Prevents browser from automatically re-selecting checkboxes after page reload following delete actions.
2026-02-12
- Fixed Run Checks modal ticket display: Implemented two-source display logic (ticket_job_runs + ticket_scopes). Previously only showed tickets after they were resolved (when ticket_job_runs entry was created). Now shows tickets immediately upon creation via scope query.
- Fixed copy button in Edge: Moved clipboard functions inside IIFE scope for proper closure access (Edge is stricter than Firefox about scope resolution).
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.