diff --git a/deploy/windows/installer/ShopDBFlask.iss b/deploy/windows/installer/ShopDBFlask.iss index e39dbfe6..31e47078 100644 --- a/deploy/windows/installer/ShopDBFlask.iss +++ b/deploy/windows/installer/ShopDBFlask.iss @@ -201,6 +201,7 @@ var // the check takes a few seconds, runs hidden, and gives no sign of life. CheckingPage: TOutputProgressWizardPage; PluginPage: TInputOptionWizardPage; + CatalogPage: TInputOptionWizardPage; PluginNames: TArrayOfString; PluginPageReady: Boolean; DbPageReady: Boolean; @@ -397,7 +398,21 @@ begin if Trim(PluginNames[I]) <> '' then PluginPage.Add(PluginLabel(Trim(PluginNames[I]))); - DbChoicePage := CreateInputOptionPage(PluginPage.ID, + // Offered, not forced. A site that machines nothing inherits 21 machine types + // it will never use, and a catalog nobody wanted is just clutter in every + // dropdown. Ticked by default because the alternative is retyping a vendor and + // model list another site already built. + CatalogPage := CreateInputOptionPage(PluginPage.ID, + 'Starter data', 'Load the shared equipment catalog?', + 'Vendors, model numbers and the type lists, as built up across the other ' + + 'sites. It only ADDS - nothing existing is changed or removed - and it can ' + + 'be loaded later with "flask seed catalog". No assets, locations or people ' + + 'are included; this is catalogue information only.', + False, False); + CatalogPage.Add('Load the shared vendor and model catalog'); + CatalogPage.Values[0] := True; + + DbChoicePage := CreateInputOptionPage(CatalogPage.ID, 'Database', 'Where should ShopDB-Flask store its data?', 'Most sites already run MySQL for the existing shopdb application. If so, ' + 'choose the second option - installing a second server would collide on ' + @@ -1177,6 +1192,8 @@ begin ' -OnFailure never' + ' -ClientIpSource ' + ClientIpSourceArg + ' -SitePlugins "' + SelectedPlugins + '"'; + if CatalogPage.Values[0] then + Common := Common + ' -SeedCatalog'; // Subpath deployment: an IIS Application under the existing site instead of a // site of its own. Appended here because Pascal has no conditional expression. diff --git a/deploy/windows/installer/shopdb-admin.ps1 b/deploy/windows/installer/shopdb-admin.ps1 index 43733b7a..547c56c9 100644 --- a/deploy/windows/installer/shopdb-admin.ps1 +++ b/deploy/windows/installer/shopdb-admin.ps1 @@ -22,7 +22,7 @@ [CmdletBinding()] param( [ValidateSet('menu','status','start','stop','restart','logs','open','backup', - 'check','sessions','plugins','add-plugin','verify','uninstall')] + 'check','repair','sessions','plugins','add-plugin','verify','uninstall')] [string] $Command = 'menu', [string] $Path = '', # Machine-readable output for 'check'. The people running this are expected to @@ -577,11 +577,101 @@ function Invoke-Check { $env:FLASK_APP = 'shopdb' try { & $flask db-utils preflight 2>&1 | ForEach-Object { Say " $_" } } finally { Pop-Location } + + # Surface an unfinished install HERE, where somebody is already looking, + # rather than leaving them to infer it from 500s on unrelated pages. + $state = Test-Provisioned + if ($state.SchemaCurrent -eq $false -or $state.Seeded -eq $false) { + Say '' + Say ' THIS SERVER IS NOT FULLY PROVISIONED' 'Red' + foreach ($detail in $state.Detail) { Say (" - {0}" -f $detail) 'Red' } + Say ' Most pages will answer 500 until this is fixed. Run:' 'Yellow' + Say ' shopdb-admin.ps1 repair' 'White' + } + Say '' Say ' For help from an AI assistant, paste the output of:' 'DarkGray' Say ' shopdb-admin.ps1 check -Json' 'White' } +function Test-Provisioned { + """Is the schema current and the reference data seeded?""" + # A server whose migrations or seeds never completed does not fail politely: + # it answers 500 on most pages and 404 on settings that were never created, + # which reads as a broken application rather than an unfinished install. One + # site was stood up that way and spent a morning being debugged as a bug. + $result = @{ SchemaCurrent = $true; Seeded = $true; Detail = @() } + + $out = Invoke-Flask @('db','current') + $current = ($out | Out-String) + $head = (Invoke-Flask @('db','heads') | Out-String) + if ($script:LastFlaskExit -ne 0) { + $result.SchemaCurrent = $false + $result.Detail += 'could not read the schema version' + } elseif ($current -notmatch '\(head\)' -and $head.Trim()) { + # `db current` appends "(head)" when the database is at the newest + # revision. Its absence means migrations are outstanding. + $result.SchemaCurrent = $false + $result.Detail += 'database schema is behind the application' + } + + # Sentinel seeds. permissions, settings and the reference data are each + # created by a `flask seed` command, and their absence is what produced the + # 404s on settings keys at a site whose install never finished. + $seedOut = (Invoke-Flask @('db-utils','seed-state') | Out-String) + if ($seedOut -match 'No such command') { + # An older build with no seed-state. Report UNKNOWN rather than healthy: + # claiming a clean bill of health from a check that did not run is how a + # broken server passes inspection. + $result.Seeded = $null + $result.Detail += 'seed state could not be checked (older build)' + } elseif ($script:LastFlaskExit -ne 0 -or $seedOut -match 'MISSING') { + $result.Seeded = $false + $result.Detail += 'seed data is missing (permissions, settings or reference data)' + } + return $result +} + +function Invoke-Repair { + Head 'Repair provisioning' + Say ' Brings the database up to the application: migrations, plugin' + Say ' migrations, and the seed data. Every step is idempotent, so running' + Say ' this on a healthy server changes nothing.' + Say '' + + $steps = @( + @{ Name = 'core schema'; Args = @('db','upgrade') }, + @{ Name = 'plugin schemas'; Args = @('plugin','upgrade-all') }, + @{ Name = 'permissions'; Args = @('seed','permissions') }, + @{ Name = 'settings'; Args = @('seed','settings') }, + @{ Name = 'reference data'; Args = @('seed','reference-data') } + ) + $failed = @() + foreach ($step in $steps) { + Say (" {0} ..." -f $step.Name) 'White' + $out = Invoke-Flask $step.Args + if ($script:LastFlaskExit -ne 0) { + $failed += $step.Name + Say (" FAILED (exit {0})" -f $script:LastFlaskExit) 'Red' + $out | Select-Object -Last 6 | ForEach-Object { Say " $_" 'Red' } + } else { + Say ' done' 'Green' + } + } + + if ($failed.Count -gt 0) { + Say '' + Say (" {0} step(s) failed: {1}" -f $failed.Count, ($failed -join ', ')) 'Red' + Say ' Nothing further was skipped - each step ran independently.' 'Red' + Say ' Send the output above, or run: shopdb-admin.ps1 check -Json' 'Red' + return + } + + Say '' + Say ' Provisioning complete. Restarting the application.' 'Green' + Restart-App +} + function Show-Sessions { Head 'Worker processes' $w = Get-CimInstance Win32_Process -Filter "Name='w3wp.exe'" -EA SilentlyContinue @@ -856,6 +946,7 @@ switch ($Command) { 'open' { Open-Site } 'backup' { Backup-Db $Path } 'check' { Invoke-Check } + 'repair' { Invoke-Repair } 'sessions' { Show-Sessions } 'plugins' { Show-Plugins } 'add-plugin'{ Add-Plugin $Path } diff --git a/deploy/windows/installer/shopdb-install.ps1 b/deploy/windows/installer/shopdb-install.ps1 index d7c04854..b3ffb528 100644 --- a/deploy/windows/installer/shopdb-install.ps1 +++ b/deploy/windows/installer/shopdb-install.ps1 @@ -127,6 +127,10 @@ param( # client reads as 127.0.0.1: the GE-Enforce IP allowlist, the dashboard # visitor-location lookup and per-host login rate limiting all break quietly. [ValidateSet('direct','proxy')] [string] $ClientIpSource = 'direct', + # Load the shared vendor/model catalog during stage 3. Offered by the + # wizard rather than assumed: a site that machines nothing does not want + # 21 machine types cluttering its dropdowns. + [switch] $SeedCatalog, # Required before this installer will alter an installation it did not # create. Everything here is built for a greenfield server: it makes its own # Python, its own venv and its own IIS objects, and its upgrade path assumes @@ -1747,6 +1751,14 @@ build to get a matching pair, then send the install log to support. Invoke-Native $Flask @('seed',$seed) "seed $seed" } + # Additive and idempotent, so a re-run adds only what is genuinely new + # and never overwrites a description somebody corrected here. + if ($SeedCatalog) { + Write-Log 'flask seed catalog (shared vendor and model catalog)' + $catalogOut = Invoke-Native $Flask @('seed','catalog') 'seed catalog' -OkExit 0,1 + $catalogOut | Select-Object -Last 12 | ForEach-Object { Write-Log " $_" } + } + # Plugin registry starts empty on a fresh box. apply-profile resolves the # declared set's hard-dependency closure and installs + enables it in # dependency order, idempotently (ADR-013). It fails loudly if the profile diff --git a/plugins/network/frontend/views/NetworkDeviceForm.vue b/plugins/network/frontend/views/NetworkDeviceForm.vue index 605144b3..87fc0271 100644 --- a/plugins/network/frontend/views/NetworkDeviceForm.vue +++ b/plugins/network/frontend/views/NetworkDeviceForm.vue @@ -205,30 +205,41 @@