Auto-commit local changes before build (2026-03-20 11:31:49)

This commit is contained in:
Ivo Oskamp 2026-03-20 11:31:49 +01:00
parent 44214cc2c6
commit c7021d393d
6 changed files with 81 additions and 12 deletions

View File

@ -224,20 +224,22 @@ def upsert_cloud_connect_report(mail_message_id: int, html_body: str) -> dict:
counters = {"total": len(rows), "linked": 0, "unlinked": 0, "created": 0, "skipped": 0}
for row in rows:
user = row["user"]
section = row["section"]
user = row["user"]
section = row["section"]
repo_name = row["repo_name"] or "" # never None — part of unique key
# Upsert the staging record — keyed on (user, section).
acc = CloudConnectAccount.query.filter_by(user=user, section=section).first()
# Upsert the staging record — keyed on (user, section, repo_name).
acc = CloudConnectAccount.query.filter_by(user=user, section=section, repo_name=repo_name).first()
if acc is None:
acc = CloudConnectAccount(
user=user,
section=section,
repo_name=repo_name,
first_seen_at=now,
)
db.session.add(acc)
acc.repo_name = row["repo_name"]
acc.repo_name = repo_name
acc.repo_type = row["repo_type"]
acc.num_items = row["num_items"]
acc.total_quota = row["total_quota"]
@ -262,7 +264,8 @@ def upsert_cloud_connect_report(mail_message_id: int, html_body: str) -> dict:
continue
# Deduplicate: one run per job per report date.
external_id = f"vcc-{user}-{section}-{report_date}".lower().replace(" ", "_")
repo_slug = repo_name.lower().replace(" ", "_")
external_id = f"vcc-{user}-{section}-{repo_slug}-{report_date}".lower().replace(" ", "_")
existing = JobRun.query.filter_by(job_id=job.id, external_id=external_id).first()
if existing:

View File

@ -40,7 +40,9 @@ def cloud_connect_accounts():
acc.derived_backup_type = (
"Cloud Connect Agent" if acc.section == "Agent" else "Cloud Connect Backup"
)
acc.derived_job_name = acc.user
# Use repo_name as the suggested job name if set (distinguishes multiple repos
# per user); fall back to user name when repo_name is absent.
acc.derived_job_name = acc.repo_name.strip() if acc.repo_name and acc.repo_name.strip() else acc.user
return render_template(
"main/cloud_connect_accounts.html",
@ -149,7 +151,7 @@ def cloud_connect_account_link(cc_account_db_id: int):
customer = Customer.query.get_or_404(customer_id)
job_name = acc.user.strip()
job_name = (acc.repo_name.strip() if acc.repo_name and acc.repo_name.strip() else acc.user.strip())
backup_type = "Cloud Connect Agent" if acc.section == "Agent" else "Cloud Connect Backup"
job = Job(

View File

@ -1312,6 +1312,60 @@ def migrate_cloud_connect_accounts_table() -> None:
print(f"[migrations] Failed to migrate cloud_connect_accounts table: {exc}")
def migrate_cc_accounts_repo_unique_key() -> None:
"""Extend the cloud_connect_accounts unique key to include repo_name.
Old key: (user, section)
New key: (user, section, repo_name)
This allows a single user to have multiple repository entries in the Cloud Connect
daily report (e.g. both a Veeam Cloud Connect Repository and an Immutable repository),
each linked to a separate Backupchecks job.
"""
try:
engine = db.get_engine()
except Exception as exc:
print(f"[migrations] Could not get engine for cc repo key migration: {exc}")
return
try:
with engine.begin() as conn:
# Make repo_name NOT NULL with default '' (required for unique constraint).
conn.execute(text(
"UPDATE cloud_connect_accounts SET repo_name = '' WHERE repo_name IS NULL"
))
conn.execute(text(
"ALTER TABLE cloud_connect_accounts ALTER COLUMN repo_name SET NOT NULL"
))
conn.execute(text(
"ALTER TABLE cloud_connect_accounts ALTER COLUMN repo_name SET DEFAULT ''"
))
# Drop old (user, section) constraint if it still exists.
conn.execute(text(
"ALTER TABLE cloud_connect_accounts "
"DROP CONSTRAINT IF EXISTS uq_cloud_connect_accounts_user_section"
))
# Add new (user, section, repo_name) constraint if not already present.
conn.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'uq_cloud_connect_accounts_user_section_repo'
) THEN
ALTER TABLE cloud_connect_accounts
ADD CONSTRAINT uq_cloud_connect_accounts_user_section_repo
UNIQUE ("user", section, repo_name);
END IF;
END $$;
"""))
print("[migrations] migrate_cc_accounts_repo_unique_key completed.")
except Exception as exc:
print(f"[migrations] Failed migrate_cc_accounts_repo_unique_key: {exc}")
def run_migrations() -> None:
print("[migrations] Starting migrations...")
migrate_add_username_to_users()
@ -1358,6 +1412,7 @@ def run_migrations() -> None:
migrate_cove_integration()
migrate_cove_accounts_table()
migrate_cloud_connect_accounts_table()
migrate_cc_accounts_repo_unique_key()
migrate_entra_sso_settings()
print("[migrations] All migrations completed.")

View File

@ -399,7 +399,7 @@ class CloudConnectAccount(db.Model):
user = db.Column(db.String(255), nullable=False)
section = db.Column(db.String(32), nullable=False)
repo_name = db.Column(db.String(512), nullable=True)
repo_name = db.Column(db.String(512), nullable=False, default="")
repo_type = db.Column(db.String(255), nullable=True)
num_items = db.Column(db.String(64), nullable=True)
total_quota = db.Column(db.String(32), nullable=True)
@ -418,7 +418,7 @@ class CloudConnectAccount(db.Model):
job = db.relationship("Job", backref=db.backref("cloud_connect_account", uselist=False))
__table_args__ = (
db.UniqueConstraint("user", "section", name="uq_cloud_connect_accounts_user_section"),
db.UniqueConstraint("user", "section", "repo_name", name="uq_cloud_connect_accounts_user_section_repo"),
)

View File

@ -33,7 +33,8 @@
data-id="{{ acc.id }}"
data-user="{{ acc.user | e }}"
data-section="{{ acc.section | e }}"
data-backup-type="{{ acc.derived_backup_type | e }}">
data-backup-type="{{ acc.derived_backup_type | e }}"
data-job-name="{{ acc.derived_job_name | e }}">
<td class="fw-semibold">{{ acc.user }}</td>
<td><span class="badge bg-secondary">{{ acc.section }}</span></td>
<td class="text-muted small">{{ acc.repo_name or '—' }}<br><span class="text-muted" style="font-size:11px;">{{ acc.repo_type or '' }}</span></td>
@ -236,11 +237,12 @@
var user = row.getAttribute('data-user');
var section = row.getAttribute('data-section');
var backupType = row.getAttribute('data-backup-type');
var jobName = row.getAttribute('data-job-name') || user;
var linkUrl = linkUrlTpl.replace('0', id);
document.getElementById('ccLinkModalTitle').textContent = user + ' (' + section + ')';
document.getElementById('ccDisplayBackupType').textContent = backupType;
document.getElementById('ccDisplayJobName').textContent = user;
document.getElementById('ccDisplayJobName').textContent = jobName;
var customerInput = document.getElementById('ccCustomerInput');
var customerIdField = document.getElementById('ccCustomerId');

View File

@ -5,6 +5,13 @@ This file documents all changes made to this project via Claude Code.
## [2026-03-20]
### Fixed
- Cloud Connect accounts: users with multiple repositories (e.g. Veeam Cloud Connect Repository + Cloud Connect Immutable) now get a separate staging account entry per repository instead of overwriting each other:
- `CloudConnectAccount` unique key changed from `(user, section)` to `(user, section, repo_name)`
- Migration `migrate_cc_accounts_repo_unique_key`: drops old constraint, makes `repo_name` NOT NULL (default `''`), adds new constraint
- Importer: upserts on `(user, section, repo_name)`; `external_id` now includes repo slug so each repo gets its own `JobRun`
- Job creation suggestion uses `repo_name` as the default job name (falls back to user when repo_name is empty)
- Cloud Connect accounts page: `data-job-name` attribute on row, modal reads it correctly
- Cloud Connect runs in job detail page popup now show a structured CC summary instead of the raw report email with all tenants:
- `routes_inbox.py` (`inbox_message_detail`): accepts optional `?run_id=` parameter; when the run has `source_type = "cloud_connect"`, returns `cloud_connect_summary` dict and per-run objects from `run_object_links` instead of MailObjects
- `job_detail.html`: passes `run_id` to the detail API; if `cloud_connect_summary` is returned, shows the CC summary panel, collapses the raw email (accessible via "show" toggle), and shows only the single per-run repository object