Repair files broken by the web-UI merge

The merge of add-backend-api into main committed unresolved conflicts
and corrupted uploads; the API could not import on main.

- backend/app/settings.py, deploy/backup-dashboard-api.service,
  collectors/veeam/README.md: remove conflict markers, restoring the
  add-backend-api versions
- backend/app/__init__.py, backend/tests/__init__.py: were 1024 null
  bytes ("source code string cannot contain null bytes"); now empty
- Remove the accidental Backup-Dashboard gitlink at the repo root and a
  duplicate collectors/veeam/MockVeeam.ps1 (identical to tests/)
- Restore the 60-minute collector interval from 17ee303, which the
  merge reverted to 15, and update the docs to match

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-07 15:13:49 -06:00
co-authored by Claude Opus 5.5
parent 10bfc7a3aa
commit b4b703ceee
9 changed files with 6 additions and 111 deletions
Submodule Backup-Dashboard deleted from ba798284df
Binary file not shown.
-11
View File
@@ -5,12 +5,9 @@ from pathlib import Path
# The modal shows this many days of run history; retention can never be shorter. # The modal shows this many days of run history; retention can never be shorter.
HISTORY_DAYS = 30 HISTORY_DAYS = 30
<<<<<<< HEAD
=======
# Shared by the systemd service (EnvironmentFile=) and manage.py, so both always use the same DB. # Shared by the systemd service (EnvironmentFile=) and manage.py, so both always use the same DB.
DEFAULT_ENV_FILE = "/etc/backup-dashboard/backup-dashboard.env" DEFAULT_ENV_FILE = "/etc/backup-dashboard/backup-dashboard.env"
>>>>>>> add-backend-api
@dataclass(frozen=True) @dataclass(frozen=True)
class Settings: class Settings:
@@ -20,13 +17,6 @@ class Settings:
serve_static: bool = False serve_static: bool = False
<<<<<<< HEAD
def load_settings() -> Settings:
return Settings(
db_path=Path(os.environ.get("BACKUP_DASH_DB", "backup-dashboard.db")),
retention_days=max(HISTORY_DAYS, int(os.environ.get("BACKUP_DASH_RETENTION_DAYS", "90"))),
serve_static=os.environ.get("BACKUP_DASH_SERVE_STATIC") == "1",
=======
def read_env_file(path: Path) -> dict[str, str]: def read_env_file(path: Path) -> dict[str, str]:
"""Parse simple KEY=VALUE lines (the subset systemd's EnvironmentFile accepts).""" """Parse simple KEY=VALUE lines (the subset systemd's EnvironmentFile accepts)."""
values = {} values = {}
@@ -50,5 +40,4 @@ def load_settings() -> Settings:
db_path=Path(env.get("BACKUP_DASH_DB", "backup-dashboard.db")), db_path=Path(env.get("BACKUP_DASH_DB", "backup-dashboard.db")),
retention_days=max(HISTORY_DAYS, int(env.get("BACKUP_DASH_RETENTION_DAYS", "90"))), retention_days=max(HISTORY_DAYS, int(env.get("BACKUP_DASH_RETENTION_DAYS", "90"))),
serve_static=env.get("BACKUP_DASH_SERVE_STATIC") == "1", serve_static=env.get("BACKUP_DASH_SERVE_STATIC") == "1",
>>>>>>> add-backend-api
) )
Binary file not shown.
+1 -1
View File
@@ -22,7 +22,7 @@ param(
[Parameter(Mandatory)][string]$ApiUrl, [Parameter(Mandatory)][string]$ApiUrl,
[Parameter(Mandatory)][string]$Tenant, [Parameter(Mandatory)][string]$Tenant,
[string]$SourceSystem = 'Veeam B&R', [string]$SourceSystem = 'Veeam B&R',
[ValidateRange(5, 1440)][int]$IntervalMinutes = 15, [ValidateRange(5, 1440)][int]$IntervalMinutes = 60,
[ValidateRange(1, 720)][int]$LookbackHours = 48, [ValidateRange(1, 720)][int]$LookbackHours = 48,
[string]$InstallDir = (Join-Path $env:ProgramFiles 'BackupDashboard'), [string]$InstallDir = (Join-Path $env:ProgramFiles 'BackupDashboard'),
# Defaults to the machine-wide PowerShell 7 install # Defaults to the machine-wide PowerShell 7 install
-83
View File
@@ -1,83 +0,0 @@
# Stand-ins for the Veeam cmdlets, shaped like the real objects' properties the collector reads.
# Dot-source this before running the collector to try it without a VBR server:
#
# . ./tests/MockVeeam.ps1
# ./Send-VeeamBackupStatus.ps1 -ConfigPath ./tests/config.json -DryRun
# Fixed to the top of the hour, and IDs derived from names/times, so repeated runs see the same
# jobs and sessions, like a real server
$now = Get-Date -Minute 0 -Second 0 -Millisecond 0
function New-StableGuid([string]$Seed) {
[guid]::new([Security.Cryptography.MD5]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes($Seed)))
}
$notSet = Get-Date '1900-01-01'
function New-MockJob($Name, $Type, $NextRun, [bool]$Enabled = $true) {
[pscustomobject]@{
Id = New-StableGuid $Name; Name = $Name; JobType = $Type
IsScheduleEnabled = $Enabled
ScheduleOptions = [pscustomobject]@{ NextRun = $NextRun }
}
}
function New-MockSession($Job, $Start, $End, $State, $Result, $Reason = '', $Tasks = @(), $LogRecords = @(), $Size = 0) {
$s = [pscustomobject]@{
Id = New-StableGuid "$($Job.Name)|$($Start.Ticks)"; JobId = $Job.Id; JobName = $Job.Name
CreationTime = $Start; EndTime = $End; State = $State; Result = $Result
IsRetryMode = $false
Info = [pscustomobject]@{ Reason = $Reason }
BackupStats = [pscustomobject]@{ BackupSize = $Size; DataSize = $Size * 3 }
Logger = [pscustomobject]@{ Records = $LogRecords }
Tasks = $Tasks
}
$s.Logger | Add-Member ScriptMethod GetLog { [pscustomobject]@{ UpdatedRecords = $this.Records } }
$s | Add-Member ScriptMethod GetTaskSessions { $this.Tasks }
$s
}
function New-MockTask($Name, $Status, $Reason = '') {
[pscustomobject]@{ Name = $Name; Status = $Status; Info = [pscustomobject]@{ Reason = $Reason } }
}
$global:MockJobs = @(
(New-MockJob 'File Server – Nightly' 'Backup' $now.Date.AddDays(1).AddHours(22)),
# Older VBR versions return NextRun as a culture-formatted string
(New-MockJob 'SQL01 – Logs' 'Backup' $now.AddMinutes(40).ToString()),
(New-MockJob 'Offsite Copy' 'BackupSync' $null),
(New-MockJob 'Old Exchange' 'Backup' $now.AddDays(1) $false),
# VBR 12+ backup copy job (immediate mode, no schedule): the "policy" job Get-VBRJob returns
(New-MockJob 'Cloud Copy' 'SimpleBackupCopyPolicy' $null)
)
$fs, $sql, $copy, $old, $cloud = $global:MockJobs
# Its hidden worker job, one per source job. Get-VBRJob doesn't return it, and Get-VBRBackupSession
# returns only the worker's sessions. Shape taken from a real VBR 13 server.
$cloudWorker = [pscustomobject]@{
Id = New-StableGuid 'Cloud Copy\File Server – Nightly'; Name = 'Cloud Copy\File Server – Nightly'
JobType = 'SimpleBackupCopyWorker'; PolicyJobId = $cloud.Id
}
function New-MockWorkerSession($Start, $End, $Result) {
$s = New-MockSession $cloudWorker $Start $End 'Stopped' $Result -Size 40GB
$s | Add-Member ScriptMethod GetJob { $cloudWorker }
$s
}
$global:MockSessions = @(
(New-MockSession $fs $now.AddDays(-1).AddHours(-2) $now.AddDays(-1).AddHours(-1) 'Stopped' 'Success' -Size 128GB -Tasks @(
(New-MockTask 'FS01' 'Success'), (New-MockTask 'FS02' 'Success'))),
(New-MockSession $fs $now.AddMinutes(-20) $notSet 'Working' 'None' -Tasks @((New-MockTask 'FS01' 'InProgress'))),
(New-MockSession $sql $now.AddHours(-1) $now.AddMinutes(-55) 'Stopped' 'Warning' -Size 2GB `
-Tasks @((New-MockTask 'SQL01' 'Warning' 'Failed to truncate transaction logs for database "ERP_PROD"'))),
(New-MockSession $copy $now.AddHours(-5) $now.AddHours(-4) 'Stopped' 'Failed' 'Error: Unable to connect to repository NAS02' `
-LogRecords @(
[pscustomobject]@{ Title = 'Preparing backup chain'; Status = 'ESucceeded' },
[pscustomobject]@{ Title = 'Network path not found (0x80070035)'; Status = 'EFailed' })),
(New-MockSession $old $now.AddDays(-10) $now.AddDays(-10).AddHours(1) 'Stopped' 'Success'), # outside 48h lookback
(New-MockWorkerSession $now.AddDays(-1).AddMinutes(-55) $now.AddDays(-1).AddMinutes(-30) 'Success'),
(New-MockWorkerSession $now.AddMinutes(-55) $now.AddMinutes(-35) 'Warning'),
# A session from a job the collector doesn't know (e.g. deleted): must be ignored, not crash
(New-MockSession ([pscustomobject]@{ Id = New-StableGuid 'gone'; Name = 'Deleted job' }) $now.AddHours(-2) $now.AddHours(-1) 'Stopped' 'Success')
)
function Get-VBRJob { [CmdletBinding()] param() $global:MockJobs }
function Get-VBRBackupSession { [CmdletBinding()] param() $global:MockSessions }
+4 -9
View File
@@ -13,19 +13,16 @@ account can't run it. The installer checks for this before changing anything.
```bash ```bash
python manage.py create-key veeam-acme python manage.py create-key veeam-acme
``` ```
2. Copy this folder to the VBR server and run: 2. Copy this folder to the VBR server and run, from PowerShell 7 (`pwsh`):
3. Copy this folder to the VBR server and run, from PowerShell 7 (`pwsh`):
```powershell ```powershell
.\Install-VeeamCollector.ps1 -ApiUrl https://backups.example.com -Tenant "Acme Corp" .\Install-VeeamCollector.ps1 -ApiUrl https://backups.example.com -Tenant "Acme Corp"
``` ```
It prompts for the key, then: It prompts for the key, then:
- writes `C:\ProgramData\BackupDashboard\veeam-collector.json` (readable only by SYSTEM and Administrators) - writes `C:\ProgramData\BackupDashboard\veeam-collector.json` (readable only by SYSTEM and Administrators)
- copies the script to `C:\Program Files\BackupDashboard\` - copies the script to `C:\Program Files\BackupDashboard\`
- registers the scheduled task **Backup Dashboard - Veeam collector** (SYSTEM, every 15 minutes) - registers the scheduled task **Backup Dashboard - Veeam collector** (SYSTEM, every 60 minutes,
4. Preview the payloads, then backfill 30 days so the history view is populated straight away:
- registers the scheduled task **Backup Dashboard - Veeam collector** (SYSTEM, every 15 minutes,
run by `C:\Program Files\PowerShell\7\pwsh.exe`; use `-PwshPath` if it's installed elsewhere) run by `C:\Program Files\PowerShell\7\pwsh.exe`; use `-PwshPath` if it's installed elsewhere)
5. Preview the payloads, then backfill 30 days so the history view is populated straight away 3. Preview the payloads, then backfill 30 days so the history view is populated straight away
(from `pwsh`): (from `pwsh`):
```powershell ```powershell
& 'C:\Program Files\BackupDashboard\Send-VeeamBackupStatus.ps1' -DryRun & 'C:\Program Files\BackupDashboard\Send-VeeamBackupStatus.ps1' -DryRun
@@ -47,11 +44,10 @@ delete the scheduled task and both `BackupDashboard` folders.
| Message | Session reason, each VM or object that didn't succeed, and warning/error lines from the session log | | Message | Session reason, each VM or object that didn't succeed, and warning/error lines from the session log |
| Size | `BackupStats.BackupSize` | | Size | `BackupStats.BackupSize` |
=======
**Backup copy jobs (VBR 12+):** Veeam records their sessions under hidden per-source "worker" jobs. **Backup copy jobs (VBR 12+):** Veeam records their sessions under hidden per-source "worker" jobs.
The collector files each worker session under its copy job (via the worker's `PolicyJobId`), so a The collector files each worker session under its copy job (via the worker's `PolicyJobId`), so a
copy job gets one run per source job it copies, with the source in `copied_from`. copy job gets one run per source job it copies, with the source in `copied_from`.
i
Each run covers the last 48 hours (`LookbackHours`), so a few missed runs (server rebooting, API Each run covers the last 48 hours (`LookbackHours`), so a few missed runs (server rebooting, API
down) fill in automatically. Sessions already delivered are recorded in down) fill in automatically. Sessions already delivered are recorded in
`veeam-collector.state.json`. Running sessions are re-sent until they finish. `veeam-collector.state.json`. Running sessions are re-sent until they finish.
@@ -62,7 +58,6 @@ down) fill in automatically. Sessions already delivered are recorded in
- Exit codes: `0` OK, `1` some jobs failed to send (they're retried next run), `2` setup failed - Exit codes: `0` OK, `1` some jobs failed to send (they're retried next run), `2` setup failed
(config, Veeam module, or Veeam connection). (config, Veeam module, or Veeam connection).
- `API key rejected`: the key was revoked or mistyped. Re-run the installer with a new key. - `API key rejected`: the key was revoked or mistyped. Re-run the installer with a new key.
=======
- A job shows **Unknown** on the dashboard: the collector sent the job but matched no sessions to it. - A job shows **Unknown** on the dashboard: the collector sent the job but matched no sessions to it.
Run `.\Get-VeeamCollectorDiagnostics.ps1` (read-only). It lists sessions whose job ID matches no Run `.\Get-VeeamCollectorDiagnostics.ps1` (read-only). It lists sessions whose job ID matches no
job, plus the type and link properties of the jobs left without sessions. job, plus the type and link properties of the jobs left without sessions.
+1 -1
View File
@@ -3,7 +3,7 @@
Reports Veeam Backup & Replication job results to the Backup Dashboard. Reports Veeam Backup & Replication job results to the Backup Dashboard.
.DESCRIPTION .DESCRIPTION
Runs on the VBR server, normally as a SYSTEM scheduled task every 15 minutes Runs on the VBR server, normally as a SYSTEM scheduled task every 60 minutes
(see Install-VeeamCollector.ps1). For every backup, replica and backup copy job it sends: (see Install-VeeamCollector.ps1). For every backup, replica and backup copy job it sends:
- the job's next scheduled run (drives the dashboard's Overdue flag), and - the job's next scheduled run (drives the dashboard's Overdue flag), and
- every session from the last LookbackHours that the dashboard hasn't already received - every session from the last LookbackHours that the dashboard hasn't already received
-5
View File
@@ -6,13 +6,8 @@ After=network.target
User=backupdash User=backupdash
Group=backupdash Group=backupdash
WorkingDirectory=/opt/backup-dashboard/backend WorkingDirectory=/opt/backup-dashboard/backend
<<<<<<< HEAD
Environment=BACKUP_DASH_DB=/var/lib/backup-dashboard/backup-dashboard.db
Environment=BACKUP_DASH_RETENTION_DAYS=90
=======
# Settings live in this file so manage.py reads exactly the same ones (see deploy/backup-dashboard.env) # Settings live in this file so manage.py reads exactly the same ones (see deploy/backup-dashboard.env)
EnvironmentFile=/etc/backup-dashboard/backup-dashboard.env EnvironmentFile=/etc/backup-dashboard/backup-dashboard.env
>>>>>>> add-backend-api
# Bound to localhost: only nginx can reach it, so nginx's auth can't be bypassed from outside. # Bound to localhost: only nginx can reach it, so nginx's auth can't be bypassed from outside.
# One worker is plenty at this scale and keeps SQLite writes simple. # One worker is plenty at this scale and keeps SQLite writes simple.
ExecStart=/opt/backup-dashboard/venv/bin/uvicorn --factory app.main:create_app --host 127.0.0.1 --port 8000 --proxy-headers ExecStart=/opt/backup-dashboard/venv/bin/uvicorn --factory app.main:create_app --host 127.0.0.1 --port 8000 --proxy-headers