From 7477f16407d4a46bbb06dc841aa8ba5ee3cbd875 Mon Sep 17 00:00:00 2001 From: stefanodangelo <61756862+stefanodangelo@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:15:59 +0200 Subject: [PATCH 1/3] chore: add dev scripts to run the mock UI and push to a printer scripts/run-dev-ui.ps1 starts the backend and UI locally and falls back to mock mode when Moonraker is not reachable. scripts/push-to-printer.ps1 pushes the working tree to a printer for development: -WebOnly (default) syncs the web files and restarts only the web service, -Full runs the installer after the unit tests pass and the printer is idle, -Rollback restores the installer's extruder.py backup, -DryRun packs the tree without touching the printer. -PrinterHost is required. Remove the per-setup entries from .gitignore, including scripts/, which would hide these files. Local ignores belong in .git/info/exclude. --- .gitignore | 5 - scripts/push-to-printer.ps1 | 275 ++++++++++++++++++++++++++++++++++++ scripts/run-dev-ui.ps1 | 76 ++++++++++ 3 files changed, 351 insertions(+), 5 deletions(-) create mode 100644 scripts/push-to-printer.ps1 create mode 100644 scripts/run-dev-ui.ps1 diff --git a/.gitignore b/.gitignore index ab309a36..15f80c69 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,3 @@ __pycache__/ *.log *.log.[0-9] /dist/ - -# Claude Code local files -.claude/ -scripts/ -.devdata/ diff --git a/scripts/push-to-printer.ps1 b/scripts/push-to-printer.ps1 new file mode 100644 index 00000000..5d914056 --- /dev/null +++ b/scripts/push-to-printer.ps1 @@ -0,0 +1,275 @@ +<# +.SYNOPSIS +Push the working tree to a printer without cutting a release. + +.DESCRIPTION +Two push classes; -WebOnly is the default. + + -WebOnly (default) Syncs multiace/web, multiace/i18n and multiace/tools, + restarts the web service, never touches Klipper. + The inner development loop: the worst case is a + broken web page. + + -Full Syncs the whole multiace tree and runs + install_multiace.sh --install-web, then restarts + Klipper. It replaces Klipper extras, so it is gated on + the unit tests passing and the printer being idle. + + -Rollback Restores the installer's own backup of + kinematics/extruder.py and restarts Klipper. + + -DryRun Stages and packs the tree, then prints what would + happen. Nothing is copied or run on the printer. + +-PrinterHost is required. SSH must already work with a key (no password +prompt): every ssh call runs with BatchMode=yes. + +.EXAMPLE + .\push-to-printer.ps1 -PrinterHost 192.168.1.50 + .\push-to-printer.ps1 -PrinterHost 192.168.1.50 -DryRun + .\push-to-printer.ps1 -PrinterHost 192.168.1.50 -Full + .\push-to-printer.ps1 -PrinterHost 192.168.1.50 -Rollback +#> +[CmdletBinding()] +param( + [Parameter(Mandatory)][string]$PrinterHost, + [string]$PrinterUser = "root", + [switch]$WebOnly, + [switch]$Full, + [switch]$Rollback, + [switch]$DryRun +) + +$ErrorActionPreference = "Stop" + +if ($WebOnly -and $Full) { throw "-WebOnly and -Full are mutually exclusive." } + +$RepoRoot = Split-Path -Parent $PSScriptRoot +$RemoteTar = "/tmp/multiace-dev.tar.gz" +$RemoteDir = "/tmp/multiace-dev" +$Mode = if ($Full) { "full" } else { "web-only" } + +function Say ($m) { Write-Host "STATUS: $m" } +function Warn ($m) { Write-Warning $m } +function Die ($m) { Write-Error $m; exit 1 } + +# With $ErrorActionPreference = "Stop", any stderr line from a native .exe +# becomes a terminating error regardless of its exit code. Run native +# commands with "Continue" and judge success from $LASTEXITCODE instead. +function Invoke-NativeChecked { + param([Parameter(Mandatory)][scriptblock]$Script) + $prevEAP = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { & $Script } finally { $ErrorActionPreference = $prevEAP } +} + +function Invoke-Remote ($Command) { + # ssh ships the string to the printer's shell byte for byte, and a stray + # CR from a CRLF here-string breaks lines like "set -e`r". Strip it. + $Command = $Command -replace "`r", "" + if ($DryRun) { + Write-Host "DRY-RUN: ssh $PrinterUser@$PrinterHost $Command" + return "" + } + $out = Invoke-NativeChecked { ssh -o BatchMode=yes "$PrinterUser@$PrinterHost" $Command 2>&1 } + if ($LASTEXITCODE -ne 0) { Die "remote command failed ($LASTEXITCODE): $out" } + return ($out -join "`n") +} + +function Get-PrintState { + try { + $r = Invoke-RestMethod -TimeoutSec 5 ` + -Uri "http://$PrinterHost/printer/objects/query?print_stats" + return [string]$r.result.status.print_stats.state + } catch { + return "" + } +} + +# Fails closed: "cannot tell" is not the same answer as "idle". +function Assert-PrinterIdle { + $state = Get-PrintState + if (-not $state) { $state = "unknown" } + if ($state -in @("printing", "paused", "unknown")) { + Die ("printer is '$state'. A full install or rollback restarts Klipper " + + "and would abort the print. Wait for it to finish.") + } + Say "printer state: $state" +} + +function Invoke-LocalTests { + Say "running the unit tests (a tree that fails tests is not pushed)" + if ($DryRun) { Write-Host "DRY-RUN: python -m pytest tests/unit -q"; return } + Push-Location $RepoRoot + try { + Invoke-NativeChecked { python -m pytest tests/unit -q } + if ($LASTEXITCODE -ne 0) { Die "tests failed - not pushing." } + } finally { Pop-Location } +} + +function Restart-Klipper { + Say "restarting Klipper" + # Not firmware_restart: that reloads the config without re-importing + # already-loaded extras modules, so changed .py files would sit unused. + try { Invoke-Remote '/etc/init.d/S60klipper restart' | Out-Null } + catch { Warn "S60klipper restart failed - check the printer" } +} + +function Restart-Web { + Say "restarting the multiACE web service" + Invoke-Remote '/etc/init.d/S98multiace-web restart >/dev/null 2>&1 || true' | Out-Null +} + +function Confirm-Recovery { + if ($DryRun) { return } + Say "waiting for the printer to come back" + $up = $false + foreach ($i in 1..60) { + try { + $h = Invoke-RestMethod -TimeoutSec 3 -Uri "http://$PrinterHost/multiace/api/health" + if ($h.status -eq "ok") { $up = $true; Say "multiACE web is up"; break } + } catch { } + Start-Sleep -Seconds 2 + } + if (-not $up) { + Warn ("multiACE web did not answer within 2 minutes. If this push broke " + + "something: .\push-to-printer.ps1 -PrinterHost $PrinterHost -Rollback") + } +} + +function Invoke-Rollback { + Say "restoring the installer's backup of extruder.py" + $script = @' +set -e +K=/home/lava/klipper/klippy/kinematics +if [ ! -f "$K/extruder_pre_multiace.py" ]; then + echo "ERROR: no extruder_pre_multiace.py backup found - nothing to roll back" >&2 + exit 1 +fi +cp "$K/extruder_pre_multiace.py" "$K/extruder.py" +echo "STATUS: restored extruder.py" +'@ + Invoke-Remote $script | Out-Null + Restart-Klipper + Say "rollback done" +} + +# --- main ------------------------------------------------------------------ + +if ($Rollback) { + Say "target: $PrinterUser@$PrinterHost class: rollback" + Assert-PrinterIdle + Invoke-Rollback + Confirm-Recovery + exit 0 +} + +Say "target: $PrinterUser@$PrinterHost class: $Mode" +if ($Mode -eq "full") { + Say "FULL push: replaces Klipper extras and restarts Klipper." + Invoke-LocalTests + Assert-PrinterIdle +} else { + Say "WEB-ONLY push: web, i18n and tools only. Klipper is not touched." + # A panel that blinks mid-print is bad UX, not damage: warn, do not refuse. + $state = Get-PrintState + if ($state -in @("printing", "paused")) { + Warn "printer is $state - the panel will blink, nothing mechanical is touched" + } +} + +$Stage = Join-Path ([System.IO.Path]::GetTempPath()) ("multiace-push-" + [guid]::NewGuid().ToString("N")) +New-Item -ItemType Directory -Path $Stage -Force | Out-Null +try { + Say "staging the working tree" + $sources = if ($Mode -eq "full") { @("multiace") } else { @("multiace/web", "multiace/tools", "multiace/i18n") } + foreach ($rel in $sources) { + $src = Join-Path $RepoRoot ($rel -replace "/", "\") + $dst = Join-Path $Stage ($rel -replace "/", "\") + New-Item -ItemType Directory -Path (Split-Path -Parent $dst) -Force | Out-Null + Copy-Item -Recurse -Force $src $dst + } + Get-ChildItem -Path $Stage -Recurse -Force -Directory ` + | Where-Object { $_.Name -eq "__pycache__" } | Remove-Item -Recurse -Force + Get-ChildItem -Path $Stage -Recurse -Force -Filter "*.pyc" | Remove-Item -Force + + # busybox ash fails a `#!/bin/bash\r` shebang with a bare "required file + # not found", so strip CRLF from everything the printer executes or + # parses. Write without a BOM, which breaks a shebang the same way. + $noBom = New-Object System.Text.UTF8Encoding $false + Get-ChildItem -Path $Stage -Recurse -Force -File ` + -Include '*.sh','*.cfg','*.py','*.json','*.sudoers','post-commit','S[0-9][0-9]*' ` + | ForEach-Object { + $text = [System.IO.File]::ReadAllText($_.FullName) + if ($text.Contains("`r`n")) { + [System.IO.File]::WriteAllText($_.FullName, ($text -replace "`r`n", "`n"), $noBom) + } + } + + Say "packing" + $tarball = Join-Path $Stage "multiace-dev.tar.gz" + # Git for Windows ships a GNU tar that reads "C:\..." as host:path, so + # prefer the Windows bsdtar when it exists. + $tarExe = Join-Path $env:SystemRoot "System32\tar.exe" + if (-not (Test-Path $tarExe)) { $tarExe = "tar" } + Invoke-NativeChecked { & $tarExe -C $Stage -czf $tarball multiace } + if ($LASTEXITCODE -ne 0) { Die "tar failed - is bsdtar/tar on PATH?" } + + if ($DryRun) { + $mb = [math]::Round((Get-Item $tarball).Length / 1MB, 2) + Say "dry run: would copy $mb MB to $PrinterHost ($Mode)" + exit 0 + } + + Say "copying to ${PrinterHost}:$RemoteTar" + # Not scp: its SFTP and legacy modes crashed sshd on the printer. Stream + # the file over a plain ssh exec channel as base64 instead. The base64 + # goes through a file and cmd's `<` redirection, because piping one very + # long string through PowerShell 5.1 into a native process truncates it. + $b64 = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($tarball)) + $b64File = Join-Path $Stage "payload.b64" + [System.IO.File]::WriteAllText($b64File, $b64, (New-Object System.Text.ASCIIEncoding)) + Invoke-NativeChecked { + cmd /c "ssh -T -o BatchMode=yes $PrinterUser@$PrinterHost `"base64 -d > $RemoteTar`" < `"$b64File`"" + } + if ($LASTEXITCODE -ne 0) { Die "streaming copy to the printer failed" } + + Say "extracting" + Invoke-Remote "rm -rf '$RemoteDir' && mkdir -p '$RemoteDir' && tar -C '$RemoteDir' -xzf '$RemoteTar'" | Out-Null + + if ($Mode -eq "full") { + Say "running install_multiace.sh --install-web" + Invoke-Remote "cd '$RemoteDir/multiace' && chmod +x install_multiace.sh && ./install_multiace.sh --install-web" + Restart-Klipper + Confirm-Recovery + } else { + # Same destinations and layout as install_multiace.sh uses for these + # directories (multiace/i18n lands in multiace_web/i18n, where the + # backend looks for it). + Say "syncing the web files in place (no installer, no Klipper restart)" + $sync = @" +set -e +WEB_DEST=/home/lava/multiace_web +if [ ! -d "`$WEB_DEST/backend" ]; then + echo 'ERROR: '"`$WEB_DEST"' not found - multiACE Web is not installed on this printer. Run a -Full push first.' >&2 + exit 1 +fi +mkdir -p "`$WEB_DEST/backend" "`$WEB_DEST/frontend" "`$WEB_DEST/i18n" +cp -a '$RemoteDir/multiace/web/backend/.' "`$WEB_DEST/backend/" +cp -a '$RemoteDir/multiace/web/frontend/.' "`$WEB_DEST/frontend/" +cp -a '$RemoteDir/multiace/i18n/.' "`$WEB_DEST/i18n/" +rm -rf "`$WEB_DEST/backend/__pycache__" 2>/dev/null || true +chown -R lava:lava "`$WEB_DEST" 2>/dev/null || true +mkdir -p /home/lava/printer_data/config/tools +cp -f '$RemoteDir/multiace/tools/'*.py /home/lava/printer_data/config/tools/ +echo 'STATUS: synced backend, frontend, i18n and tools' +"@ + Invoke-Remote $sync | Out-Null + Restart-Web + Confirm-Recovery + } + + Say "done" +} finally { + Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue +} diff --git a/scripts/run-dev-ui.ps1 b/scripts/run-dev-ui.ps1 new file mode 100644 index 00000000..255b2d3c --- /dev/null +++ b/scripts/run-dev-ui.ps1 @@ -0,0 +1,76 @@ +# run-dev-ui.ps1 - start the multiACE web UI locally, against a real +# Moonraker if one is reachable and against tests\fixtures\*.json if not. +# +# .\scripts\run-dev-ui.ps1 +# $env:MOONRAKER_URL = "http://printer:7125"; .\scripts\run-dev-ui.ps1 +# $env:MULTIACE_MOCK_MODE = "1"; .\scripts\run-dev-ui.ps1 # force mock +# $env:MULTIACE_MOCK_STATE_FILE = "mock_state_head.json"; .\scripts\run-dev-ui.ps1 +# # dashboard in head mode instead of the default multi-ACE fixture +# +# Mock mode also unlocks POST /api/debug/simulate (inject a fake error +# notification) and GET /api/debug/sample-gcode (a small 4-filament +# sample print, tests\fixtures\sample_4color.gcode). + +$ErrorActionPreference = "Stop" + +$Port = if ($env:PORT) { $env:PORT } else { "7126" } +$MoonrakerUrl = if ($env:MOONRAKER_URL) { $env:MOONRAKER_URL } else { "http://127.0.0.1:7125" } +$RepoRoot = Split-Path -Parent $PSScriptRoot +$BackendDir = Join-Path $RepoRoot "multiace\web\backend" + +if ($env:MULTIACE_MOCK_MODE) { + Write-Host "-> mock mode forced by MULTIACE_MOCK_MODE=$($env:MULTIACE_MOCK_MODE)" +} else { + try { + Invoke-WebRequest -Uri "$MoonrakerUrl/printer/info" -TimeoutSec 2 -UseBasicParsing | Out-Null + Write-Host "OK Moonraker found at $MoonrakerUrl - using real data" + $env:MULTIACE_MOCK_MODE = "0" + } catch { + Write-Host "-- Moonraker not reachable at $MoonrakerUrl - using mock data" + $env:MULTIACE_MOCK_MODE = "1" + } +} + +$env:MOONRAKER_URL = $MoonrakerUrl +if (-not $env:MULTIACE_MOCK_DIR) { + $env:MULTIACE_MOCK_DIR = Join-Path $RepoRoot "tests\fixtures" +} +# The installer copies the catalogs next to the backend (web\i18n); in a +# checkout they live at multiace\i18n. Without this every label in the UI +# renders as its raw key. +if (-not $env:MULTIACE_I18N_DIR) { + $env:MULTIACE_I18N_DIR = Join-Path $RepoRoot "multiace\i18n" +} +# A dev box has no /home/lava; point the config editor at a scratch copy +# so saving from the UI cannot 404 (and cannot touch a real printer). +if (-not $env:MULTIACE_CFG_PATH) { + $devCfg = Join-Path $RepoRoot ".devdata\ace.cfg" + $devDir = Split-Path -Parent $devCfg + if (-not (Test-Path $devDir)) { New-Item -ItemType Directory -Path $devDir | Out-Null } + if (-not (Test-Path $devCfg)) { + Copy-Item (Join-Path $RepoRoot "multiace\config\extended\ace.cfg") $devCfg + } + $env:MULTIACE_CFG_PATH = $devCfg +} +Write-Host " config: $($env:MULTIACE_CFG_PATH)" + +$Python = if ($env:PYTHON) { $env:PYTHON } else { "python" } +$Url = "http://127.0.0.1:$Port/" + +Start-Job -ScriptBlock { + param($u) + Start-Sleep -Seconds 2 + Start-Process $u +} -ArgumentList $Url | Out-Null + +if ($env:MULTIACE_MOCK_MODE -eq "1") { + Write-Host " mock: sample print at http://127.0.0.1:$Port/api/debug/sample-gcode" +} + +Write-Host "Dev server on $Url (Ctrl+C to stop)" +Push-Location $BackendDir +try { + & $Python -m uvicorn main:app --host 127.0.0.1 --port $Port --reload +} finally { + Pop-Location +} From 6b18da91240f785ad3b7286016669e1330e862ad Mon Sep 17 00:00:00 2001 From: stefanodangelo <61756862+stefanodangelo@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:54:36 +0200 Subject: [PATCH 2/3] chore: drop push-to-printer.ps1 from the dev scripts It is Windows only and copies to the installer's target paths by hand, so it stays out of the repository. --- scripts/push-to-printer.ps1 | 275 ------------------------------------ 1 file changed, 275 deletions(-) delete mode 100644 scripts/push-to-printer.ps1 diff --git a/scripts/push-to-printer.ps1 b/scripts/push-to-printer.ps1 deleted file mode 100644 index 5d914056..00000000 --- a/scripts/push-to-printer.ps1 +++ /dev/null @@ -1,275 +0,0 @@ -<# -.SYNOPSIS -Push the working tree to a printer without cutting a release. - -.DESCRIPTION -Two push classes; -WebOnly is the default. - - -WebOnly (default) Syncs multiace/web, multiace/i18n and multiace/tools, - restarts the web service, never touches Klipper. - The inner development loop: the worst case is a - broken web page. - - -Full Syncs the whole multiace tree and runs - install_multiace.sh --install-web, then restarts - Klipper. It replaces Klipper extras, so it is gated on - the unit tests passing and the printer being idle. - - -Rollback Restores the installer's own backup of - kinematics/extruder.py and restarts Klipper. - - -DryRun Stages and packs the tree, then prints what would - happen. Nothing is copied or run on the printer. - --PrinterHost is required. SSH must already work with a key (no password -prompt): every ssh call runs with BatchMode=yes. - -.EXAMPLE - .\push-to-printer.ps1 -PrinterHost 192.168.1.50 - .\push-to-printer.ps1 -PrinterHost 192.168.1.50 -DryRun - .\push-to-printer.ps1 -PrinterHost 192.168.1.50 -Full - .\push-to-printer.ps1 -PrinterHost 192.168.1.50 -Rollback -#> -[CmdletBinding()] -param( - [Parameter(Mandatory)][string]$PrinterHost, - [string]$PrinterUser = "root", - [switch]$WebOnly, - [switch]$Full, - [switch]$Rollback, - [switch]$DryRun -) - -$ErrorActionPreference = "Stop" - -if ($WebOnly -and $Full) { throw "-WebOnly and -Full are mutually exclusive." } - -$RepoRoot = Split-Path -Parent $PSScriptRoot -$RemoteTar = "/tmp/multiace-dev.tar.gz" -$RemoteDir = "/tmp/multiace-dev" -$Mode = if ($Full) { "full" } else { "web-only" } - -function Say ($m) { Write-Host "STATUS: $m" } -function Warn ($m) { Write-Warning $m } -function Die ($m) { Write-Error $m; exit 1 } - -# With $ErrorActionPreference = "Stop", any stderr line from a native .exe -# becomes a terminating error regardless of its exit code. Run native -# commands with "Continue" and judge success from $LASTEXITCODE instead. -function Invoke-NativeChecked { - param([Parameter(Mandatory)][scriptblock]$Script) - $prevEAP = $ErrorActionPreference - $ErrorActionPreference = "Continue" - try { & $Script } finally { $ErrorActionPreference = $prevEAP } -} - -function Invoke-Remote ($Command) { - # ssh ships the string to the printer's shell byte for byte, and a stray - # CR from a CRLF here-string breaks lines like "set -e`r". Strip it. - $Command = $Command -replace "`r", "" - if ($DryRun) { - Write-Host "DRY-RUN: ssh $PrinterUser@$PrinterHost $Command" - return "" - } - $out = Invoke-NativeChecked { ssh -o BatchMode=yes "$PrinterUser@$PrinterHost" $Command 2>&1 } - if ($LASTEXITCODE -ne 0) { Die "remote command failed ($LASTEXITCODE): $out" } - return ($out -join "`n") -} - -function Get-PrintState { - try { - $r = Invoke-RestMethod -TimeoutSec 5 ` - -Uri "http://$PrinterHost/printer/objects/query?print_stats" - return [string]$r.result.status.print_stats.state - } catch { - return "" - } -} - -# Fails closed: "cannot tell" is not the same answer as "idle". -function Assert-PrinterIdle { - $state = Get-PrintState - if (-not $state) { $state = "unknown" } - if ($state -in @("printing", "paused", "unknown")) { - Die ("printer is '$state'. A full install or rollback restarts Klipper " + - "and would abort the print. Wait for it to finish.") - } - Say "printer state: $state" -} - -function Invoke-LocalTests { - Say "running the unit tests (a tree that fails tests is not pushed)" - if ($DryRun) { Write-Host "DRY-RUN: python -m pytest tests/unit -q"; return } - Push-Location $RepoRoot - try { - Invoke-NativeChecked { python -m pytest tests/unit -q } - if ($LASTEXITCODE -ne 0) { Die "tests failed - not pushing." } - } finally { Pop-Location } -} - -function Restart-Klipper { - Say "restarting Klipper" - # Not firmware_restart: that reloads the config without re-importing - # already-loaded extras modules, so changed .py files would sit unused. - try { Invoke-Remote '/etc/init.d/S60klipper restart' | Out-Null } - catch { Warn "S60klipper restart failed - check the printer" } -} - -function Restart-Web { - Say "restarting the multiACE web service" - Invoke-Remote '/etc/init.d/S98multiace-web restart >/dev/null 2>&1 || true' | Out-Null -} - -function Confirm-Recovery { - if ($DryRun) { return } - Say "waiting for the printer to come back" - $up = $false - foreach ($i in 1..60) { - try { - $h = Invoke-RestMethod -TimeoutSec 3 -Uri "http://$PrinterHost/multiace/api/health" - if ($h.status -eq "ok") { $up = $true; Say "multiACE web is up"; break } - } catch { } - Start-Sleep -Seconds 2 - } - if (-not $up) { - Warn ("multiACE web did not answer within 2 minutes. If this push broke " + - "something: .\push-to-printer.ps1 -PrinterHost $PrinterHost -Rollback") - } -} - -function Invoke-Rollback { - Say "restoring the installer's backup of extruder.py" - $script = @' -set -e -K=/home/lava/klipper/klippy/kinematics -if [ ! -f "$K/extruder_pre_multiace.py" ]; then - echo "ERROR: no extruder_pre_multiace.py backup found - nothing to roll back" >&2 - exit 1 -fi -cp "$K/extruder_pre_multiace.py" "$K/extruder.py" -echo "STATUS: restored extruder.py" -'@ - Invoke-Remote $script | Out-Null - Restart-Klipper - Say "rollback done" -} - -# --- main ------------------------------------------------------------------ - -if ($Rollback) { - Say "target: $PrinterUser@$PrinterHost class: rollback" - Assert-PrinterIdle - Invoke-Rollback - Confirm-Recovery - exit 0 -} - -Say "target: $PrinterUser@$PrinterHost class: $Mode" -if ($Mode -eq "full") { - Say "FULL push: replaces Klipper extras and restarts Klipper." - Invoke-LocalTests - Assert-PrinterIdle -} else { - Say "WEB-ONLY push: web, i18n and tools only. Klipper is not touched." - # A panel that blinks mid-print is bad UX, not damage: warn, do not refuse. - $state = Get-PrintState - if ($state -in @("printing", "paused")) { - Warn "printer is $state - the panel will blink, nothing mechanical is touched" - } -} - -$Stage = Join-Path ([System.IO.Path]::GetTempPath()) ("multiace-push-" + [guid]::NewGuid().ToString("N")) -New-Item -ItemType Directory -Path $Stage -Force | Out-Null -try { - Say "staging the working tree" - $sources = if ($Mode -eq "full") { @("multiace") } else { @("multiace/web", "multiace/tools", "multiace/i18n") } - foreach ($rel in $sources) { - $src = Join-Path $RepoRoot ($rel -replace "/", "\") - $dst = Join-Path $Stage ($rel -replace "/", "\") - New-Item -ItemType Directory -Path (Split-Path -Parent $dst) -Force | Out-Null - Copy-Item -Recurse -Force $src $dst - } - Get-ChildItem -Path $Stage -Recurse -Force -Directory ` - | Where-Object { $_.Name -eq "__pycache__" } | Remove-Item -Recurse -Force - Get-ChildItem -Path $Stage -Recurse -Force -Filter "*.pyc" | Remove-Item -Force - - # busybox ash fails a `#!/bin/bash\r` shebang with a bare "required file - # not found", so strip CRLF from everything the printer executes or - # parses. Write without a BOM, which breaks a shebang the same way. - $noBom = New-Object System.Text.UTF8Encoding $false - Get-ChildItem -Path $Stage -Recurse -Force -File ` - -Include '*.sh','*.cfg','*.py','*.json','*.sudoers','post-commit','S[0-9][0-9]*' ` - | ForEach-Object { - $text = [System.IO.File]::ReadAllText($_.FullName) - if ($text.Contains("`r`n")) { - [System.IO.File]::WriteAllText($_.FullName, ($text -replace "`r`n", "`n"), $noBom) - } - } - - Say "packing" - $tarball = Join-Path $Stage "multiace-dev.tar.gz" - # Git for Windows ships a GNU tar that reads "C:\..." as host:path, so - # prefer the Windows bsdtar when it exists. - $tarExe = Join-Path $env:SystemRoot "System32\tar.exe" - if (-not (Test-Path $tarExe)) { $tarExe = "tar" } - Invoke-NativeChecked { & $tarExe -C $Stage -czf $tarball multiace } - if ($LASTEXITCODE -ne 0) { Die "tar failed - is bsdtar/tar on PATH?" } - - if ($DryRun) { - $mb = [math]::Round((Get-Item $tarball).Length / 1MB, 2) - Say "dry run: would copy $mb MB to $PrinterHost ($Mode)" - exit 0 - } - - Say "copying to ${PrinterHost}:$RemoteTar" - # Not scp: its SFTP and legacy modes crashed sshd on the printer. Stream - # the file over a plain ssh exec channel as base64 instead. The base64 - # goes through a file and cmd's `<` redirection, because piping one very - # long string through PowerShell 5.1 into a native process truncates it. - $b64 = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($tarball)) - $b64File = Join-Path $Stage "payload.b64" - [System.IO.File]::WriteAllText($b64File, $b64, (New-Object System.Text.ASCIIEncoding)) - Invoke-NativeChecked { - cmd /c "ssh -T -o BatchMode=yes $PrinterUser@$PrinterHost `"base64 -d > $RemoteTar`" < `"$b64File`"" - } - if ($LASTEXITCODE -ne 0) { Die "streaming copy to the printer failed" } - - Say "extracting" - Invoke-Remote "rm -rf '$RemoteDir' && mkdir -p '$RemoteDir' && tar -C '$RemoteDir' -xzf '$RemoteTar'" | Out-Null - - if ($Mode -eq "full") { - Say "running install_multiace.sh --install-web" - Invoke-Remote "cd '$RemoteDir/multiace' && chmod +x install_multiace.sh && ./install_multiace.sh --install-web" - Restart-Klipper - Confirm-Recovery - } else { - # Same destinations and layout as install_multiace.sh uses for these - # directories (multiace/i18n lands in multiace_web/i18n, where the - # backend looks for it). - Say "syncing the web files in place (no installer, no Klipper restart)" - $sync = @" -set -e -WEB_DEST=/home/lava/multiace_web -if [ ! -d "`$WEB_DEST/backend" ]; then - echo 'ERROR: '"`$WEB_DEST"' not found - multiACE Web is not installed on this printer. Run a -Full push first.' >&2 - exit 1 -fi -mkdir -p "`$WEB_DEST/backend" "`$WEB_DEST/frontend" "`$WEB_DEST/i18n" -cp -a '$RemoteDir/multiace/web/backend/.' "`$WEB_DEST/backend/" -cp -a '$RemoteDir/multiace/web/frontend/.' "`$WEB_DEST/frontend/" -cp -a '$RemoteDir/multiace/i18n/.' "`$WEB_DEST/i18n/" -rm -rf "`$WEB_DEST/backend/__pycache__" 2>/dev/null || true -chown -R lava:lava "`$WEB_DEST" 2>/dev/null || true -mkdir -p /home/lava/printer_data/config/tools -cp -f '$RemoteDir/multiace/tools/'*.py /home/lava/printer_data/config/tools/ -echo 'STATUS: synced backend, frontend, i18n and tools' -"@ - Invoke-Remote $sync | Out-Null - Restart-Web - Confirm-Recovery - } - - Say "done" -} finally { - Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue -} From ec60c573f5286de21d0cadd5f9bf262dcb75611c Mon Sep 17 00:00:00 2001 From: stefanodangelo <61756862+stefanodangelo@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:54:36 +0200 Subject: [PATCH 3/3] chore: add run-dev-ui.sh and keep .devdata/ ignored Same environment variables as run-dev-ui.ps1, for Linux and macOS. run-dev-ui creates .devdata/ in every checkout, so it stays in .gitignore. --- .gitignore | 2 ++ scripts/run-dev-ui.sh | 84 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100755 scripts/run-dev-ui.sh diff --git a/.gitignore b/.gitignore index 15f80c69..c93bc460 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ __pycache__/ *.log *.log.[0-9] /dist/ +# Scratch config created by scripts/run-dev-ui +.devdata/ diff --git a/scripts/run-dev-ui.sh b/scripts/run-dev-ui.sh new file mode 100755 index 00000000..23760737 --- /dev/null +++ b/scripts/run-dev-ui.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# run-dev-ui.sh - start the multiACE web UI locally, against a real +# Moonraker if one is reachable and against tests/fixtures/*.json if not. +# +# ./scripts/run-dev-ui.sh # auto-detect +# MOONRAKER_URL=http://printer:7125 ./scripts/run-dev-ui.sh +# MULTIACE_MOCK_MODE=1 ./scripts/run-dev-ui.sh # force mock +# MULTIACE_MOCK_STATE_FILE=mock_state_head.json ./scripts/run-dev-ui.sh +# # dashboard in head mode instead of the default multi-ACE fixture +# +# Mock mode also unlocks POST /api/debug/simulate (inject a fake error +# notification) and GET /api/debug/sample-gcode (a small 4-filament +# sample print, tests/fixtures/sample_4color.gcode). +set -e + +PORT="${PORT:-7126}" +MOONRAKER_URL="${MOONRAKER_URL:-http://127.0.0.1:7125}" +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BACKEND_DIR="$REPO_ROOT/multiace/web/backend" + +# Git Bash / MSYS hands POSIX paths ("/c/Users/…") to a NATIVE Windows +# Python, which reads them as drive-relative and silently finds nothing - +# every label in the UI then renders as its raw i18n key. Convert every +# path we export. No-op on Linux/macOS, where cygpath does not exist. +winpath() { + if command -v cygpath >/dev/null 2>&1; then cygpath -w "$1"; else printf '%s' "$1"; fi +} + +if [ -n "$MULTIACE_MOCK_MODE" ]; then + echo "-> mock mode forced by MULTIACE_MOCK_MODE=$MULTIACE_MOCK_MODE" +elif curl -fsS --max-time 2 "$MOONRAKER_URL/printer/info" >/dev/null 2>&1; then + echo "OK Moonraker found at $MOONRAKER_URL - using real data" + MULTIACE_MOCK_MODE=0 +else + echo "-- Moonraker not reachable at $MOONRAKER_URL - using mock data" + MULTIACE_MOCK_MODE=1 +fi + +export MULTIACE_MOCK_MODE +export MOONRAKER_URL +export MULTIACE_MOCK_DIR="${MULTIACE_MOCK_DIR:-$(winpath "$REPO_ROOT/tests/fixtures")}" +# The installer copies the catalogs next to the backend (web/i18n); in a +# checkout they live at multiace/i18n. Without this every label in the UI +# renders as its raw key. +export MULTIACE_I18N_DIR="${MULTIACE_I18N_DIR:-$(winpath "$REPO_ROOT/multiace/i18n")}" +# A dev box has no /home/lava; point the config editor at a scratch copy +# so saving from the UI cannot 404 (and cannot touch a real printer). +if [ -z "$MULTIACE_CFG_PATH" ]; then + MULTIACE_CFG_PATH="$REPO_ROOT/.devdata/ace.cfg" + mkdir -p "$(dirname "$MULTIACE_CFG_PATH")" + [ -f "$MULTIACE_CFG_PATH" ] || cp "$REPO_ROOT/multiace/config/extended/ace.cfg" "$MULTIACE_CFG_PATH" + MULTIACE_CFG_PATH="$(winpath "$MULTIACE_CFG_PATH")" + export MULTIACE_CFG_PATH +fi +echo " config: $MULTIACE_CFG_PATH" + +# Pick an interpreter that actually RUNS: a python3 that merely exists on +# PATH is not enough (a broken Windows Store / stale install shim is on +# most dev boxes, and it fails only once uvicorn is already "started"). +PY="" +for cand in "$PYTHON" python3 python py; do + [ -n "$cand" ] || continue + if "$cand" -c 'import sys' >/dev/null 2>&1; then PY="$cand"; break; fi +done +if [ -z "$PY" ]; then + echo "No working Python found - set PYTHON=/path/to/python" >&2 + exit 1 +fi + +URL="http://127.0.0.1:$PORT/" +( + sleep 2 + if command -v xdg-open >/dev/null 2>&1; then xdg-open "$URL" + elif command -v open >/dev/null 2>&1; then open "$URL" + fi +) & + +if [ "$MULTIACE_MOCK_MODE" = "1" ]; then + echo " mock: sample print at http://127.0.0.1:$PORT/api/debug/sample-gcode" +fi + +echo "Dev server on $URL (Ctrl+C to stop)" +cd "$BACKEND_DIR" +exec "$PY" -m uvicorn main:app --host 127.0.0.1 --port "$PORT" --reload