Update technical-notes-codex.md with Cove integration and recent changes
- Add complete Cove Data Protection integration section: API details, column codes, status mapping, inbox flow, CoveAccount model, routes, migrations, background thread, settings UI - Update Data Model section: CoveAccount model, Job.cove_account_id, JobRun.source_type + external_id, SystemSettings Cove fields - Update Application Architecture: cove_importer_service background thread - Add debug logging snippet for ticket linking issues - Add Recent Changes entry for 2026-02-23 - Add Cove files to Quick References Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9b19283c97
commit
8deecd4c11
@ -1,6 +1,6 @@
|
|||||||
# Technical Notes (Internal)
|
# Technical Notes (Internal)
|
||||||
|
|
||||||
Last updated: 2026-02-19
|
Last updated: 2026-02-23
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
Internal technical snapshot of the `backupchecks` repository for faster onboarding, troubleshooting, and change impact analysis.
|
Internal technical snapshot of the `backupchecks` repository for faster onboarding, troubleshooting, and change impact analysis.
|
||||||
@ -30,8 +30,9 @@ Internal technical snapshot of the `backupchecks` repository for faster onboardi
|
|||||||
- Database initialization at startup:
|
- Database initialization at startup:
|
||||||
- `db.create_all()`
|
- `db.create_all()`
|
||||||
- `run_migrations()`
|
- `run_migrations()`
|
||||||
- Background task:
|
- Background tasks:
|
||||||
- `start_auto_importer(app)` starts the automatic mail importer thread.
|
- `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:
|
- Health endpoint:
|
||||||
- `GET /health` returns `{ "status": "ok" }`.
|
- `GET /health` returns `{ "status": "ok" }`.
|
||||||
|
|
||||||
@ -71,11 +72,13 @@ File: `containers/backupchecks/src/backend/app/models.py`
|
|||||||
- System settings:
|
- System settings:
|
||||||
- `SystemSettings` with Graph/mail settings, import settings, UI timezone, dashboard policy, sandbox flag.
|
- `SystemSettings` with Graph/mail settings, import settings, UI timezone, dashboard policy, sandbox flag.
|
||||||
- Autotask configuration and cache fields are present.
|
- 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`.
|
||||||
- Logging:
|
- Logging:
|
||||||
- `AuditLog` (legacy alias `AdminLog`).
|
- `AuditLog` (legacy alias `AdminLog`).
|
||||||
- Domain:
|
- Domain:
|
||||||
- `Customer`, `Job`, `JobRun`, `Override`
|
- `Customer`, `Job`, `JobRun`, `Override`
|
||||||
- `MailMessage`, `MailObject`
|
- `MailMessage`, `MailObject`
|
||||||
|
- `CoveAccount` (Cove staging table — see Cove integration section)
|
||||||
- `Ticket`, `TicketScope`, `TicketJobRun`
|
- `Ticket`, `TicketScope`, `TicketJobRun`
|
||||||
- `Remark`, `RemarkScope`, `RemarkJobRun`
|
- `Remark`, `RemarkScope`, `RemarkJobRun`
|
||||||
- `FeedbackItem`, `FeedbackVote`, `FeedbackReply`, `FeedbackAttachment`
|
- `FeedbackItem`, `FeedbackVote`, `FeedbackReply`, `FeedbackAttachment`
|
||||||
@ -101,8 +104,13 @@ Critical deletion order to avoid constraint violations:
|
|||||||
**Job model:**
|
**Job model:**
|
||||||
- `customer_id` - FK to Customer
|
- `customer_id` - FK to Customer
|
||||||
- `job_name` - parsed from email
|
- `job_name` - parsed from email
|
||||||
- `backup_software` - e.g., "Veeam", "Synology"
|
- `backup_software` - e.g., "Veeam", "Synology", "Cove Data Protection"
|
||||||
- `backup_type` - e.g., "Backup Job", "Active Backup"
|
- `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
|
## Parser Architecture
|
||||||
- Folder: `containers/backupchecks/src/backend/app/parsers/`
|
- Folder: `containers/backupchecks/src/backend/app/parsers/`
|
||||||
@ -140,6 +148,121 @@ Critical deletion order to avoid constraint violations:
|
|||||||
- Hostname extraction from multiple patterns
|
- Hostname extraction from multiple patterns
|
||||||
- Returns: backup_type "Updates", job_name "Synology Automatic Update"
|
- 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. On the next import, linked accounts generate `JobRun` records (deduplicated via `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 the Unix timestamp from `D09F15`.
|
||||||
|
Before creating a `JobRun`, check `JobRun.query.filter_by(external_id=external_id).first()`.
|
||||||
|
|
||||||
|
### 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)
|
## Ticketing and Autotask (Critical Rules)
|
||||||
|
|
||||||
### Two Ticket Types
|
### Two Ticket Types
|
||||||
@ -208,6 +331,30 @@ All pages use **explicit link-based queries** (no date-based logic):
|
|||||||
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`)
|
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
|
- 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 vs Deleted
|
||||||
- **Resolved**: Ticket completed in Autotask (tracked in internal `tickets.resolved_at`)
|
- **Resolved**: Ticket completed in Autotask (tracked in internal `tickets.resolved_at`)
|
||||||
- Stops propagating to new runs
|
- Stops propagating to new runs
|
||||||
@ -358,11 +505,30 @@ File: `build-and-push.sh`
|
|||||||
- Parsers: `containers/backupchecks/src/backend/app/parsers/registry.py`
|
- Parsers: `containers/backupchecks/src/backend/app/parsers/registry.py`
|
||||||
- Ticketing utilities: `containers/backupchecks/src/backend/app/ticketing_utils.py`
|
- Ticketing utilities: `containers/backupchecks/src/backend/app/ticketing_utils.py`
|
||||||
- Run Checks routes: `containers/backupchecks/src/backend/app/main/routes_run_checks.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`
|
- Compose stack: `deploy/backupchecks-stack.yml`
|
||||||
- Build script: `build-and-push.sh`
|
- Build script: `build-and-push.sh`
|
||||||
|
|
||||||
## Recent Changes
|
## 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
|
### 2026-02-19
|
||||||
- **Added 3CX Update parser support**: `threecx.py` now recognizes subject `3CX Notification: Update Successful - <host>` and stores it as informational with:
|
- **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_software = 3CX`
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user