diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml deleted file mode 100644 index 5fee6fc..0000000 --- a/.gitea/workflows/deploy.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Build and Deploy - -on: - push: - tags: - - 'v*' - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Deploy to /opt/wled-controller - run: | - DEPLOY_DIR=/opt/wled-controller - - # Ensure deploy directory exists - mkdir -p "$DEPLOY_DIR/data" "$DEPLOY_DIR/logs" "$DEPLOY_DIR/config" - - # Copy server files to deploy directory - rsync -a --delete \ - --exclude 'data/' \ - --exclude 'logs/' \ - server/ "$DEPLOY_DIR/" - - # Build and restart - cd "$DEPLOY_DIR" - docker compose down - docker compose up -d --build diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..11ab8a0 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,74 @@ +name: Build Release + +on: + push: + tags: + - 'v*' + +jobs: + build-windows: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Build portable distribution + shell: pwsh + run: | + .\build-dist.ps1 -Version "${{ gitea.ref_name }}" + + - name: Upload build artifact + uses: actions/upload-artifact@v3 + with: + name: LedGrab-${{ gitea.ref_name }}-win-x64 + path: build/LedGrab-*.zip + retention-days: 90 + + - name: Create Gitea release + shell: pwsh + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + $tag = "${{ gitea.ref_name }}" + $zipFile = Get-ChildItem "build\LedGrab-*.zip" | Select-Object -First 1 + if (-not $zipFile) { throw "ZIP not found" } + + $baseUrl = "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}" + $headers = @{ + "Authorization" = "token $env:GITEA_TOKEN" + "Content-Type" = "application/json" + } + + # Create release + $body = @{ + tag_name = $tag + name = "LedGrab $tag" + body = "Portable Windows build — unzip, run ``LedGrab.bat``, open http://localhost:8080" + draft = $false + prerelease = ($tag -match '(alpha|beta|rc)') + } | ConvertTo-Json + + $release = Invoke-RestMethod -Method Post ` + -Uri "$baseUrl/releases" ` + -Headers $headers -Body $body + + Write-Host "Created release: $($release.html_url)" + + # Upload ZIP asset + $uploadHeaders = @{ + "Authorization" = "token $env:GITEA_TOKEN" + } + $uploadUrl = "$baseUrl/releases/$($release.id)/assets?name=$($zipFile.Name)" + Invoke-RestMethod -Method Post -Uri $uploadUrl ` + -Headers $uploadHeaders ` + -ContentType "application/octet-stream" ` + -InFile $zipFile.FullName + + Write-Host "Uploaded: $($zipFile.Name)" diff --git a/build-dist.ps1 b/build-dist.ps1 new file mode 100644 index 0000000..65c40d6 --- /dev/null +++ b/build-dist.ps1 @@ -0,0 +1,250 @@ +<# +.SYNOPSIS + Build a portable Windows distribution of LedGrab. + +.DESCRIPTION + Downloads embedded Python, installs all dependencies, copies app code, + builds the frontend bundle, and produces a self-contained ZIP. + +.PARAMETER Version + Version string (e.g. "0.1.0" or "v0.1.0"). Auto-detected from git tag + or __init__.py if omitted. + +.PARAMETER PythonVersion + Embedded Python version to download. Default: 3.11.9 + +.PARAMETER SkipFrontend + Skip npm ci + npm run build (use if frontend is already built). + +.PARAMETER SkipPerf + Skip installing optional [perf] extras (dxcam, bettercam, windows-capture). + +.EXAMPLE + .\build-dist.ps1 + .\build-dist.ps1 -Version "0.2.0" + .\build-dist.ps1 -SkipFrontend -SkipPerf +#> +param( + [string]$Version = "", + [string]$PythonVersion = "3.11.9", + [switch]$SkipFrontend, + [switch]$SkipPerf +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' # faster downloads + +$ScriptRoot = $PSScriptRoot +$BuildDir = Join-Path $ScriptRoot "build" +$DistName = "LedGrab" +$DistDir = Join-Path $BuildDir $DistName +$ServerDir = Join-Path $ScriptRoot "server" +$PythonDir = Join-Path $DistDir "python" +$AppDir = Join-Path $DistDir "app" + +# ── Version detection ────────────────────────────────────────── + +if (-not $Version) { + # Try git tag + try { + $gitTag = git describe --tags --exact-match 2>$null + if ($gitTag) { $Version = $gitTag } + } catch {} +} +if (-not $Version) { + # Try env var (CI) + if ($env:GITEA_REF_NAME) { $Version = $env:GITEA_REF_NAME } + elseif ($env:GITHUB_REF_NAME) { $Version = $env:GITHUB_REF_NAME } +} +if (-not $Version) { + # Parse from __init__.py + $initFile = Join-Path $ServerDir "src\wled_controller\__init__.py" + $match = Select-String -Path $initFile -Pattern '__version__\s*=\s*"([^"]+)"' + if ($match) { $Version = $match.Matches[0].Groups[1].Value } +} +if (-not $Version) { $Version = "0.0.0" } + +# Strip leading 'v' for filenames +$VersionClean = $Version -replace '^v', '' +$ZipName = "LedGrab-v${VersionClean}-win-x64.zip" + +Write-Host "=== Building LedGrab v${VersionClean} ===" -ForegroundColor Cyan +Write-Host " Python: $PythonVersion" +Write-Host " Output: build\$ZipName" +Write-Host "" + +# ── Clean ────────────────────────────────────────────────────── + +if (Test-Path $DistDir) { + Write-Host "[1/8] Cleaning previous build..." + Remove-Item -Recurse -Force $DistDir +} +New-Item -ItemType Directory -Path $DistDir -Force | Out-Null + +# ── Download embedded Python ─────────────────────────────────── + +$PythonZipUrl = "https://www.python.org/ftp/python/${PythonVersion}/python-${PythonVersion}-embed-amd64.zip" +$PythonZipPath = Join-Path $BuildDir "python-embed.zip" + +Write-Host "[2/8] Downloading embedded Python ${PythonVersion}..." +if (-not (Test-Path $PythonZipPath)) { + Invoke-WebRequest -Uri $PythonZipUrl -OutFile $PythonZipPath +} +Write-Host " Extracting to python/..." +Expand-Archive -Path $PythonZipPath -DestinationPath $PythonDir -Force + +# ── Patch ._pth to enable site-packages ──────────────────────── + +Write-Host "[3/8] Patching Python path configuration..." +$pthFile = Get-ChildItem -Path $PythonDir -Filter "python*._pth" | Select-Object -First 1 +if (-not $pthFile) { throw "Could not find python*._pth in $PythonDir" } + +$pthContent = Get-Content $pthFile.FullName -Raw +# Uncomment 'import site' +$pthContent = $pthContent -replace '#\s*import site', 'import site' +# Add Lib\site-packages if not present +if ($pthContent -notmatch 'Lib\\site-packages') { + $pthContent = $pthContent.TrimEnd() + "`nLib\site-packages`n" +} +Set-Content -Path $pthFile.FullName -Value $pthContent -NoNewline +Write-Host " Patched $($pthFile.Name)" + +# ── Install pip ──────────────────────────────────────────────── + +Write-Host "[4/8] Installing pip..." +$GetPipPath = Join-Path $BuildDir "get-pip.py" +if (-not (Test-Path $GetPipPath)) { + Invoke-WebRequest -Uri "https://bootstrap.pypa.io/get-pip.py" -OutFile $GetPipPath +} +$python = Join-Path $PythonDir "python.exe" +$ErrorActionPreference = 'Continue' +& $python $GetPipPath --no-warn-script-location 2>&1 | Out-Null +$ErrorActionPreference = 'Stop' +if ($LASTEXITCODE -ne 0) { throw "Failed to install pip" } + +# ── Install dependencies ────────────────────────────────────── + +Write-Host "[5/8] Installing dependencies..." +$extras = "camera,notifications" +if (-not $SkipPerf) { $extras += ",perf" } + +# Install the project (pulls all deps via pyproject.toml), then remove +# the installed package itself — PYTHONPATH handles app code loading. +$ErrorActionPreference = 'Continue' +& $python -m pip install --no-warn-script-location "${ServerDir}[${extras}]" 2>&1 | ForEach-Object { + if ($_ -match 'ERROR|Failed') { Write-Host " $_" -ForegroundColor Red } +} +$ErrorActionPreference = 'Stop' +if ($LASTEXITCODE -ne 0) { + Write-Host " Some optional deps may have failed (continuing)..." -ForegroundColor Yellow +} + +# Remove the installed wled_controller package to avoid duplication +$sitePackages = Join-Path $PythonDir "Lib\site-packages" +Get-ChildItem -Path $sitePackages -Filter "wled*" -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue +Get-ChildItem -Path $sitePackages -Filter "wled*.dist-info" -Directory | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + +# Clean up caches and test files to reduce size +Write-Host " Cleaning up caches..." +Get-ChildItem -Path $sitePackages -Recurse -Directory -Filter "__pycache__" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue +Get-ChildItem -Path $sitePackages -Recurse -Directory -Filter "tests" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue +Get-ChildItem -Path $sitePackages -Recurse -Directory -Filter "test" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + +# ── Build frontend ───────────────────────────────────────────── + +if (-not $SkipFrontend) { + Write-Host "[6/8] Building frontend bundle..." + Push-Location $ServerDir + try { + $ErrorActionPreference = 'Continue' + & npm ci --loglevel error 2>&1 | Out-Null + & npm run build 2>&1 | ForEach-Object { + $line = "$_" + if ($line -and $line -notmatch 'RemoteException') { Write-Host " $line" } + } + $ErrorActionPreference = 'Stop' + } finally { + Pop-Location + } +} else { + Write-Host "[6/8] Skipping frontend build (--SkipFrontend)" +} + +# ── Copy application files ───────────────────────────────────── + +Write-Host "[7/8] Copying application files..." +New-Item -ItemType Directory -Path $AppDir -Force | Out-Null + +# Copy source code (includes static/dist bundle, templates, locales) +$srcDest = Join-Path $AppDir "src" +Copy-Item -Path (Join-Path $ServerDir "src") -Destination $srcDest -Recurse + +# Copy config +$configDest = Join-Path $AppDir "config" +Copy-Item -Path (Join-Path $ServerDir "config") -Destination $configDest -Recurse + +# Create empty data/ and logs/ directories +New-Item -ItemType Directory -Path (Join-Path $DistDir "data") -Force | Out-Null +New-Item -ItemType Directory -Path (Join-Path $DistDir "logs") -Force | Out-Null + +# Clean up source maps and __pycache__ from app code +Get-ChildItem -Path $srcDest -Recurse -Filter "*.map" | Remove-Item -Force -ErrorAction SilentlyContinue +Get-ChildItem -Path $srcDest -Recurse -Directory -Filter "__pycache__" | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + +# ── Create launcher ──────────────────────────────────────────── + +Write-Host "[8/8] Creating launcher..." + +$launcherContent = @' +@echo off +title LedGrab v%VERSION% +cd /d "%~dp0" + +:: Set paths +set PYTHONPATH=%~dp0app\src +set WLED_CONFIG_PATH=%~dp0app\config\default_config.yaml + +:: Create data directory if missing +if not exist "%~dp0data" mkdir "%~dp0data" +if not exist "%~dp0logs" mkdir "%~dp0logs" + +echo. +echo ============================================= +echo LedGrab v%VERSION% +echo Open http://localhost:8080 in your browser +echo ============================================= +echo. + +:: Start the server (open browser after short delay) +start "" /b cmd /c "timeout /t 2 /nobreak >nul && start http://localhost:8080" +"%~dp0python\python.exe" -m uvicorn wled_controller.main:app --host 0.0.0.0 --port 8080 + +pause +'@ + +$launcherContent = $launcherContent -replace '%VERSION%', $VersionClean +$launcherPath = Join-Path $DistDir "LedGrab.bat" +Set-Content -Path $launcherPath -Value $launcherContent -Encoding ASCII + +# ── Create ZIP ───────────────────────────────────────────────── + +$ZipPath = Join-Path $BuildDir $ZipName +if (Test-Path $ZipPath) { Remove-Item -Force $ZipPath } + +Write-Host "" +Write-Host "Creating $ZipName..." -ForegroundColor Cyan + +# Use 7-Zip if available (faster, handles locked files), else fall back to Compress-Archive +$7z = Get-Command 7z -ErrorAction SilentlyContinue +if ($7z) { + & 7z a -tzip -mx=7 $ZipPath "$DistDir\*" | Select-Object -Last 3 +} else { + Compress-Archive -Path "$DistDir\*" -DestinationPath $ZipPath -CompressionLevel Optimal +} + +$zipSize = (Get-Item $ZipPath).Length / 1MB +Write-Host "" +Write-Host "=== Build complete ===" -ForegroundColor Green +Write-Host " Archive: $ZipPath" +Write-Host " Size: $([math]::Round($zipSize, 1)) MB" +Write-Host "" diff --git a/server/src/wled_controller/static/css/base.css b/server/src/wled_controller/static/css/base.css index 799f48d..d278c24 100644 --- a/server/src/wled_controller/static/css/base.css +++ b/server/src/wled_controller/static/css/base.css @@ -81,6 +81,7 @@ html { background: var(--bg-color); overflow-y: scroll; scroll-behavior: smooth; + scrollbar-gutter: stable; } body {