From 821f518bb30fe0b83d9754d346bed948c444e914 Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Mon, 2 Mar 2026 16:08:38 +1300 Subject: [PATCH] Process 1 build 0203261608 --- .build_number | 2 +- backend/app/build_info.py | 2 +- frontend/app/admin/SettingsPage.tsx | 15 +- frontend/app/globals.css | 176 ++++++++++++++++++ frontend/app/ui/AdminShell.tsx | 15 +- scripts/build_release.ps1 | 2 +- scripts/process1.ps1 | 275 ++++++++++++++++++++++++++++ 7 files changed, 466 insertions(+), 21 deletions(-) create mode 100644 scripts/process1.ps1 diff --git a/.build_number b/.build_number index 3a3390b..632b7a0 100644 --- a/.build_number +++ b/.build_number @@ -1 +1 @@ -0203261511 +0203261608 diff --git a/backend/app/build_info.py b/backend/app/build_info.py index 2cbf598..a40700a 100644 --- a/backend/app/build_info.py +++ b/backend/app/build_info.py @@ -1,4 +1,4 @@ -BUILD_NUMBER = "0203261511" +BUILD_NUMBER = "0203261608" CHANGELOG = '2026-01-22\\n- Initial commit\\n- Ignore build artifacts\\n- Update README\\n- Update README with Docker-first guide\\n\\n2026-01-23\\n- Fix cache titles via Seerr media lookup\\n- Split search actions and improve download options\\n- Fallback manual grab to qBittorrent\\n- Hide header actions when signed out\\n- Add feedback form and webhook\\n- Fix cache titles and move feedback link\\n- Show available status on landing when in Jellyfin\\n- Add default branding assets when missing\\n- Use bundled branding assets\\n- Remove password fields from users page\\n- Add Docker Hub compose override\\n- Fix backend Dockerfile paths for root context\\n- Copy public assets into frontend image\\n- Use backend branding assets for logo and favicon\\n\\n2026-01-24\\n- Route grabs through Sonarr/Radarr only\\n- Document fix buttons in how-it-works\\n- Clarify how-it-works steps and fixes\\n- Map Prowlarr releases to Arr indexers for manual grab\\n- Improve request handling and qBittorrent categories\\n\\n2026-01-25\\n- Add site banner, build number, and changelog\\n- Automate build number tagging and sync\\n- Improve mobile header layout\\n- Move account actions into avatar menu\\n- Add user stats and activity tracking\\n- Add Jellyfin login cache and admin-only stats\\n- Tidy request sync controls\\n- Seed branding logo from bundled assets\\n- Serve bundled branding assets by default\\n- Harden request cache titles and cache-only reads\\n- Build 2501262041\\n\\n2026-01-26\\n- Fix cache title hydration\\n- Fix sync progress bar animation\\n\\n2026-01-27\\n- Add cache control artwork stats\\n- Improve cache stats performance (build 271261145)\\n- Fix backend cache stats import (build 271261149)\\n- Clarify request sync settings (build 271261159)\\n- Bump build number to 271261202\\n- Fix request titles in snapshots (build 271261219)\\n- Fix snapshot title fallback (build 271261228)\\n- Add cache load spinner (build 271261238)\\n- Bump build number (process 2) 271261322\\n- Add service test buttons (build 271261335)\\n- Fallback to TMDB when artwork cache fails (build 271261524)\\n- Hydrate missing artwork from Seerr (build 271261539)\\n\\n2026-01-29\\n- release: 2901262036\\n- release: 2901262044\\n- release: 2901262102\\n- Hardcode build number in backend\\n- Bake build number and changelog\\n- Update full changelog\\n- Tidy full changelog\\n- Build 2901262240: cache users\n\n2026-01-30\n- Merge backend and frontend into one container' diff --git a/frontend/app/admin/SettingsPage.tsx b/frontend/app/admin/SettingsPage.tsx index 4d449df..067c176 100644 --- a/frontend/app/admin/SettingsPage.tsx +++ b/frontend/app/admin/SettingsPage.tsx @@ -1613,11 +1613,11 @@ export default function SettingsPage({ section }: SettingsPageProps) { > {status &&
{status}
} {settingsSections.length > 0 ? ( -
+
{settingsSections .filter(shouldRenderSection) .map((sectionGroup) => ( -
+

{sectionGroup.key === 'requests' ? 'Request sync controls' : sectionGroup.title} @@ -2228,6 +2228,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { ) : null}

)} {showLogs && ( -
+

Activity log

@@ -2283,7 +2284,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
)} {showCacheExtras && ( -
+

Saved requests (cache)

@@ -2312,7 +2313,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
)} {showMaintenance && ( -
+

Maintenance

@@ -2379,7 +2380,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
)} {showRequestsExtras && ( -
+

Scheduled tasks

diff --git a/frontend/app/globals.css b/frontend/app/globals.css index a0aafad..045ab6b 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1545,6 +1545,13 @@ button span { align-items: end; } +.settings-section-actions .settings-action-button { + width: 190px; + min-width: 190px; + flex: 0 0 190px; + justify-content: center; +} + .settings-inline-field { display: grid; gap: 6px; @@ -6214,6 +6221,12 @@ textarea { width: 100%; } + .settings-section-actions .settings-action-button { + width: 100%; + min-width: 0; + flex-basis: auto; + } + .sync-meta, .diagnostic-card-top, .diagnostics-category-header, @@ -6233,3 +6246,166 @@ textarea { overflow-wrap: anywhere; } } + +/* Final admin shell + settings section cleanup */ +.admin-shell, +.admin-shell-nav, +.admin-card, +.admin-shell-rail, +.admin-sidebar, +.admin-panel { + min-width: 0; +} + +.admin-shell { + display: grid; + grid-template-columns: minmax(220px, 260px) minmax(0, 1fr); + grid-template-areas: "nav main"; + gap: 22px; + align-items: start; +} + +.admin-shell.admin-shell--with-rail { + grid-template-columns: minmax(220px, 260px) minmax(0, 1fr) minmax(300px, 380px); + grid-template-areas: "nav main rail"; +} + +.admin-shell-nav { + grid-area: nav; +} + +.admin-card { + grid-area: main; +} + +.admin-shell-rail { + grid-area: rail; + position: sticky; + top: 20px; + align-self: start; + display: grid; + gap: 10px; +} + +.admin-zone-stack { + gap: 18px; +} + +.admin-zone { + display: grid; + gap: 14px; + padding: 18px; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.07); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.015)), + rgba(255, 255, 255, 0.012); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025); +} + +[data-theme='light'] .admin-zone { + border-color: rgba(17, 19, 24, 0.08); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.72)), + rgba(17, 19, 24, 0.018); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5); +} + +.admin-zone .section-header { + align-items: flex-start; + padding-bottom: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +[data-theme='light'] .admin-zone .section-header { + border-bottom-color: rgba(17, 19, 24, 0.08); +} + +.admin-zone .section-header h2 { + position: relative; + display: inline-block; + padding-bottom: 8px; +} + +.admin-zone .section-header h2::after { + content: ''; + position: absolute; + left: 0; + bottom: 0; + width: 52px; + height: 2px; + border-radius: 999px; + background: linear-gradient(90deg, var(--accent-2), rgba(255, 255, 255, 0)); +} + +.admin-zone .section-subtitle { + margin-top: -4px; + font-size: 13px; + line-height: 1.5; +} + +@media (max-width: 1280px) { + .admin-shell { + grid-template-columns: minmax(220px, 250px) minmax(0, 1fr); + grid-template-areas: "nav main"; + } + + .admin-shell.admin-shell--with-rail { + grid-template-areas: + "nav main" + "nav rail"; + } + + .admin-shell-rail { + position: static; + top: auto; + width: 100%; + } +} + +@media (max-width: 1080px) { + .admin-shell, + .admin-shell.admin-shell--with-rail { + grid-template-columns: minmax(0, 1fr); + gap: 16px; + } + + .admin-shell { + grid-template-areas: + "nav" + "main"; + } + + .admin-shell.admin-shell--with-rail { + grid-template-areas: + "nav" + "main" + "rail"; + } + + .admin-shell-nav, + .admin-card, + .admin-shell-rail { + width: 100%; + } + + .admin-shell-nav { + position: static; + top: auto; + } + + .admin-grid, + .users-page-toolbar-grid, + .users-summary-grid, + .users-page-overview-grid, + .maintenance-action-grid, + .schedule-grid, + .diagnostics-inline-summary, + .diagnostics-grid { + grid-template-columns: 1fr; + } + + .admin-zone { + padding: 16px; + } +} diff --git a/frontend/app/ui/AdminShell.tsx b/frontend/app/ui/AdminShell.tsx index f195cf2..cc6ebe5 100644 --- a/frontend/app/ui/AdminShell.tsx +++ b/frontend/app/ui/AdminShell.tsx @@ -12,8 +12,10 @@ type AdminShellProps = { } export default function AdminShell({ title, subtitle, actions, rail, children }: AdminShellProps) { + const hasRail = Boolean(rail) + return ( -
+
@@ -27,16 +29,7 @@ export default function AdminShell({ title, subtitle, actions, rail, children }:
{children} - + {hasRail ? : null}
) } diff --git a/scripts/build_release.ps1 b/scripts/build_release.ps1 index 70bed2e..70d288b 100644 --- a/scripts/build_release.ps1 +++ b/scripts/build_release.ps1 @@ -4,7 +4,7 @@ $repoRoot = Resolve-Path "$PSScriptRoot\\.." Set-Location $repoRoot $now = Get-Date -$buildNumber = "{0}{1}{2}{3}{4}" -f $now.ToString("dd"), $now.ToString("M"), $now.ToString("yy"), $now.ToString("HH"), $now.ToString("mm") +$buildNumber = "{0}{1}{2}{3}{4}" -f $now.ToString("dd"), $now.ToString("MM"), $now.ToString("yy"), $now.ToString("HH"), $now.ToString("mm") Write-Host "Build number: $buildNumber" diff --git a/scripts/process1.ps1 b/scripts/process1.ps1 new file mode 100644 index 0000000..62143d4 --- /dev/null +++ b/scripts/process1.ps1 @@ -0,0 +1,275 @@ +param( + [string]$CommitMessage, + [switch]$SkipCommit, + [switch]$SkipDiscord +) + +$ErrorActionPreference = "Stop" + +$repoRoot = Resolve-Path "$PSScriptRoot\.." +Set-Location $repoRoot + +$Utf8NoBom = New-Object System.Text.UTF8Encoding($false) +$script:CurrentStep = "initializing" + +function Write-TextFile { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$Content + ) + + $fullPath = Join-Path $repoRoot $Path + $normalized = $Content -replace "`r`n", "`n" + [System.IO.File]::WriteAllText($fullPath, $normalized, $Utf8NoBom) +} + +function Read-TextFile { + param([Parameter(Mandatory = $true)][string]$Path) + + $fullPath = Join-Path $repoRoot $Path + return [System.IO.File]::ReadAllText($fullPath) +} + +function Get-EnvFileValue { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$Name + ) + + if (-not (Test-Path $Path)) { + return $null + } + + $match = Select-String -Path $Path -Pattern "^$([regex]::Escape($Name))=(.*)$" | Select-Object -First 1 + if (-not $match) { + return $null + } + + return $match.Matches[0].Groups[1].Value.Trim() +} + +function Get-DiscordWebhookUrl { + $candidateNames = @( + "PROCESS_DISCORD_WEBHOOK_URL", + "MAGENT_NOTIFY_DISCORD_WEBHOOK_URL", + "DISCORD_WEBHOOK_URL" + ) + + foreach ($name in $candidateNames) { + $value = [System.Environment]::GetEnvironmentVariable($name) + if (-not [string]::IsNullOrWhiteSpace($value)) { + return $value.Trim() + } + } + + foreach ($name in $candidateNames) { + $value = Get-EnvFileValue -Path ".env" -Name $name + if (-not [string]::IsNullOrWhiteSpace($value)) { + return $value.Trim() + } + } + + $configPath = Join-Path $repoRoot "backend/app/config.py" + if (Test-Path $configPath) { + $configContent = Read-TextFile -Path "backend/app/config.py" + $match = [regex]::Match( + $configContent, + 'discord_webhook_url:\s*Optional\[str\]\s*=\s*Field\(\s*default="([^"]+)"', + [System.Text.RegularExpressions.RegexOptions]::Singleline + ) + if ($match.Success) { + return $match.Groups[1].Value.Trim() + } + } + + return $null +} + +function Send-DiscordUpdate { + param( + [Parameter(Mandatory = $true)][string]$Title, + [Parameter(Mandatory = $true)][string]$Body + ) + + if ($SkipDiscord) { + Write-Host "Skipping Discord notification." + return + } + + $webhookUrl = Get-DiscordWebhookUrl + if ([string]::IsNullOrWhiteSpace($webhookUrl)) { + Write-Warning "Discord webhook not configured for Process 1." + return + } + + $content = "**$Title**`n$Body" + Invoke-RestMethod -Method Post -Uri $webhookUrl -ContentType "application/json" -Body (@{ content = $content } | ConvertTo-Json -Compress) | Out-Null +} + +function Get-BuildNumber { + $current = "" + if (Test-Path ".build_number") { + $current = (Get-Content ".build_number" -Raw).Trim() + } + + $candidate = Get-Date + $buildNumber = $candidate.ToString("ddMMyyHHmm") + if ($buildNumber -eq $current) { + $buildNumber = $candidate.AddMinutes(1).ToString("ddMMyyHHmm") + } + + return $buildNumber +} + +function Wait-ForHttp { + param( + [Parameter(Mandatory = $true)][string]$Url, + [int]$Attempts = 30, + [int]$DelaySeconds = 2 + ) + + $lastError = $null + for ($attempt = 1; $attempt -le $Attempts; $attempt++) { + try { + return Invoke-RestMethod -Uri $Url -TimeoutSec 10 + } catch { + $lastError = $_ + Start-Sleep -Seconds $DelaySeconds + } + } + + throw $lastError +} + +function Update-BuildFiles { + param([Parameter(Mandatory = $true)][string]$BuildNumber) + + Write-TextFile -Path ".build_number" -Content "$BuildNumber`n" + + $buildInfo = Read-TextFile -Path "backend/app/build_info.py" + $updatedBuildInfo = [regex]::Replace( + $buildInfo, + '^BUILD_NUMBER = "\d+"$', + "BUILD_NUMBER = `"$BuildNumber`"", + [System.Text.RegularExpressions.RegexOptions]::Multiline + ) + Write-TextFile -Path "backend/app/build_info.py" -Content $updatedBuildInfo + + $envPath = Join-Path $repoRoot ".env" + if (Test-Path $envPath) { + $envContent = Read-TextFile -Path ".env" + if ($envContent -match '^BUILD_NUMBER=.*$') { + $updatedEnv = [regex]::Replace( + $envContent, + '^BUILD_NUMBER=.*$', + "BUILD_NUMBER=$BuildNumber", + [System.Text.RegularExpressions.RegexOptions]::Multiline + ) + } else { + $updatedEnv = "BUILD_NUMBER=$BuildNumber`n$envContent" + } + Write-TextFile -Path ".env" -Content $updatedEnv + } + + Push-Location frontend + try { + npm version $BuildNumber --no-git-tag-version --allow-same-version | Out-Null + } finally { + Pop-Location + } +} + +function Get-ChangedFilesSummary { + $files = git diff --cached --name-only + if (-not $files) { + return "No staged files" + } + + $count = ($files | Measure-Object).Count + $sample = $files | Select-Object -First 8 + $summary = ($sample -join ", ") + if ($count -gt $sample.Count) { + $summary = "$summary, +$($count - $sample.Count) more" + } + + return "$count files: $summary" +} + +$buildNumber = $null +$branch = $null +$commit = $null +$publicInfo = $null +$changedFiles = "No staged files" + +try { + $branch = (git rev-parse --abbrev-ref HEAD).Trim() + $buildNumber = Get-BuildNumber + Write-Host "Process 1 build number: $buildNumber" + + $script:CurrentStep = "updating build metadata" + Update-BuildFiles -BuildNumber $buildNumber + + $script:CurrentStep = "rebuilding local docker stack" + docker compose up -d --build + + $script:CurrentStep = "verifying backend health" + $health = Wait-ForHttp -Url "http://127.0.0.1:8000/health" + if ($health.status -ne "ok") { + throw "Health endpoint returned unexpected payload: $($health | ConvertTo-Json -Compress)" + } + + $script:CurrentStep = "verifying public build metadata" + $publicInfo = Wait-ForHttp -Url "http://127.0.0.1:8000/site/public" + if ($publicInfo.buildNumber -ne $buildNumber) { + throw "Public build number mismatch. Expected $buildNumber but got $($publicInfo.buildNumber)." + } + + $script:CurrentStep = "committing changes" + git add -A + $changedFiles = Get-ChangedFilesSummary + if ((git status --short).Trim()) { + if (-not $SkipCommit) { + if ([string]::IsNullOrWhiteSpace($CommitMessage)) { + $CommitMessage = "Process 1 build $buildNumber" + } + git commit -m $CommitMessage + } + } + + $commit = (git rev-parse --short HEAD).Trim() + + $body = @( + "Build: $buildNumber" + "Branch: $branch" + "Commit: $commit" + "Health: ok" + "Public build: $($publicInfo.buildNumber)" + "Changes: $changedFiles" + ) -join "`n" + Send-DiscordUpdate -Title "Process 1 complete" -Body $body + + Write-Host "Process 1 completed successfully." +} catch { + $failureCommit = "" + try { + $failureCommit = (git rev-parse --short HEAD).Trim() + } catch { + $failureCommit = "unknown" + } + + $failureBody = @( + "Build: $buildNumber" + "Branch: $branch" + "Commit: $failureCommit" + "Step: $script:CurrentStep" + "Error: $($_.Exception.Message)" + ) -join "`n" + + try { + Send-DiscordUpdate -Title "Process 1 failed" -Body $failureBody + } catch { + Write-Warning "Failed to send Discord failure notification: $($_.Exception.Message)" + } + + throw +}