diff --git a/containers/backupchecks/src/backend/app/cove_importer.py b/containers/backupchecks/src/backend/app/cove_importer.py index a8168a4..27371d8 100644 --- a/containers/backupchecks/src/backend/app/cove_importer.py +++ b/containers/backupchecks/src/backend/app/cove_importer.py @@ -59,6 +59,21 @@ STATUS_MAP: dict[int, str] = { 12: "Warning", # Restarted } +# Mapping from Cove status code to readable label +STATUS_LABELS: dict[int, str] = { + 1: "In process", + 2: "Failed", + 3: "Aborted", + 5: "Completed", + 6: "Interrupted", + 7: "Not started", + 8: "Completed with errors", + 9: "In progress with faults", + 10: "Over quota", + 11: "No selection", + 12: "Restarted", +} + # Datasource label mapping (column prefix → human-readable label) DATASOURCE_LABELS: dict[str, str] = { "D1": "Files & Folders", @@ -210,6 +225,16 @@ def _map_status(code: Any) -> str: return "Warning" +def _status_label(code: Any) -> str: + """Map a Cove status code (int) to a human-readable label.""" + if code is None: + return "Unknown" + try: + return STATUS_LABELS.get(int(code), f"Code {int(code)}") + except (ValueError, TypeError): + return "Unknown" + + def _ts_to_dt(value: Any) -> datetime | None: """Convert a Unix timestamp (int or str) to a naive UTC datetime.""" if value is None: @@ -223,6 +248,13 @@ def _ts_to_dt(value: Any) -> datetime | None: return None +def _fmt_utc(dt: datetime | None) -> str: + """Format a naive UTC datetime to readable text for run object messages.""" + if not dt: + return "unknown" + return dt.strftime("%Y-%m-%d %H:%M UTC") + + def run_cove_import(settings) -> tuple[int, int, int, int]: """Fetch Cove account statistics and update the staging table + JobRuns. @@ -381,13 +413,20 @@ def _process_account(account: dict) -> bool: return False status = _map_status(last_status_code) + run_remark = ( + f"Cove account: {account_name or account_id} | " + f"Computer: {computer_name or '-'} | " + f"Customer: {customer_name or '-'} | " + f"Last status: {_status_label(last_status_code)} ({last_status_code if last_status_code is not None else '-'}) | " + f"Last run: {_fmt_utc(last_run_at)}" + ) run = JobRun( job_id=job.id, mail_message_id=None, run_at=last_run_at, status=status, - remark=None, + remark=run_remark, missed=False, override_applied=False, source_type="cove_api", @@ -422,6 +461,11 @@ def _persist_datasource_objects( continue status = _map_status(status_code) + ds_last_ts = _ts_to_dt(flat.get(f"{ds_prefix}F15")) + status_msg = ( + f"Cove datasource status: {_status_label(status_code)} " + f"({status_code}); last session: {_fmt_utc(ds_last_ts)}" + ) # Upsert customer_objects customer_object_id = conn.execute( @@ -461,10 +505,11 @@ def _persist_datasource_objects( text( """ INSERT INTO run_object_links (run_id, customer_object_id, status, error_message, observed_at) - VALUES (:run_id, :customer_object_id, :status, NULL, :observed_at) + VALUES (:run_id, :customer_object_id, :status, :error_message, :observed_at) ON CONFLICT (run_id, customer_object_id) DO UPDATE SET status = EXCLUDED.status, + error_message = EXCLUDED.error_message, observed_at = EXCLUDED.observed_at """ ), @@ -472,6 +517,7 @@ def _persist_datasource_objects( "run_id": run_id, "customer_object_id": customer_object_id, "status": status, - "observed_at": observed_at, + "error_message": status_msg, + "observed_at": ds_last_ts or observed_at, }, ) diff --git a/containers/backupchecks/src/backend/app/main/routes_cove.py b/containers/backupchecks/src/backend/app/main/routes_cove.py index 552a636..9dfa459 100644 --- a/containers/backupchecks/src/backend/app/main/routes_cove.py +++ b/containers/backupchecks/src/backend/app/main/routes_cove.py @@ -9,6 +9,7 @@ import re from .routes_shared import * # noqa: F401,F403 from .routes_shared import _log_admin_event +from ..cove_importer import CoveImportError, run_cove_import from ..models import CoveAccount, Customer, Job, SystemSettings @@ -129,6 +130,8 @@ def cove_account_link(cove_account_db_id: int): action = (request.form.get("action") or "").strip() # "create" or "link" + linked_job_name = "" + if action == "create": # Create a new job from the Cove account data customer_id_raw = (request.form.get("customer_id") or "").strip() @@ -171,11 +174,8 @@ def cove_account_link(cove_account_db_id: int): f"Created job {job.id} and linked Cove account {cove_acc.account_id} ({cove_acc.account_name})", details=f"customer={customer.name}, job_name={job_name}", ) - flash( - f"Job '{job_name}' created for customer '{customer.name}'. " - "Runs will appear after the next Cove import.", - "success", - ) + linked_job_name = job_name + flash(f"Job '{job_name}' created for customer '{customer.name}'.", "success") elif action == "link": # Link to an existing job @@ -204,14 +204,62 @@ def cove_account_link(cove_account_db_id: int): f"Linked Cove account {cove_acc.account_id} ({cove_acc.account_name}) to existing job {job.id}", details=f"job_name={job.job_name}", ) - flash( - f"Cove account linked to job '{job.job_name}'. " - "Runs will appear after the next Cove import.", - "success", - ) + linked_job_name = job.job_name or "" + flash(f"Cove account linked to job '{job.job_name}'.", "success") else: flash("Unknown action.", "warning") + return redirect(url_for("main.cove_accounts")) + + # Trigger an immediate import so the latest Cove run appears right away + # after linking (instead of waiting for the next scheduled/manual import). + settings = SystemSettings.query.first() + if settings and getattr(settings, "cove_enabled", False): + try: + total, created, skipped, errors = run_cove_import(settings) + _log_admin_event( + "cove_import_after_link", + ( + "Triggered immediate Cove import after account link. " + f"accounts={total}, created={created}, skipped={skipped}, errors={errors}" + ), + ) + if created > 0: + flash( + ( + f"Immediate import complete for '{linked_job_name}'. " + f"New runs: {created} (accounts: {total}, skipped: {skipped}, errors: {errors})." + ), + "success" if errors == 0 else "warning", + ) + else: + flash( + ( + f"Immediate import complete for '{linked_job_name}', but no new run was found yet. " + f"(accounts: {total}, skipped: {skipped}, errors: {errors})" + ), + "info" if errors == 0 else "warning", + ) + except CoveImportError as exc: + _log_admin_event( + "cove_import_after_link_error", + f"Immediate Cove import after account link failed: {exc}", + ) + flash( + "Account linked, but immediate import failed. " + "You can run import again from Cove settings.", + "warning", + ) + except Exception as exc: + _log_admin_event( + "cove_import_after_link_error", + f"Unexpected immediate Cove import error after account link: {exc}", + ) + flash( + "Account linked, but immediate import encountered an unexpected error. " + "You can run import again from Cove settings.", + "warning", + ) return redirect(url_for("main.cove_accounts"))