backupchecks/docs/technical-notes-codex.md

610 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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, starts `gunicorn` with `backend.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 with `backupchecks`, `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()` in `containers/backupchecks/src/backend/app/__init__.py`.
- Blueprints:
- `auth_bp` for authentication.
- `main_bp` for core functionality.
- `doc_bp` for 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 when `cove_import_enabled` is set).
- Health endpoint:
- `GET /health` returns `{ "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 `JobRun` records.
- 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_KEY`
- `APP_ENV`
- `APP_PORT`
- `POSTGRES_DB`
- `POSTGRES_USER`
- `POSTGRES_PASSWORD`
- `DB_HOST`
- `DB_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:
- `User` with role(s), active role in session.
- System settings:
- `SystemSettings` with 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 alias `AdminLog`).
- Domain:
- `Customer`, `Job`, `JobRun`, `Override`
- `MailMessage`, `MailObject`
- `CoveAccount` (Cove staging table — see Cove integration section)
- `Ticket`, `TicketScope`, `TicketJobRun`
- `Remark`, `RemarkScope`, `RemarkJobRun`
- `FeedbackItem`, `FeedbackVote`, `FeedbackReply`, `FeedbackAttachment`
### Foreign Key Relationships & Deletion Order
Critical deletion order to avoid constraint violations:
1. Clean auxiliary tables (ticket_job_runs, remark_job_runs, scopes, overrides)
2. Unlink mails from jobs (UPDATE mail_messages SET job_id = NULL)
3. Delete mail_objects
4. Delete jobs (cascades to job_runs)
5. Delete mails
### Key Model Fields
**MailMessage model:**
- `from_address` (NOT `sender`!) - sender email
- `subject` - email subject
- `text_body` - plain text content
- `html_body` - HTML content
- `received_at` - timestamp
- `location` - inbox/processed/deleted
- `job_id` - link to Job (nullable)
**Job model:**
- `customer_id` - FK to Customer
- `job_name` - parsed from email
- `backup_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 runs
- `external_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).
- 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 creation
- `containers/backupchecks/src/backend/app/cove_importer_service.py` background polling thread
- `containers/backupchecks/src/backend/app/main/routes_cove.py` `/cove/accounts` routes
- `containers/backupchecks/src/templates/main/cove_accounts.html` inbox-style accounts page
### API Details
- Endpoint: `https://api.backup.management/jsonapi` (JSON-RPC 2.0)
- **Login**: `POST` with `{"jsonrpc":"2.0","id":"jsonrpc","method":"Login","params":{"username":"...","password":"..."}}`
- Returns `visa` at top level (`data["visa"]`), **not** inside `result`
- Returns `PartnerId` inside `result`
- **EnumerateAccountStatistics**: `POST` with visa in payload, `query` (lowercase) with `PartnerId`, `StartRecordNumber`, `RecordsCount`, `Columns`
- Settings format per account: `[{"D09F00": "5"}, {"I1": "device name"}, ...]` — list of single-key dicts, flatten with `dict.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)
1. Cove importer fetches all accounts via paginated `EnumerateAccountStatistics` (250/page).
2. Every account is upserted into the `cove_accounts` staging table (always, regardless of job link).
3. Accounts without a `job_id` appear on `/cove/accounts` ("Cove Accounts" page) for admin action.
4. Admin can:
- **Create new job** creates a `Job` with `backup_software="Cove Data Protection"` and links it.
- **Link to existing job** sets `job.cove_account_id` and `cove_acc.job_id`.
5. Linking an account triggers an immediate import attempt; linked accounts then generate `JobRun` records (deduplicated per job via `job_id + external_id`).
6. Per-datasource objects are persisted to `customer_objects`, `job_object_links`, `run_object_links`.
### CoveAccount Model
```python
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.
### Run Enrichment
- Cove-created `JobRun.remark` contains 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/accounts` derives display fields to align with existing job logic:
- `backup_software`: `Cove Data Protection`
- `backup_type`: `Server`, `Workstation`, or `Microsoft 365`
- `job_name`: based on Cove account/computer fallback
- readable datasource labels instead of raw `I78` code stream
- `computer_name` is 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 to `system_settings`, `cove_account_id` to `jobs`, `source_type` + `external_id` to `job_runs`, dedup index on `job_runs.external_id`
- `migrate_cove_accounts_table()` — creates `cove_accounts` table with indexes
---
## Ticketing and Autotask (Critical Rules)
### Two Ticket Types
1. **Internal Tickets** (tickets table)
- Created manually or via Autotask integration
- Stored in `tickets` table with `ticket_code` (e.g., "T20250123.0001")
- Linked to runs via `ticket_job_runs` many-to-many table
- Scoped to jobs via `ticket_scopes` table
- Have `resolved_at` field for resolution tracking
- **Auto-propagation**: Automatically linked to new runs via `link_open_internal_tickets_to_run`
2. **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 `Admin` dropdown
- secondary non-admin links under `More` dropdown
- Primary operational links remain visible (notably `Run Checks`).
- Viewer role now exposes `Customers` and `Jobs` directly in navbar.
- Have `autotask_ticket_deleted_at` field for deletion tracking
- Resolution tracked via matching internal ticket's `resolved_at` field
- **Auto-propagation**: Linked to new runs via two-strategy approach
### 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_runs` links automatically
- Tickets remain visible until explicitly resolved
- **NO date-based logic** - resolved = immediately hidden from new runs
**Strategy 2: Autotask ticket propagation (independent)**
1. Check if internal ticket code exists → find matching Autotask run → copy ticket info
2. 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)
3. Copy `autotask_ticket_id`, `autotask_ticket_number`, `created_at`, `created_by_user_id` to new run
### Where Ticket Linking is Called
`link_open_internal_tickets_to_run` is invoked in three locations:
1. **Email-based runs**: `routes_inbox.py` and `mail_importer.py` - after creating JobRun from parsed email
2. **Missed runs**: `routes_run_checks.py` in `_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:
1. Direct links (`ticket_job_runs WHERE job_run_id = X`) → always show (audit trail)
2. Active window (`ticket_scopes WHERE job_id = Y AND resolved_at IS NULL`) → only unresolved
- 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**:
1. Direct links: `tickets JOIN ticket_job_runs WHERE job_run_id = X`
2. Job-level scope: `tickets JOIN ticket_scopes WHERE job_id = Y AND resolved_at IS NULL AND active_from_date <= run_date`
- Prevents duplicates by tracking seen ticket IDs
- Shows newly created tickets immediately (via scope) without waiting for resolve action
- **Two-source remark display**:
1. Direct links: `remarks JOIN remark_job_runs WHERE job_run_id = X`
2. 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 from `start_date`)
- 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:
```python
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_date` OR `active_from_date <= run_date`
- ✅ Only show tickets that are ACTUALLY LINKED via `ticket_job_runs` table
- ✅ 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:
1. **Modern Clipboard API**: `navigator.clipboard.writeText()` - works in modern browsers with HTTPS
2. **Legacy execCommand**: `document.execCommand('copy')` - fallback for older browsers and Edge
3. **Prompt fallback**: `window.prompt()` - last resort if clipboard access fails
- 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_id` query parameter in `routes_jobs.py`.
- If set: returns jobs for that customer only.
- If not set: keeps default filter that hides jobs linked to inactive customers.
- Jobs UI behavior:
- Shows active filter banner with selected customer name.
- Provides "Clear filter" action back to unfiltered `/jobs`.
- Templates touched:
- `templates/main/customers.html`
- `templates/main/jobs.html`
### Global Grouped Search (2026-02-16)
- New route:
- `GET /search` in `main/routes_search.py`
- New UI:
- Navbar search form in `templates/layout/base.html`
- Grouped result page in `templates/main/search.html`
- 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 <section>" links pass `q` to destination overview pages so page-level filtering matches the search term.
- 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_id` query 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_checks` results are restricted to `admin`/`operator`.
- `reports` supports `admin`/`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.
## 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`
- PostgreSQL persistent volume:
- `/docker/appdata/backupchecks/backupchecks-postgres:/var/lib/postgresql/data`
- `deploy/backupchecks-stack.yml` also contains example `.env` variables at the bottom.
## Build/Release Flow
File: `build-and-push.sh`
- Bump options:
- `1` patch, `2` minor, `3` major, `t` test.
- Release build:
- update `version.txt`
- commit + tag + push
- docker push of `:<version>`, `:dev`, `:latest`
- Test build:
- only `:dev`
- no commit/tag.
- Services are discovered under `containers/*` with Dockerfile-per-service.
## Technical Observations / Attention Points
- `README.md` is currently empty; quick-start entry context is missing.
- `LICENSE` is currently empty.
- `docs/architecture.md` is currently empty.
- `deploy/backupchecks-stack.yml` contains 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 `description` field 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 as `auto_importer_service.py`)
- `CoveAccount` staging model + `migrate_cove_accounts_table()` migration
- `SystemSettings` 8 new Cove fields, `Job` `cove_account_id`, `JobRun` `source_type` + `external_id`
- `routes_cove.py` inbox-style `/cove/accounts` with link/unlink routes
- `cove_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 with `flat.update(item)`
- EnumerateAccountStatistics params must use lowercase `query` key and `RecordsCount` (not `RecordCount`)
- Login params must use lowercase `username`/`password`
- D02/D03 are legacy; use D10/D11 or D09 (Total) instead
### 2026-02-19
- **Added 3CX Update parser support**: `threecx.py` now recognizes subject `3CX Notification: Update Successful - <host>` and stores it as informational with:
- `backup_software = 3CX`
- `backup_type = Update`
- `overall_status = Success`
- **3CX informational schedule behavior**:
- `3CX / Update` and `3CX / SSL Certificate` are excluded from schedule inference in `routes_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.
- **Fixed remark visibility mismatch**:
- `/api/job-runs/<run_id>/alerts` now loads remarks from both:
1. `remark_job_runs` (explicit run links),
2. `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_run` calls 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. Required `db.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.