Improve Cove accounts typing and datasource readability

This commit is contained in:
Ivo Oskamp 2026-02-23 11:10:48 +01:00
parent 0c5adf17ab
commit 7803b7647c
2 changed files with 73 additions and 13 deletions

View File

@ -5,12 +5,57 @@ Mirrors the Inbox flow for mail messages:
/cove/accounts/<id>/link link an account to an existing or new job /cove/accounts/<id>/link link an account to an existing or new job
/cove/accounts/<id>/unlink remove the job link /cove/accounts/<id>/unlink remove the job link
""" """
import re
from .routes_shared import * # noqa: F401,F403 from .routes_shared import * # noqa: F401,F403
from .routes_shared import _log_admin_event from .routes_shared import _log_admin_event
from ..models import CoveAccount, Customer, Job, SystemSettings from ..models import CoveAccount, Customer, Job, SystemSettings
_COVE_DATASOURCE_LABELS = {
"D01": "Files & Folders",
"D1": "Files & Folders",
"D02": "System State",
"D2": "System State",
"D10": "VssMsSql",
"D11": "VssSharePoint",
"D19": "M365 Exchange",
"D20": "M365 OneDrive",
"D05": "M365 SharePoint",
"D5": "M365 SharePoint",
"D23": "M365 Teams",
}
_COVE_M365_CODES = {"D19", "D20", "D05", "D5", "D23"}
def _parse_cove_datasource_codes(raw: str | None) -> list[str]:
"""Extract datasource codes from Cove I78 strings like 'D01D02D10'."""
text = (raw or "").strip().upper()
if not text:
return []
return re.findall(r"D\d{1,2}", text)
def _derive_backup_type_for_account(cove_acc: CoveAccount) -> str:
"""Return Backupchecks-style backup type for a Cove account."""
codes = set(_parse_cove_datasource_codes(getattr(cove_acc, "datasource_types", None)))
if codes.intersection(_COVE_M365_CODES):
return "Microsoft 365"
return "Servers/Workstations"
def _humanize_datasources(raw: str | None) -> str:
"""Return readable datasource labels from Cove I78 code string."""
labels: list[str] = []
for code in _parse_cove_datasource_codes(raw):
label = _COVE_DATASOURCE_LABELS.get(code, code)
if label not in labels:
labels.append(label)
return ", ".join(labels)
@main_bp.route("/cove/accounts") @main_bp.route("/cove/accounts")
@login_required @login_required
@roles_required("admin", "operator") @roles_required("admin", "operator")
@ -39,6 +84,12 @@ def cove_accounts():
customers = Customer.query.filter_by(active=True).order_by(Customer.name.asc()).all() customers = Customer.query.filter_by(active=True).order_by(Customer.name.asc()).all()
jobs = Job.query.filter_by(archived=False).order_by(Job.job_name.asc()).all() jobs = Job.query.filter_by(archived=False).order_by(Job.job_name.asc()).all()
for acc in unmatched + matched:
acc.derived_backup_software = "Cove Data Protection"
acc.derived_backup_type = _derive_backup_type_for_account(acc)
acc.derived_job_name = (acc.account_name or acc.computer_name or f"Cove account {acc.account_id}").strip()
acc.datasource_display = _humanize_datasources(acc.datasource_types) or ""
return render_template( return render_template(
"main/cove_accounts.html", "main/cove_accounts.html",
unmatched=unmatched, unmatched=unmatched,
@ -87,8 +138,9 @@ def cove_account_link(cove_account_db_id: int):
flash("Customer not found.", "danger") flash("Customer not found.", "danger")
return redirect(url_for("main.cove_accounts")) return redirect(url_for("main.cove_accounts"))
job_name = (request.form.get("job_name") or cove_acc.account_name or "").strip() default_job_name = (cove_acc.account_name or cove_acc.computer_name or f"Cove account {cove_acc.account_id}").strip()
backup_type = (request.form.get("backup_type") or cove_acc.datasource_types or "Backup").strip() job_name = (request.form.get("job_name") or default_job_name).strip()
backup_type = (request.form.get("backup_type") or _derive_backup_type_for_account(cove_acc)).strip()
job = Job( job = Job(
customer_id=customer.id, customer_id=customer.id,

View File

@ -27,10 +27,12 @@
<table class="table table-sm table-hover align-middle"> <table class="table table-sm table-hover align-middle">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th>Account name</th> <th>Backup software</th>
<th>Type</th>
<th>Job name</th>
<th>Computer</th> <th>Computer</th>
<th>Customer (Cove)</th> <th>Customer (Cove)</th>
<th>Datasource</th> <th>Datasources</th>
<th>Last status</th> <th>Last status</th>
<th>Last run</th> <th>Last run</th>
<th>First seen</th> <th>First seen</th>
@ -40,10 +42,12 @@
<tbody> <tbody>
{% for acc in unmatched %} {% for acc in unmatched %}
<tr> <tr>
<td>{{ acc.account_name or '—' }}</td> <td>{{ acc.derived_backup_software }}</td>
<td>{{ acc.derived_backup_type }}</td>
<td>{{ acc.derived_job_name }}</td>
<td class="text-muted small">{{ acc.computer_name or '—' }}</td> <td class="text-muted small">{{ acc.computer_name or '—' }}</td>
<td>{{ acc.customer_name or '—' }}</td> <td>{{ acc.customer_name or '—' }}</td>
<td class="text-muted small">{{ acc.datasource_types or '—' }}</td> <td class="text-muted small">{{ acc.datasource_display }}</td>
<td> <td>
{% if acc.last_status_code is not none %} {% if acc.last_status_code is not none %}
<span class="badge bg-{{ STATUS_CLASS.get(acc.last_status_code, 'secondary') }}"> <span class="badge bg-{{ STATUS_CLASS.get(acc.last_status_code, 'secondary') }}">
@ -112,14 +116,14 @@
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Job name</label> <label class="form-label">Job name</label>
<input type="text" class="form-control" name="job_name" <input type="text" class="form-control" name="job_name"
value="{{ acc.account_name or '' }}" /> value="{{ acc.derived_job_name }}" />
<div class="form-text">Defaults to the Cove account name.</div> <div class="form-text">Defaults to the Cove account name.</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Backup type</label> <label class="form-label">Backup type</label>
<input type="text" class="form-control" name="backup_type" <input type="text" class="form-control" name="backup_type"
value="{{ acc.datasource_types or 'Backup' }}" /> value="{{ acc.derived_backup_type }}" />
<div class="form-text">From Cove datasource info.</div> <div class="form-text">Derived from Cove datasource profile.</div>
</div> </div>
<div class="d-flex justify-content-end gap-2"> <div class="d-flex justify-content-end gap-2">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
@ -177,9 +181,11 @@
<table class="table table-sm table-hover align-middle"> <table class="table table-sm table-hover align-middle">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th>Account name</th> <th>Backup software</th>
<th>Type</th>
<th>Job name</th>
<th>Customer (Cove)</th> <th>Customer (Cove)</th>
<th>Datasource</th> <th>Datasources</th>
<th>Last status</th> <th>Last status</th>
<th>Last run</th> <th>Last run</th>
<th>Linked job</th> <th>Linked job</th>
@ -189,9 +195,11 @@
<tbody> <tbody>
{% for acc in matched %} {% for acc in matched %}
<tr> <tr>
<td>{{ acc.account_name or '—' }}</td> <td>{{ acc.derived_backup_software }}</td>
<td>{{ acc.derived_backup_type }}</td>
<td>{{ acc.derived_job_name }}</td>
<td>{{ acc.customer_name or '—' }}</td> <td>{{ acc.customer_name or '—' }}</td>
<td class="text-muted small">{{ acc.datasource_types or '—' }}</td> <td class="text-muted small">{{ acc.datasource_display }}</td>
<td> <td>
{% if acc.last_status_code is not none %} {% if acc.last_status_code is not none %}
<span class="badge bg-{{ STATUS_CLASS.get(acc.last_status_code, 'secondary') }}"> <span class="badge bg-{{ STATUS_CLASS.get(acc.last_status_code, 'secondary') }}">