Auto-commit local changes before build (2026-03-20 11:31:49)
This commit is contained in:
parent
44214cc2c6
commit
c7021d393d
@ -226,18 +226,20 @@ def upsert_cloud_connect_report(mail_message_id: int, html_body: str) -> dict:
|
|||||||
for row in rows:
|
for row in rows:
|
||||||
user = row["user"]
|
user = row["user"]
|
||||||
section = row["section"]
|
section = row["section"]
|
||||||
|
repo_name = row["repo_name"] or "" # never None — part of unique key
|
||||||
|
|
||||||
# Upsert the staging record — keyed on (user, section).
|
# Upsert the staging record — keyed on (user, section, repo_name).
|
||||||
acc = CloudConnectAccount.query.filter_by(user=user, section=section).first()
|
acc = CloudConnectAccount.query.filter_by(user=user, section=section, repo_name=repo_name).first()
|
||||||
if acc is None:
|
if acc is None:
|
||||||
acc = CloudConnectAccount(
|
acc = CloudConnectAccount(
|
||||||
user=user,
|
user=user,
|
||||||
section=section,
|
section=section,
|
||||||
|
repo_name=repo_name,
|
||||||
first_seen_at=now,
|
first_seen_at=now,
|
||||||
)
|
)
|
||||||
db.session.add(acc)
|
db.session.add(acc)
|
||||||
|
|
||||||
acc.repo_name = row["repo_name"]
|
acc.repo_name = repo_name
|
||||||
acc.repo_type = row["repo_type"]
|
acc.repo_type = row["repo_type"]
|
||||||
acc.num_items = row["num_items"]
|
acc.num_items = row["num_items"]
|
||||||
acc.total_quota = row["total_quota"]
|
acc.total_quota = row["total_quota"]
|
||||||
@ -262,7 +264,8 @@ def upsert_cloud_connect_report(mail_message_id: int, html_body: str) -> dict:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Deduplicate: one run per job per report date.
|
# 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()
|
existing = JobRun.query.filter_by(job_id=job.id, external_id=external_id).first()
|
||||||
if existing:
|
if existing:
|
||||||
|
|||||||
@ -40,7 +40,9 @@ def cloud_connect_accounts():
|
|||||||
acc.derived_backup_type = (
|
acc.derived_backup_type = (
|
||||||
"Cloud Connect Agent" if acc.section == "Agent" else "Cloud Connect Backup"
|
"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(
|
return render_template(
|
||||||
"main/cloud_connect_accounts.html",
|
"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)
|
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"
|
backup_type = "Cloud Connect Agent" if acc.section == "Agent" else "Cloud Connect Backup"
|
||||||
|
|
||||||
job = Job(
|
job = Job(
|
||||||
|
|||||||
@ -1312,6 +1312,60 @@ def migrate_cloud_connect_accounts_table() -> None:
|
|||||||
print(f"[migrations] Failed to migrate cloud_connect_accounts table: {exc}")
|
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:
|
def run_migrations() -> None:
|
||||||
print("[migrations] Starting migrations...")
|
print("[migrations] Starting migrations...")
|
||||||
migrate_add_username_to_users()
|
migrate_add_username_to_users()
|
||||||
@ -1358,6 +1412,7 @@ def run_migrations() -> None:
|
|||||||
migrate_cove_integration()
|
migrate_cove_integration()
|
||||||
migrate_cove_accounts_table()
|
migrate_cove_accounts_table()
|
||||||
migrate_cloud_connect_accounts_table()
|
migrate_cloud_connect_accounts_table()
|
||||||
|
migrate_cc_accounts_repo_unique_key()
|
||||||
migrate_entra_sso_settings()
|
migrate_entra_sso_settings()
|
||||||
print("[migrations] All migrations completed.")
|
print("[migrations] All migrations completed.")
|
||||||
|
|
||||||
|
|||||||
@ -399,7 +399,7 @@ class CloudConnectAccount(db.Model):
|
|||||||
user = db.Column(db.String(255), nullable=False)
|
user = db.Column(db.String(255), nullable=False)
|
||||||
section = db.Column(db.String(32), 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)
|
repo_type = db.Column(db.String(255), nullable=True)
|
||||||
num_items = db.Column(db.String(64), nullable=True)
|
num_items = db.Column(db.String(64), nullable=True)
|
||||||
total_quota = db.Column(db.String(32), 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))
|
job = db.relationship("Job", backref=db.backref("cloud_connect_account", uselist=False))
|
||||||
|
|
||||||
__table_args__ = (
|
__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"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,8 @@
|
|||||||
data-id="{{ acc.id }}"
|
data-id="{{ acc.id }}"
|
||||||
data-user="{{ acc.user | e }}"
|
data-user="{{ acc.user | e }}"
|
||||||
data-section="{{ acc.section | 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 class="fw-semibold">{{ acc.user }}</td>
|
||||||
<td><span class="badge bg-secondary">{{ acc.section }}</span></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>
|
<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 user = row.getAttribute('data-user');
|
||||||
var section = row.getAttribute('data-section');
|
var section = row.getAttribute('data-section');
|
||||||
var backupType = row.getAttribute('data-backup-type');
|
var backupType = row.getAttribute('data-backup-type');
|
||||||
|
var jobName = row.getAttribute('data-job-name') || user;
|
||||||
var linkUrl = linkUrlTpl.replace('0', id);
|
var linkUrl = linkUrlTpl.replace('0', id);
|
||||||
|
|
||||||
document.getElementById('ccLinkModalTitle').textContent = user + ' (' + section + ')';
|
document.getElementById('ccLinkModalTitle').textContent = user + ' (' + section + ')';
|
||||||
document.getElementById('ccDisplayBackupType').textContent = backupType;
|
document.getElementById('ccDisplayBackupType').textContent = backupType;
|
||||||
document.getElementById('ccDisplayJobName').textContent = user;
|
document.getElementById('ccDisplayJobName').textContent = jobName;
|
||||||
|
|
||||||
var customerInput = document.getElementById('ccCustomerInput');
|
var customerInput = document.getElementById('ccCustomerInput');
|
||||||
var customerIdField = document.getElementById('ccCustomerId');
|
var customerIdField = document.getElementById('ccCustomerId');
|
||||||
|
|||||||
@ -5,6 +5,13 @@ This file documents all changes made to this project via Claude Code.
|
|||||||
## [2026-03-20]
|
## [2026-03-20]
|
||||||
|
|
||||||
### Fixed
|
### 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:
|
- 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
|
- `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
|
- `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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user