From 1ec829aa71fcd499b3f89ba44e5d791367be1dfc Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:27:23 +0200 Subject: [PATCH 01/34] feat(installer): enable selectable install directory and branded setup --- electron-builder.json | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/electron-builder.json b/electron-builder.json index 4e325ba..5b5f1eb 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -16,7 +16,9 @@ { "from": "languages", "to": "languages", - "filter": ["**/*"] + "filter": [ + "**/*" + ] } ], "win": { @@ -31,9 +33,9 @@ ] }, "nsis": { - "oneClick": true, + "oneClick": false, "perMachine": false, - "allowToChangeInstallationDirectory": false, + "allowToChangeInstallationDirectory": true, "createDesktopShortcut": true, "createStartMenuShortcut": true, "shortcutName": "SOCDOF Desktop", @@ -43,6 +45,9 @@ "installerIcon": "public/socdof_icon.ico", "uninstallerIcon": "public/socdof_icon.ico", "installerHeaderIcon": "public/socdof_icon.ico", - "uninstallerSidebar": "public/uninstallerSidebar.bmp" + "uninstallerSidebar": "public/uninstallerSidebar.bmp", + "installerSidebar": "public/installerSidebar.bmp", + "installerHeader": "public/installerHeader.bmp", + "displayLanguageSelector": false } } From 758f528aa467f80e13a02f7fb2da2c1846dc3d82 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:27:26 +0200 Subject: [PATCH 02/34] feat(desktop): show branded startup splash immediately --- electron/main.cjs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/electron/main.cjs b/electron/main.cjs index 1fc49ac..e5cee66 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -15,6 +15,42 @@ if (!gotSingleInstanceLock) { } let mainWindow; +let splashWindow = null; + +// Show a lightweight branded window while the main workspace initializes. +function createSplashWindow() { + if (splashWindow && !splashWindow.isDestroyed()) return; + splashWindow = new BrowserWindow({ + width: 390, + height: 300, + frame: false, + transparent: false, + resizable: false, + movable: true, + center: true, + show: false, + skipTaskbar: false, + backgroundColor: '#101116', + title: 'SOCDOF wird gestartet', + icon: process.platform === 'win32' + ? path.join(__dirname, '../public/socdof_icon.ico') + : path.join(__dirname, '../public/socdof_icon.png'), + webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true } + }); + splashWindow.once('ready-to-show', () => { + if (splashWindow && !splashWindow.isDestroyed()) splashWindow.show(); + }); + splashWindow.on('closed', () => { splashWindow = null; }); + splashWindow.loadFile(path.join(__dirname, 'splash.html')); +} + +function closeSplashWindow() { + if (splashWindow && !splashWindow.isDestroyed()) { + const current = splashWindow; + splashWindow = null; + current.close(); + } +} let mobileSyncHttpServer = null; const mobileSyncStore = new Map(); @@ -461,6 +497,7 @@ function createWindow() { // Reveal window once first paint is completed and DOM is ready mainWindow.once('ready-to-show', () => { if (mainWindow) { + closeSplashWindow(); mainWindow.setFullScreen(true); mainWindow.show(); mainWindow.focus(); @@ -1565,6 +1602,7 @@ app.on('second-instance', () => { }); app.whenReady().then(() => { + createSplashWindow(); createWindow(); // Defer background language seed & backup folder readiness so the UI thread renders immediately From 21e56fbe3e7fe26198e8f4be1de405c052c7356e Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:27:39 +0200 Subject: [PATCH 03/34] feat(desktop): add minimal branded startup splash --- electron/splash.html | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 electron/splash.html diff --git a/electron/splash.html b/electron/splash.html new file mode 100644 index 0000000..50794cd --- /dev/null +++ b/electron/splash.html @@ -0,0 +1,33 @@ + + + + + +SOCDOF + + + +
+ +

SOCDOF

+

Der Arbeitsbereich wird vorbereitet …

+
+ +
+ + From 87d2f346ac8ae5d0a9354f684e4276094f77bd23 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:28:16 +0200 Subject: [PATCH 04/34] chore(release): bump SOCDOF to 24.10.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3d7e4f6..2182b4b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "socdof", "private": true, - "version": "24.9.1", + "version": "24.10.0", "description": "Strudel's Organization, Commerce & Documentation Offline Flow - Offline-first Windows ERP Desktop Suite & Web Workspace", "author": "Yuri / Strudel", "publisher": "Yuri / Strudel", From 993c17d777550f8bd2cdea0d39210ca7bc1f21eb Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:28:19 +0200 Subject: [PATCH 05/34] chore(version): document 24.10.0 installer improvements --- src/lib/version.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/lib/version.ts b/src/lib/version.ts index cc5dfc1..dcca98b 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -5,7 +5,7 @@ export interface VersionRelease { highlights: string[]; } -export const APP_VERSION = '24.9.1'; +export const APP_VERSION = '24.10.0'; export const APP_NAME = 'SOCDOF'; export const APP_FULL_NAME = "Strudel's Organization, Commerce & Documentation Offline Flow"; export const APP_AUTHOR = 'Yuri / Strudel'; @@ -13,6 +13,16 @@ export const APP_LOCATION = 'South Tyrol, Italy'; export const APP_COPYRIGHT = '© Strudel'; export const VERSION_HISTORY: VersionRelease[] = [ + { + version: '24.10.0', + date: '2026-10-13', + title: 'Selectable Installer & Immediate Branded Startup Splash', + highlights: [ + 'Selectable Windows installation directory while retaining per-user installation under Local AppData by default', + 'Added a compact branded startup splash that appears while the SOCDOF workspace initializes, then closes when the main window is ready', + 'Applied SOCDOF installer header and sidebar branding to the location-selection setup flow' + ] + }, { version: '24.9.1', date: '2026-10-13', From 58a02cf968244535ff2279c6c471b12f9aeb547c Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:28:24 +0200 Subject: [PATCH 06/34] docs(changelog): document selectable installer and startup splash --- versions/V24.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/versions/V24.md b/versions/V24.md index 9985ce2..74bddc6 100644 --- a/versions/V24.md +++ b/versions/V24.md @@ -4,6 +4,16 @@ All notable changes and technical implementation details for SOCDOF Version 24.x --- +## Version 24.10.0 — Selectable Installer & Immediate Branded Startup Splash +- **Selectable Windows Installation Directory (`electron-builder.json`)**: + - Re-enabled the guided setup flow with a user-selectable installation directory while retaining per-user installation and the Local AppData-based default. + - Applied SOCDOF's custom installer header and sidebar artwork to the setup flow. +- **Branded Startup Feedback (`electron/main.cjs` & `electron/splash.html`)**: + - Added a lightweight frameless splash window that appears while the Electron workspace initializes and closes once the main window is ready. + - Kept the splash lightweight so it does not wait for the full React workspace before showing status feedback. + +--- + ## Version 24.9.1 — Smart Homescreen Download Intelligence & Version-Aware Update Prompts - **Homescreen & Start Menu Download Visibility Intelligence (`src/lib/desktopDownloadStatus.ts` & `src/components/DesktopWindowWorkspace.tsx`)**: - Implemented version-aware evaluation for the Start Menu and Homescreen download banners: If the user has already downloaded or installed the desktop app, the download banner is completely hidden. From 69dc74dcd237f695163a06a851352573f84437ac Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:28:28 +0200 Subject: [PATCH 07/34] docs(release): add SOCDOF v24.10.0 overview --- versions/releases/v24-release.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/versions/releases/v24-release.md b/versions/releases/v24-release.md index 1cd1d7f..bcdfef9 100644 --- a/versions/releases/v24-release.md +++ b/versions/releases/v24-release.md @@ -1,5 +1,11 @@ # SOCDOF v24 Release Overview +## v24.10.0 — Selectable Installer & Immediate Branded Startup Splash + +- Restored a guided Windows setup flow with a selectable installation directory and a Local AppData per-user default. +- Applied SOCDOF's custom installer header and sidebar assets to the setup flow. +- Added a minimal frameless branded splash while the desktop workspace initializes, dismissing it when the main window is ready. + ## v24.6.2 — Polished Settings Overview & Clear Section Navigation - Replaced the low-contrast native compact-window selector with a grouped themed menu that supports Escape and outside-click dismissal. From d45e05d73afc1aac977cbba925ac9ea437fe9624 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:28:30 +0200 Subject: [PATCH 08/34] docs(todo): track Windows installer verification --- todo/todo.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/todo/todo.md b/todo/todo.md index 0e2be47..7f9837f 100644 --- a/todo/todo.md +++ b/todo/todo.md @@ -21,6 +21,8 @@ ## 2. Active Roadmap & Pending Tasks +- [ ] Verify v24.10.0 installer and startup splash on Windows: test custom install path, Local AppData default, upgrade over an existing install, and startup behavior; the NSIS installer still has a native setup phase before a custom app window can launch. + ### 2.5 System Optimization & Continuous Polishing - [ ] Continuous module performance and responsive UX refinements. - [ ] Automated regression coverage for critical local data workflows. From fc86a4ebecb53530a2c34dd72648a0f9605ef828 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:28:33 +0200 Subject: [PATCH 09/34] docs(changelog): note installer and startup feedback improvements --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85208f5..5ab42d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,3 +6,6 @@ Format guidelines: - Keep it concise: what is new, what changed/improved, what was fixed. - Avoid exhaustive UI/visual layout breakdowns. --> + +- **Changed:** Windows setup now allows choosing the installation directory while defaulting to a per-user Local AppData location. +- **Improved:** Added a minimal branded startup splash while the SOCDOF desktop workspace initializes. From 6b4b10c306b27f55bd193014c507f5b7cdb53eec Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:31:32 +0200 Subject: [PATCH 10/34] feat(installer): add standalone Windows bootstrapper project --- .../SOCDOF.Bootstrapper.csproj | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 installer-bootstrapper/SOCDOF.Bootstrapper.csproj diff --git a/installer-bootstrapper/SOCDOF.Bootstrapper.csproj b/installer-bootstrapper/SOCDOF.Bootstrapper.csproj new file mode 100644 index 0000000..3939b8f --- /dev/null +++ b/installer-bootstrapper/SOCDOF.Bootstrapper.csproj @@ -0,0 +1,17 @@ + + + WinExe + net8.0-windows + true + enable + enable + SOCDOF-Installer + SOCDOF.Bootstrapper + ..\public\socdof_icon.ico + true + true + win-x64 + true + true + + From d6d2840a2cc3693c8c375b44be000e23bd08d89f Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:31:35 +0200 Subject: [PATCH 11/34] feat(installer): add immediate dark setup window with download progress --- installer-bootstrapper/Program.cs | 422 ++++++++++++++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 installer-bootstrapper/Program.cs diff --git a/installer-bootstrapper/Program.cs b/installer-bootstrapper/Program.cs new file mode 100644 index 0000000..923c82f --- /dev/null +++ b/installer-bootstrapper/Program.cs @@ -0,0 +1,422 @@ +using System.Diagnostics; +using System.Drawing.Drawing2D; +using System.Net.Http.Headers; +using System.Runtime.InteropServices; +using System.Text.Json; + +namespace SOCDOF.Bootstrapper; + +internal static class Program +{ + [STAThread] + private static void Main() + { + ApplicationConfiguration.Initialize(); + Application.Run(new InstallerForm()); + } +} + +internal sealed class InstallerForm : Form +{ + private const string Repository = "Strudelcode/SOCDOF"; + private static readonly Color Background = Color.FromArgb(30, 31, 35); + private static readonly Color Surface = Color.FromArgb(40, 42, 47); + private static readonly Color Muted = Color.FromArgb(165, 168, 180); + private static readonly Color Accent = Color.FromArgb(111, 126, 255); + + private readonly HttpClient http = new() { Timeout = TimeSpan.FromMinutes(10) }; + private readonly Label statusLabel = new(); + private readonly Label detailLabel = new(); + private readonly Label versionLabel = new(); + private readonly Label pathLabel = new(); + private readonly ProgressBar progress = new(); + private readonly Button installButton = new(); + private readonly Button changePathButton = new(); + private readonly Button closeButton = new(); + private string installDirectory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "SOCDOF"); + private string? installerPath; + private bool downloadReady; + private bool installationRunning; + private bool installed; + + public InstallerForm() + { + Text = "SOCDOF Installer"; + StartPosition = FormStartPosition.CenterScreen; + FormBorderStyle = FormBorderStyle.None; + ClientSize = new Size(620, 430); + MinimumSize = new Size(620, 430); + MaximumSize = new Size(620, 430); + BackColor = Background; + ForeColor = Color.White; + ShowInTaskbar = true; + DoubleBuffered = true; + Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); + Region = Region.FromHrgn(CreateRoundRectRgn(0, 0, Width + 1, Height + 1, 18, 18)); + + BuildInterface(); + Shown += async (_, _) => await PrepareDownloadAsync(); + FormClosed += (_, _) => http.Dispose(); + } + + private void BuildInterface() + { + var titleBar = new Panel { Dock = DockStyle.Top, Height = 48, BackColor = Background }; + titleBar.MouseDown += DragWindow; + Controls.Add(titleBar); + + var titleIcon = new PictureBox + { + Location = new Point(22, 13), + Size = new Size(22, 22), + SizeMode = PictureBoxSizeMode.Zoom, + Image = Icon?.ToBitmap(), + BackColor = Color.Transparent + }; + titleBar.Controls.Add(titleIcon); + + var title = new Label + { + Text = "SOCDOF Setup", + Location = new Point(52, 14), + AutoSize = true, + ForeColor = Color.FromArgb(226, 228, 235), + Font = new Font("Segoe UI", 9.5f, FontStyle.Regular) + }; + title.MouseDown += DragWindow; + titleBar.Controls.Add(title); + + closeButton.Text = "×"; + closeButton.Size = new Size(44, 36); + closeButton.Location = new Point(ClientSize.Width - 52, 6); + closeButton.Anchor = AnchorStyles.Top | AnchorStyles.Right; + closeButton.FlatStyle = FlatStyle.Flat; + closeButton.FlatAppearance.BorderSize = 0; + closeButton.FlatAppearance.MouseOverBackColor = Color.FromArgb(190, 55, 65); + closeButton.BackColor = Background; + closeButton.ForeColor = Color.FromArgb(205, 207, 215); + closeButton.Font = new Font("Segoe UI", 16f); + closeButton.Cursor = Cursors.Hand; + closeButton.Click += (_, _) => Close(); + titleBar.Controls.Add(closeButton); + + var logo = new PictureBox + { + Location = new Point(42, 78), + Size = new Size(58, 58), + SizeMode = PictureBoxSizeMode.Zoom, + Image = Icon?.ToBitmap(), + BackColor = Color.Transparent + }; + Controls.Add(logo); + + var heading = new Label + { + Text = "SOCDOF installieren", + Location = new Point(118, 78), + Size = new Size(440, 36), + Font = new Font("Segoe UI Semibold", 20f, FontStyle.Bold), + ForeColor = Color.White + }; + Controls.Add(heading); + + versionLabel.Text = "Neueste Version wird gesucht …"; + versionLabel.Location = new Point(120, 117); + versionLabel.Size = new Size(440, 22); + versionLabel.Font = new Font("Segoe UI", 9.5f); + versionLabel.ForeColor = Muted; + Controls.Add(versionLabel); + + Controls.Add(new Panel { Location = new Point(36, 157), Size = new Size(548, 1), BackColor = Color.FromArgb(57, 59, 66) }); + + statusLabel.Text = "Installation wird vorbereitet …"; + statusLabel.Location = new Point(40, 181); + statusLabel.Size = new Size(530, 27); + statusLabel.Font = new Font("Segoe UI Semibold", 11f, FontStyle.Bold); + statusLabel.ForeColor = Color.FromArgb(240, 241, 246); + Controls.Add(statusLabel); + + detailLabel.Text = "SOCDOF wird sicher heruntergeladen und vorbereitet."; + detailLabel.Location = new Point(40, 211); + detailLabel.Size = new Size(540, 36); + detailLabel.Font = new Font("Segoe UI", 9f); + detailLabel.ForeColor = Muted; + Controls.Add(detailLabel); + + progress.Location = new Point(40, 257); + progress.Size = new Size(540, 5); + progress.Style = ProgressBarStyle.Marquee; + progress.MarqueeAnimationSpeed = 25; + progress.Visible = true; + progress.BackColor = Surface; + progress.ForeColor = Accent; + progress.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + Controls.Add(progress); + + Controls.Add(new Label + { + Text = "INSTALLATIONSORDNER", + Location = new Point(40, 286), + AutoSize = true, + Font = new Font("Segoe UI Semibold", 8f, FontStyle.Bold), + ForeColor = Muted + }); + + var pathPanel = new Panel + { + Location = new Point(40, 307), + Size = new Size(540, 42), + BackColor = Surface, + Padding = new Padding(12, 0, 0, 0) + }; + Controls.Add(pathPanel); + + pathLabel.Text = installDirectory; + pathLabel.Location = new Point(12, 0); + pathLabel.Size = new Size(405, 42); + pathLabel.TextAlign = ContentAlignment.MiddleLeft; + pathLabel.Font = new Font("Segoe UI", 9f); + pathLabel.ForeColor = Color.FromArgb(224, 226, 234); + pathLabel.AutoEllipsis = true; + pathPanel.Controls.Add(pathLabel); + + changePathButton.Text = "Ändern"; + changePathButton.Location = new Point(431, 6); + changePathButton.Size = new Size(96, 30); + changePathButton.FlatStyle = FlatStyle.Flat; + changePathButton.FlatAppearance.BorderColor = Color.FromArgb(75, 78, 88); + changePathButton.FlatAppearance.MouseOverBackColor = Color.FromArgb(57, 60, 69); + changePathButton.BackColor = Surface; + changePathButton.ForeColor = Color.White; + changePathButton.Font = new Font("Segoe UI Semibold", 9f, FontStyle.Bold); + changePathButton.Cursor = Cursors.Hand; + changePathButton.Click += ChangeInstallDirectory; + pathPanel.Controls.Add(changePathButton); + + installButton.Text = "Download wird vorbereitet …"; + installButton.Location = new Point(40, 366); + installButton.Size = new Size(540, 42); + installButton.FlatStyle = FlatStyle.Flat; + installButton.FlatAppearance.BorderSize = 0; + installButton.BackColor = Accent; + installButton.ForeColor = Color.White; + installButton.Font = new Font("Segoe UI Semibold", 10f, FontStyle.Bold); + installButton.Cursor = Cursors.Hand; + installButton.Enabled = false; + installButton.Click += async (_, _) => + { + if (installed) LaunchApplication(); + else await InstallAsync(); + }; + Controls.Add(installButton); + + Controls.Add(new Label + { + Text = "STRUDELCODE · YURI", + Location = new Point(40, 411), + Size = new Size(540, 14), + TextAlign = ContentAlignment.MiddleCenter, + Font = new Font("Segoe UI", 7f, FontStyle.Bold), + ForeColor = Color.FromArgb(103, 106, 117) + }); + } + + private async Task PrepareDownloadAsync() + { + SetStatus("Neueste Version wird gesucht …", "Verbindung zu GitHub wird hergestellt.", 0, marquee: true); + installButton.Text = "Download wird vorbereitet …"; + installButton.Enabled = false; + + try + { + http.DefaultRequestHeaders.UserAgent.Clear(); + http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SOCDOF-Installer", "1.0")); + http.DefaultRequestHeaders.Accept.Clear(); + http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); + + using var releaseResponse = await http.GetAsync($"https://api.github.com/repos/{Repository}/releases/latest"); + releaseResponse.EnsureSuccessStatusCode(); + await using var releaseStream = await releaseResponse.Content.ReadAsStreamAsync(); + using var releaseJson = await JsonDocument.ParseAsync(releaseStream); + var root = releaseJson.RootElement; + var tag = root.TryGetProperty("tag_name", out var tagElement) ? tagElement.GetString() ?? "latest" : "latest"; + var assets = root.GetProperty("assets"); + string? downloadUrl = null; + long expectedBytes = 0; + + foreach (var asset in assets.EnumerateArray()) + { + var name = asset.GetProperty("name").GetString() ?? ""; + if (name.StartsWith("SOCDOF-Setup-", StringComparison.OrdinalIgnoreCase) && + name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) + { + downloadUrl = asset.GetProperty("browser_download_url").GetString(); + expectedBytes = asset.TryGetProperty("size", out var sizeElement) ? sizeElement.GetInt64() : 0; + break; + } + } + + if (string.IsNullOrWhiteSpace(downloadUrl)) + throw new InvalidOperationException("Die neueste GitHub-Version enthält kein passendes SOCDOF-Setup."); + + versionLabel.Text = $"Version {tag} · Windows 64-Bit"; + installerPath = Path.Combine(Path.GetTempPath(), $"SOCDOF-Setup-{Guid.NewGuid():N}.exe"); + SetStatus("SOCDOF wird heruntergeladen …", "Das Installationspaket wird im Hintergrund geladen.", 0, marquee: false); + progress.Style = ProgressBarStyle.Continuous; + progress.Value = 0; + + using var response = await http.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead); + response.EnsureSuccessStatusCode(); + var totalBytes = response.Content.Headers.ContentLength ?? expectedBytes; + await using var input = await response.Content.ReadAsStreamAsync(); + await using var output = new FileStream(installerPath, FileMode.Create, FileAccess.Write, FileShare.None, 81920, true); + var buffer = new byte[81920]; + long received = 0; + int read; + while ((read = await input.ReadAsync(buffer)) > 0) + { + await output.WriteAsync(buffer.AsMemory(0, read)); + received += read; + if (totalBytes > 0) + { + var percent = (int)Math.Clamp(received * 100L / totalBytes, 0, 100); + progress.Value = percent; + detailLabel.Text = $"Installationspaket wird heruntergeladen … {percent}%"; + } + } + await output.FlushAsync(); + + downloadReady = true; + SetStatus("Bereit zur Installation", "Wählen Sie bei Bedarf einen anderen Ordner und starten Sie die Installation.", 100, marquee: false); + installButton.Text = "Installation starten"; + installButton.Enabled = true; + } + catch (Exception ex) + { + SetStatus("Download nicht möglich", "Prüfen Sie die Internetverbindung und versuchen Sie es erneut.", 0, marquee: false); + detailLabel.Text = ex.Message.Length > 100 ? ex.Message[..100] + "…" : ex.Message; + installButton.Text = "Erneut versuchen"; + installButton.Enabled = true; + downloadReady = false; + } + } + + private void ChangeInstallDirectory(object? sender, EventArgs e) + { + using var picker = new FolderBrowserDialog + { + Description = "Wählen Sie den übergeordneten Ordner für SOCDOF.", + UseDescriptionForTitle = true, + ShowNewFolderButton = true, + SelectedPath = Path.GetDirectoryName(installDirectory) ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + }; + if (picker.ShowDialog(this) == DialogResult.OK) + { + installDirectory = Path.Combine(picker.SelectedPath, "SOCDOF"); + pathLabel.Text = installDirectory; + } + } + + private async Task InstallAsync() + { + if (!downloadReady) + { + await PrepareDownloadAsync(); + return; + } + if (installationRunning || string.IsNullOrWhiteSpace(installerPath) || !File.Exists(installerPath)) return; + + installationRunning = true; + installButton.Enabled = false; + changePathButton.Enabled = false; + SetStatus("SOCDOF wird installiert …", "Die Anwendung wird eingerichtet. Bitte lassen Sie dieses Fenster geöffnet.", 0, marquee: true); + + try + { + Directory.CreateDirectory(installDirectory); + var startInfo = new ProcessStartInfo + { + FileName = installerPath, + Arguments = $"/S /D={installDirectory}", + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = Path.GetDirectoryName(installerPath) ?? Path.GetTempPath() + }; + using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Das Setup konnte nicht gestartet werden."); + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) + throw new InvalidOperationException($"Das Setup wurde mit Fehlercode {process.ExitCode} beendet."); + + installed = true; + SetStatus("Installation abgeschlossen", "SOCDOF wurde erfolgreich installiert.", 100, marquee: false); + installButton.Text = "SOCDOF starten"; + installButton.Enabled = true; + changePathButton.Enabled = true; + } + catch (Exception ex) + { + SetStatus("Installation fehlgeschlagen", "Das Setup konnte nicht abgeschlossen werden.", 0, marquee: false); + detailLabel.Text = ex.Message.Length > 120 ? ex.Message[..120] + "…" : ex.Message; + installButton.Text = "Erneut versuchen"; + installButton.Enabled = true; + changePathButton.Enabled = true; + downloadReady = File.Exists(installerPath); + } + finally + { + installationRunning = false; + } + } + + private void LaunchApplication() + { + var executable = Path.Combine(installDirectory, "SOCDOF.exe"); + if (!File.Exists(executable)) + { + MessageBox.Show(this, $"Die Anwendung wurde nicht gefunden:\n{executable}", "SOCDOF", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + try + { + Process.Start(new ProcessStartInfo(executable) { UseShellExecute = true }); + Close(); + } + catch (Exception ex) + { + MessageBox.Show(this, ex.Message, "SOCDOF konnte nicht gestartet werden", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void SetStatus(string title, string detail, int percent, bool marquee) + { + statusLabel.Text = title; + detailLabel.Text = detail; + if (marquee) + { + progress.Style = ProgressBarStyle.Marquee; + progress.MarqueeAnimationSpeed = 25; + } + else + { + progress.Style = ProgressBarStyle.Continuous; + progress.MarqueeAnimationSpeed = 0; + progress.Value = Math.Clamp(percent, 0, 100); + } + } + + private static void DragWindow(object? sender, MouseEventArgs e) + { + if (e.Button != MouseButtons.Left) return; + var control = (Control?)sender; + var form = control?.FindForm(); + if (form is null) return; + ReleaseCapture(); + SendMessage(form.Handle, 0xA1, 0x2, 0); + } + + [DllImport("user32.dll")] private static extern bool ReleaseCapture(); + [DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam); + [DllImport("gdi32.dll")] private static extern IntPtr CreateRoundRectRgn(int left, int top, int right, int bottom, int width, int height); +} From 1727c26ad487e73a9f8ef04fb6118f26c1882c83 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:31:39 +0200 Subject: [PATCH 12/34] ci: validate desktop build and Windows installer bootstrapper --- .../workflows/installer-bootstrapper-ci.yml | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/installer-bootstrapper-ci.yml diff --git a/.github/workflows/installer-bootstrapper-ci.yml b/.github/workflows/installer-bootstrapper-ci.yml new file mode 100644 index 0000000..07ca0ae --- /dev/null +++ b/.github/workflows/installer-bootstrapper-ci.yml @@ -0,0 +1,41 @@ +name: Validate SOCDOF Desktop and Windows Installer + +on: + pull_request: + branches: [main, master] + push: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: windows-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install Node dependencies + run: npm ci + + - name: TypeScript validation + run: npm run lint + + - name: Build web application + run: npm run build + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Publish standalone Windows installer bootstrapper + run: dotnet publish installer-bootstrapper/SOCDOF.Bootstrapper.csproj --configuration Release --runtime win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true --output artifacts/bootstrapper From 3d1aa496cad9e020bd6bd6d68f02583d0e0c2c37 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:33:03 +0200 Subject: [PATCH 13/34] build(release): publish instant-launch installer bootstrapper --- .github/workflows/build-windows-exe.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/build-windows-exe.yml b/.github/workflows/build-windows-exe.yml index cd27c45..9e29e9f 100644 --- a/.github/workflows/build-windows-exe.yml +++ b/.github/workflows/build-windows-exe.yml @@ -63,6 +63,16 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Build lightweight instant-launch bootstrapper + shell: powershell + run: | + $msbuild = Get-ChildItem "C:\Program Files\Microsoft Visual Studio" -Filter MSBuild.exe -Recurse | Where-Object { $_.FullName -match "Current\\Bin\\MSBuild.exe$" } | Select-Object -First 1 + if (-not $msbuild) { throw "MSBuild.exe was not found on the Windows runner." } + & $msbuild.FullName installer-bootstrapper\SOCDOF.Bootstrapper.csproj /p:Configuration=Release /p:Platform=AnyCPU /m + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + Copy-Item release\bootstrapper\SOCDOF-Installer.exe release\SOCDOF-Installer.exe -Force + if (-not (Test-Path release\SOCDOF-Installer.exe)) { throw "SOCDOF-Installer.exe was not produced." } + - name: Upload Artifacts uses: actions/upload-artifact@v4 with: From ca72c832def549d20c9823b5a457def884858bfe Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:33:06 +0200 Subject: [PATCH 14/34] feat(download): offer immediate-launch SOCDOF installer --- src/components/CustomDownloadModal.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/CustomDownloadModal.tsx b/src/components/CustomDownloadModal.tsx index af160e8..3637f3f 100644 --- a/src/components/CustomDownloadModal.tsx +++ b/src/components/CustomDownloadModal.tsx @@ -57,7 +57,7 @@ export const CustomDownloadModal: React.FC = ({ const [downloadProgress, setDownloadProgress] = useState(0); const [latestVersion, setLatestVersion] = useState(APP_VERSION); const [downloadUrl, setDownloadUrl] = useState( - `https://github.com/Strudelcode/SOCDOF/releases/download/v${APP_VERSION}/SOCDOF-Setup-${APP_VERSION}.exe` + `https://github.com/Strudelcode/SOCDOF/releases/download/v${APP_VERSION}/SOCDOF-Installer.exe` ); const [isLocallyDownloaded, setIsLocallyDownloaded] = useState(() => isDesktopInstalledOrDownloaded()); @@ -74,7 +74,9 @@ export const CustomDownloadModal: React.FC = ({ if (!isMounted || !data) return; const tag = data.tag_name ? data.tag_name.replace(/^v/, '') : APP_VERSION; setLatestVersion(tag || APP_VERSION); - const exeAsset = data.assets?.find((a: any) => + const exeAsset = data.assets?.find((a: any) => + typeof a.name === 'string' && a.name.toLowerCase() === 'socdof-installer.exe' + ) ?? data.assets?.find((a: any) => typeof a.name === 'string' && a.name.toLowerCase().endsWith('.exe') ); if (exeAsset?.browser_download_url) { @@ -308,7 +310,7 @@ export const CustomDownloadModal: React.FC = ({ Offizielles SOCDOF Windows Setup

- Dateiname: SOCDOF-Setup-{latestVersion}.exe (~95 MB) + Dateiname: SOCDOF-Installer.exe (öffnet sofort ein Installationsfenster)

@@ -345,7 +347,7 @@ export const CustomDownloadModal: React.FC = ({ {downloadStep === 'downloading' ? ( <> - Download läuft: SOCDOF-Setup-{latestVersion}.exe + Download läuft: SOCDOF-Installer.exe ) : ( <> From 6a4b83e0e133081d2795359ac4f8e9df1b5de818 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:36:06 +0200 Subject: [PATCH 15/34] feat(installer): add lightweight framework-based bootstrapper project --- .../SOCDOF.Bootstrapper.Framework.csproj | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 installer-bootstrapper/SOCDOF.Bootstrapper.Framework.csproj diff --git a/installer-bootstrapper/SOCDOF.Bootstrapper.Framework.csproj b/installer-bootstrapper/SOCDOF.Bootstrapper.Framework.csproj new file mode 100644 index 0000000..3e0abdf --- /dev/null +++ b/installer-bootstrapper/SOCDOF.Bootstrapper.Framework.csproj @@ -0,0 +1,29 @@ + + + + Release + AnyCPU + {A8B33B8E-8F45-43A1-9F56-63A0B8D9140E} + WinExe + SOCDOF.Bootstrapper + SOCDOF-Installer + v4.8 + ..\public\socdof_icon.ico + ..\release\bootstrapper\ + x64 + true + 4 + + + + + + + + + + + + + + From 60081c782beb22d0340af3aabaf86d1e1f3eb9d1 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:36:11 +0200 Subject: [PATCH 16/34] feat(installer): add immediate-start Windows setup UI with download progress --- installer-bootstrapper/Program.Framework.cs | 297 ++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 installer-bootstrapper/Program.Framework.cs diff --git a/installer-bootstrapper/Program.Framework.cs b/installer-bootstrapper/Program.Framework.cs new file mode 100644 index 0000000..744c910 --- /dev/null +++ b/installer-bootstrapper/Program.Framework.cs @@ -0,0 +1,297 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using System.Web.Script.Serialization; +using System.Windows.Forms; + +namespace SOCDOF.Bootstrapper +{ + internal static class FrameworkProgram + { + [STAThread] + private static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new SetupWindow()); + } + } + + internal sealed class SetupWindow : Form + { + private readonly Color bg = Color.FromArgb(30, 31, 35); + private readonly Color panel = Color.FromArgb(40, 42, 47); + private readonly Color muted = Color.FromArgb(165, 168, 180); + private readonly Color accent = Color.FromArgb(111, 126, 255); + private readonly HttpClient http = new HttpClient { Timeout = TimeSpan.FromMinutes(10) }; + private readonly Label status = new Label(); + private readonly Label detail = new Label(); + private readonly Label version = new Label(); + private readonly Label pathText = new Label(); + private readonly ProgressBar bar = new ProgressBar(); + private readonly Button install = new Button(); + private readonly Button browse = new Button(); + private string target = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "SOCDOF"); + private string packagePath; + private bool downloaded; + private bool installing; + private bool installed; + + public SetupWindow() + { + Text = "SOCDOF Installer"; + StartPosition = FormStartPosition.CenterScreen; + FormBorderStyle = FormBorderStyle.None; + ClientSize = new Size(620, 430); + MinimumSize = new Size(620, 430); + MaximumSize = new Size(620, 430); + BackColor = bg; + ForeColor = Color.White; + ShowInTaskbar = true; + Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); + Region = System.Drawing.Region.FromHrgn(CreateRoundRectRgn(0, 0, Width + 1, Height + 1)); + Compose(); + Shown += async delegate { await DownloadLatestAsync(); }; + FormClosed += delegate { http.Dispose(); }; + } + + private void Compose() + { + Panel top = new Panel { Dock = DockStyle.Top, Height = 48, BackColor = bg }; + top.MouseDown += Drag; + Controls.Add(top); + PictureBox smallLogo = new PictureBox { Location = new Point(22, 13), Size = new Size(22, 22), SizeMode = PictureBoxSizeMode.Zoom, Image = Icon == null ? null : Icon.ToBitmap(), BackColor = Color.Transparent }; + top.Controls.Add(smallLogo); + Label title = new Label { Text = "SOCDOF Setup", Location = new Point(52, 14), AutoSize = true, ForeColor = Color.FromArgb(226, 228, 235), Font = new Font("Segoe UI", 9.5f) }; + title.MouseDown += Drag; + top.Controls.Add(title); + Button close = new Button { Text = "×", Location = new Point(568, 6), Size = new Size(44, 36), FlatStyle = FlatStyle.Flat, BackColor = bg, ForeColor = Color.FromArgb(205, 207, 215), Font = new Font("Segoe UI", 16f), Cursor = Cursors.Hand, Anchor = AnchorStyles.Top | AnchorStyles.Right }; + close.FlatAppearance.BorderSize = 0; + close.FlatAppearance.MouseOverBackColor = Color.FromArgb(190, 55, 65); + close.Click += delegate { Close(); }; + top.Controls.Add(close); + + Controls.Add(new PictureBox { Location = new Point(42, 78), Size = new Size(58, 58), SizeMode = PictureBoxSizeMode.Zoom, Image = Icon == null ? null : Icon.ToBitmap(), BackColor = Color.Transparent }); + Controls.Add(new Label { Text = "SOCDOF installieren", Location = new Point(118, 78), Size = new Size(440, 36), Font = new Font("Segoe UI Semibold", 20f, FontStyle.Bold), ForeColor = Color.White }); + version.SetBounds(120, 117, 440, 22); + version.Font = new Font("Segoe UI", 9.5f); + version.ForeColor = muted; + version.Text = "Neueste Version wird gesucht …"; + Controls.Add(version); + Controls.Add(new Panel { Location = new Point(36, 157), Size = new Size(548, 1), BackColor = Color.FromArgb(57, 59, 66) }); + + status.SetBounds(40, 181, 530, 27); + status.Font = new Font("Segoe UI Semibold", 11f, FontStyle.Bold); + status.ForeColor = Color.FromArgb(240, 241, 246); + status.Text = "Installation wird vorbereitet …"; + Controls.Add(status); + detail.SetBounds(40, 211, 540, 36); + detail.Font = new Font("Segoe UI", 9f); + detail.ForeColor = muted; + detail.Text = "SOCDOF wird sicher heruntergeladen und vorbereitet."; + Controls.Add(detail); + bar.SetBounds(40, 257, 540, 5); + bar.Style = ProgressBarStyle.Marquee; + bar.MarqueeAnimationSpeed = 25; + bar.BackColor = panel; + bar.ForeColor = accent; + Controls.Add(bar); + + Controls.Add(new Label { Text = "INSTALLATIONSORDNER", Location = new Point(40, 286), AutoSize = true, Font = new Font("Segoe UI Semibold", 8f, FontStyle.Bold), ForeColor = muted }); + Panel pathPanel = new Panel { Location = new Point(40, 307), Size = new Size(540, 42), BackColor = panel, Padding = new Padding(12, 0, 0, 0) }; + Controls.Add(pathPanel); + pathText.SetBounds(12, 0, 405, 42); + pathText.Text = target; + pathText.TextAlign = ContentAlignment.MiddleLeft; + pathText.Font = new Font("Segoe UI", 9f); + pathText.ForeColor = Color.FromArgb(224, 226, 234); + pathText.AutoEllipsis = true; + pathPanel.Controls.Add(pathText); + browse.Text = "Ändern"; + browse.SetBounds(431, 6, 96, 30); + browse.FlatStyle = FlatStyle.Flat; + browse.FlatAppearance.BorderColor = Color.FromArgb(75, 78, 88); + browse.BackColor = panel; + browse.ForeColor = Color.White; + browse.Font = new Font("Segoe UI Semibold", 9f, FontStyle.Bold); + browse.Cursor = Cursors.Hand; + browse.Click += Browse; + pathPanel.Controls.Add(browse); + + install.Text = "Download wird vorbereitet …"; + install.SetBounds(40, 366, 540, 42); + install.FlatStyle = FlatStyle.Flat; + install.FlatAppearance.BorderSize = 0; + install.BackColor = accent; + install.ForeColor = Color.White; + install.Font = new Font("Segoe UI Semibold", 10f, FontStyle.Bold); + install.Cursor = Cursors.Hand; + install.Enabled = false; + install.Click += async delegate { if (installed) Launch(); else await InstallAsync(); }; + Controls.Add(install); + Controls.Add(new Label { Text = "STRUDELCODE · YURI", Location = new Point(40, 411), Size = new Size(540, 14), TextAlign = ContentAlignment.MiddleCenter, Font = new Font("Segoe UI", 7f, FontStyle.Bold), ForeColor = Color.FromArgb(103, 106, 117) }); + } + + private async Task DownloadLatestAsync() + { + State("Neueste Version wird gesucht …", "Verbindung zu GitHub wird hergestellt.", 0, true); + install.Text = "Download wird vorbereitet …"; + install.Enabled = false; + try + { + http.DefaultRequestHeaders.UserAgent.Clear(); + http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SOCDOF-Installer", "1.0")); + http.DefaultRequestHeaders.Accept.Clear(); + http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); + string json = await http.GetStringAsync("https://api.github.com/repos/Strudelcode/SOCDOF/releases/latest"); + Dictionary release = new JavaScriptSerializer().Deserialize>(json); + version.Text = "Version " + Convert.ToString(release["tag_name"]) + " · Windows 64-Bit"; + string url = null; + long size = 0; + ArrayList assets = release["assets"] as ArrayList; + if (assets != null) + { + foreach (object item in assets) + { + Dictionary asset = item as Dictionary; + if (asset == null) continue; + string name = Convert.ToString(asset["name"]); + if (name.StartsWith("SOCDOF-Setup-", StringComparison.OrdinalIgnoreCase) && name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) + { + url = Convert.ToString(asset["browser_download_url"]); + if (asset.ContainsKey("size")) long.TryParse(Convert.ToString(asset["size"]), out size); + break; + } + } + } + if (String.IsNullOrEmpty(url)) throw new InvalidOperationException("Kein passendes SOCDOF-Setup im neuesten Release gefunden."); + packagePath = Path.Combine(Path.GetTempPath(), "SOCDOF-Setup-" + Guid.NewGuid().ToString("N") + ".exe"); + State("SOCDOF wird heruntergeladen …", "Das Installationspaket wird im Hintergrund geladen.", 0, false); + using (HttpResponseMessage response = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead)) + { + response.EnsureSuccessStatusCode(); + long total = response.Content.Headers.ContentLength.HasValue ? response.Content.Headers.ContentLength.Value : size; + using (Stream input = await response.Content.ReadAsStreamAsync()) + using (FileStream output = new FileStream(packagePath, FileMode.Create, FileAccess.Write, FileShare.None, 81920, true)) + { + byte[] buffer = new byte[81920]; + long got = 0; + int read; + while ((read = await input.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + await output.WriteAsync(buffer, 0, read); + got += read; + if (total > 0) + { + int pct = (int)Math.Max(0, Math.Min(100, got * 100L / total)); + bar.Value = pct; + detail.Text = "Installationspaket wird heruntergeladen … " + pct + "%"; + } + } + await output.FlushAsync(); + } + } + downloaded = true; + State("Bereit zur Installation", "Sie können den Installationsordner noch ändern.", 100, false); + install.Text = "Installation starten"; + install.Enabled = true; + } + catch (Exception ex) + { + State("Download nicht möglich", "Prüfen Sie Ihre Internetverbindung und versuchen Sie es erneut.", 0, false); + detail.Text = ex.Message.Length > 110 ? ex.Message.Substring(0, 110) + "…" : ex.Message; + install.Text = "Erneut versuchen"; + install.Enabled = true; + } + } + + private void Browse(object sender, EventArgs e) + { + using (FolderBrowserDialog dialog = new FolderBrowserDialog()) + { + dialog.Description = "Wählen Sie den übergeordneten Ordner für SOCDOF."; + dialog.ShowNewFolderButton = true; + dialog.SelectedPath = Path.GetDirectoryName(target); + if (dialog.ShowDialog(this) == DialogResult.OK) + { + target = Path.Combine(dialog.SelectedPath, "SOCDOF"); + pathText.Text = target; + } + } + } + + private async Task InstallAsync() + { + if (!downloaded) { await DownloadLatestAsync(); return; } + if (installing || String.IsNullOrEmpty(packagePath) || !File.Exists(packagePath)) return; + installing = true; + install.Enabled = false; + browse.Enabled = false; + State("SOCDOF wird installiert …", "Die Anwendung wird eingerichtet. Bitte lassen Sie dieses Fenster geöffnet.", 0, true); + try + { + Directory.CreateDirectory(target); + ProcessStartInfo info = new ProcessStartInfo { FileName = packagePath, Arguments = "/S /D=" + target, UseShellExecute = false, CreateNoWindow = true, WorkingDirectory = Path.GetTempPath() }; + using (Process process = Process.Start(info)) + { + if (process == null) throw new InvalidOperationException("Das Setup konnte nicht gestartet werden."); + await Task.Run(delegate { process.WaitForExit(); }); + if (process.ExitCode != 0) throw new InvalidOperationException("Das Setup wurde mit Fehlercode " + process.ExitCode + " beendet."); + } + installed = true; + State("Installation abgeschlossen", "SOCDOF wurde erfolgreich installiert.", 100, false); + install.Text = "SOCDOF starten"; + install.Enabled = true; + } + catch (Exception ex) + { + State("Installation fehlgeschlagen", "Das Setup konnte nicht abgeschlossen werden.", 0, false); + detail.Text = ex.Message.Length > 110 ? ex.Message.Substring(0, 110) + "…" : ex.Message; + install.Text = "Erneut versuchen"; + install.Enabled = true; + } + finally { installing = false; browse.Enabled = true; } + } + + private void Launch() + { + string exe = Path.Combine(target, "SOCDOF.exe"); + if (!File.Exists(exe)) + { + MessageBox.Show(this, "Die Anwendung wurde nicht gefunden:\n" + exe, "SOCDOF", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + try { Process.Start(new ProcessStartInfo(exe) { UseShellExecute = true }); Close(); } + catch (Exception ex) { MessageBox.Show(this, ex.Message, "SOCDOF konnte nicht gestartet werden", MessageBoxButtons.OK, MessageBoxIcon.Error); } + } + + private void State(string heading, string description, int percent, bool marquee) + { + status.Text = heading; + detail.Text = description; + if (marquee) { bar.Style = ProgressBarStyle.Marquee; bar.MarqueeAnimationSpeed = 25; } + else { bar.Style = ProgressBarStyle.Continuous; bar.MarqueeAnimationSpeed = 0; bar.Value = Math.Max(0, Math.Min(100, percent)); } + } + + private static void Drag(object sender, MouseEventArgs e) + { + if (e.Button != MouseButtons.Left) return; + Control c = sender as Control; + Form f = c == null ? null : c.FindForm(); + if (f == null) return; + ReleaseCapture(); + SendMessage(f.Handle, 0xA1, 0x2, 0); + } + + [DllImport("user32.dll")] private static extern bool ReleaseCapture(); + [DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam); + [DllImport("gdi32.dll")] private static extern IntPtr CreateRoundRectRgn(int left, int top, int right, int bottom, int width, int height); + } +} From 5c4dcbcc7b419c536ee56a4e6241f5a16e96cdfb Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:36:15 +0200 Subject: [PATCH 17/34] ci: compile lightweight framework bootstrapper on pull requests --- .../installer-bootstrapper-framework-ci.yml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/installer-bootstrapper-framework-ci.yml diff --git a/.github/workflows/installer-bootstrapper-framework-ci.yml b/.github/workflows/installer-bootstrapper-framework-ci.yml new file mode 100644 index 0000000..bc03c6e --- /dev/null +++ b/.github/workflows/installer-bootstrapper-framework-ci.yml @@ -0,0 +1,23 @@ +name: Validate SOCDOF Installer Bootstrapper (.NET Framework) + +on: + pull_request: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Build native-startup Windows bootstrapper + shell: powershell + run: | + $msbuild = Get-ChildItem "C:\Program Files\Microsoft Visual Studio" -Filter MSBuild.exe -Recurse | Where-Object { $_.FullName -match "Current\\Bin\\MSBuild.exe$" } | Select-Object -First 1 + if (-not $msbuild) { throw "MSBuild.exe was not found on the Windows runner." } + & $msbuild.FullName installer-bootstrapper\SOCDOF.Bootstrapper.Framework.csproj /p:Configuration=Release /p:Platform=AnyCPU /m + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if (-not (Test-Path release\bootstrapper\SOCDOF-Installer.exe)) { throw "Bootstrapper executable was not produced." } From 647f9885e3aaf402645679e5eaadd0efa78aae5e Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:36:21 +0200 Subject: [PATCH 18/34] build(release): use instant-start .NET Framework bootstrapper --- .github/workflows/build-windows-exe.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-windows-exe.yml b/.github/workflows/build-windows-exe.yml index 9e29e9f..b6facaf 100644 --- a/.github/workflows/build-windows-exe.yml +++ b/.github/workflows/build-windows-exe.yml @@ -68,7 +68,7 @@ jobs: run: | $msbuild = Get-ChildItem "C:\Program Files\Microsoft Visual Studio" -Filter MSBuild.exe -Recurse | Where-Object { $_.FullName -match "Current\\Bin\\MSBuild.exe$" } | Select-Object -First 1 if (-not $msbuild) { throw "MSBuild.exe was not found on the Windows runner." } - & $msbuild.FullName installer-bootstrapper\SOCDOF.Bootstrapper.csproj /p:Configuration=Release /p:Platform=AnyCPU /m + & $msbuild.FullName installer-bootstrapper\SOCDOF.Bootstrapper.Framework.csproj /p:Configuration=Release /p:Platform=AnyCPU /m if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } Copy-Item release\bootstrapper\SOCDOF-Installer.exe release\SOCDOF-Installer.exe -Force if (-not (Test-Path release\SOCDOF-Installer.exe)) { throw "SOCDOF-Installer.exe was not produced." } From 98de92416895c16f906d569846ad09a678609c8e Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:36:35 +0200 Subject: [PATCH 19/34] docs(installer): document instant-launch bootstrapper --- versions/V24.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/versions/V24.md b/versions/V24.md index 74bddc6..5067d8c 100644 --- a/versions/V24.md +++ b/versions/V24.md @@ -5,6 +5,10 @@ All notable changes and technical implementation details for SOCDOF Version 24.x --- ## Version 24.10.0 — Selectable Installer & Immediate Branded Startup Splash +- **Instant-Launch Windows Bootstrapper (`installer-bootstrapper/`)**: + - Added a lightweight .NET Framework 4.8 setup window that appears before the full Electron installer is downloaded. + - Displays real download progress, supports choosing an install directory, runs the packaged NSIS installer silently, and offers to launch SOCDOF when complete. + - Uses the latest official GitHub Release as the source for the signed/packaged setup executable. - **Selectable Windows Installation Directory (`electron-builder.json`)**: - Re-enabled the guided setup flow with a user-selectable installation directory while retaining per-user installation and the Local AppData-based default. - Applied SOCDOF's custom installer header and sidebar artwork to the setup flow. From 613631f6f65a41d0a7ae4f745126c257f45173be Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:36:39 +0200 Subject: [PATCH 20/34] docs(installer): document instant-launch bootstrapper --- versions/releases/v24-release.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/versions/releases/v24-release.md b/versions/releases/v24-release.md index bcdfef9..dfaf5dd 100644 --- a/versions/releases/v24-release.md +++ b/versions/releases/v24-release.md @@ -2,6 +2,8 @@ ## v24.10.0 — Selectable Installer & Immediate Branded Startup Splash +- Added an instant-launch Windows bootstrapper with real download progress, install-directory selection, and a clean dark setup window. + - Restored a guided Windows setup flow with a selectable installation directory and a Local AppData per-user default. - Applied SOCDOF's custom installer header and sidebar assets to the setup flow. - Added a minimal frameless branded splash while the desktop workspace initializes, dismissing it when the main window is ready. From b33cb5be26091a97a58465780bc40804f879a6c9 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:36:43 +0200 Subject: [PATCH 21/34] docs(installer): document instant-launch bootstrapper --- todo/todo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/todo/todo.md b/todo/todo.md index 7f9837f..219c937 100644 --- a/todo/todo.md +++ b/todo/todo.md @@ -21,7 +21,7 @@ ## 2. Active Roadmap & Pending Tasks -- [ ] Verify v24.10.0 installer and startup splash on Windows: test custom install path, Local AppData default, upgrade over an existing install, and startup behavior; the NSIS installer still has a native setup phase before a custom app window can launch. +- [ ] Verify v24.10.0 instant-launch bootstrapper and startup splash on Windows: test custom install path, Local AppData default, upgrade over an existing install, and startup behavior; the NSIS installer still has a native setup phase before a custom app window can launch. ### 2.5 System Optimization & Continuous Polishing - [ ] Continuous module performance and responsive UX refinements. From 563d10c8805d2732e14b3df15a36c1551f4ff2d3 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:36:46 +0200 Subject: [PATCH 22/34] docs(installer): document instant-launch bootstrapper --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ab42d6..9f600e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,3 +9,5 @@ Format guidelines: - **Changed:** Windows setup now allows choosing the installation directory while defaulting to a per-user Local AppData location. - **Improved:** Added a minimal branded startup splash while the SOCDOF desktop workspace initializes. + +- **Added:** A lightweight Windows bootstrapper opens immediately, downloads the latest setup with real progress, and lets users select the installation directory. From b3feafadbf55a5b812930f8022511c3341c50cf3 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:37:02 +0200 Subject: [PATCH 23/34] docs(version): describe instant-launch Windows bootstrapper --- src/lib/version.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/version.ts b/src/lib/version.ts index dcca98b..e44e609 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -20,7 +20,8 @@ export const VERSION_HISTORY: VersionRelease[] = [ highlights: [ 'Selectable Windows installation directory while retaining per-user installation under Local AppData by default', 'Added a compact branded startup splash that appears while the SOCDOF workspace initializes, then closes when the main window is ready', - 'Applied SOCDOF installer header and sidebar branding to the location-selection setup flow' + 'Applied SOCDOF installer header and sidebar branding to the location-selection setup flow', + 'Added a lightweight Windows bootstrapper that opens immediately, downloads the latest setup with real progress, and supports a custom installation directory' ] }, { From 8dbf1905d3c593a0e18714a7435962121fbc7180 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:37:42 +0200 Subject: [PATCH 24/34] ci: remove redundant bootstrapper workflow without lockfile --- .../workflows/installer-bootstrapper-ci.yml | 41 ------------------- 1 file changed, 41 deletions(-) delete mode 100644 .github/workflows/installer-bootstrapper-ci.yml diff --git a/.github/workflows/installer-bootstrapper-ci.yml b/.github/workflows/installer-bootstrapper-ci.yml deleted file mode 100644 index 07ca0ae..0000000 --- a/.github/workflows/installer-bootstrapper-ci.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Validate SOCDOF Desktop and Windows Installer - -on: - pull_request: - branches: [main, master] - push: - branches: [main, master] - workflow_dispatch: - -permissions: - contents: read - -jobs: - validate: - runs-on: windows-latest - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - - name: Install Node dependencies - run: npm ci - - - name: TypeScript validation - run: npm run lint - - - name: Build web application - run: npm run build - - - name: Set up .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - - name: Publish standalone Windows installer bootstrapper - run: dotnet publish installer-bootstrapper/SOCDOF.Bootstrapper.csproj --configuration Release --runtime win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true --output artifacts/bootstrapper From 3cd1b34edde5aa042735be4d0230e47441f5642b Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:37:48 +0200 Subject: [PATCH 25/34] refactor(installer): keep only lightweight framework bootstrapper --- installer-bootstrapper/Program.cs | 422 ------------------------------ 1 file changed, 422 deletions(-) delete mode 100644 installer-bootstrapper/Program.cs diff --git a/installer-bootstrapper/Program.cs b/installer-bootstrapper/Program.cs deleted file mode 100644 index 923c82f..0000000 --- a/installer-bootstrapper/Program.cs +++ /dev/null @@ -1,422 +0,0 @@ -using System.Diagnostics; -using System.Drawing.Drawing2D; -using System.Net.Http.Headers; -using System.Runtime.InteropServices; -using System.Text.Json; - -namespace SOCDOF.Bootstrapper; - -internal static class Program -{ - [STAThread] - private static void Main() - { - ApplicationConfiguration.Initialize(); - Application.Run(new InstallerForm()); - } -} - -internal sealed class InstallerForm : Form -{ - private const string Repository = "Strudelcode/SOCDOF"; - private static readonly Color Background = Color.FromArgb(30, 31, 35); - private static readonly Color Surface = Color.FromArgb(40, 42, 47); - private static readonly Color Muted = Color.FromArgb(165, 168, 180); - private static readonly Color Accent = Color.FromArgb(111, 126, 255); - - private readonly HttpClient http = new() { Timeout = TimeSpan.FromMinutes(10) }; - private readonly Label statusLabel = new(); - private readonly Label detailLabel = new(); - private readonly Label versionLabel = new(); - private readonly Label pathLabel = new(); - private readonly ProgressBar progress = new(); - private readonly Button installButton = new(); - private readonly Button changePathButton = new(); - private readonly Button closeButton = new(); - private string installDirectory = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "SOCDOF"); - private string? installerPath; - private bool downloadReady; - private bool installationRunning; - private bool installed; - - public InstallerForm() - { - Text = "SOCDOF Installer"; - StartPosition = FormStartPosition.CenterScreen; - FormBorderStyle = FormBorderStyle.None; - ClientSize = new Size(620, 430); - MinimumSize = new Size(620, 430); - MaximumSize = new Size(620, 430); - BackColor = Background; - ForeColor = Color.White; - ShowInTaskbar = true; - DoubleBuffered = true; - Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); - Region = Region.FromHrgn(CreateRoundRectRgn(0, 0, Width + 1, Height + 1, 18, 18)); - - BuildInterface(); - Shown += async (_, _) => await PrepareDownloadAsync(); - FormClosed += (_, _) => http.Dispose(); - } - - private void BuildInterface() - { - var titleBar = new Panel { Dock = DockStyle.Top, Height = 48, BackColor = Background }; - titleBar.MouseDown += DragWindow; - Controls.Add(titleBar); - - var titleIcon = new PictureBox - { - Location = new Point(22, 13), - Size = new Size(22, 22), - SizeMode = PictureBoxSizeMode.Zoom, - Image = Icon?.ToBitmap(), - BackColor = Color.Transparent - }; - titleBar.Controls.Add(titleIcon); - - var title = new Label - { - Text = "SOCDOF Setup", - Location = new Point(52, 14), - AutoSize = true, - ForeColor = Color.FromArgb(226, 228, 235), - Font = new Font("Segoe UI", 9.5f, FontStyle.Regular) - }; - title.MouseDown += DragWindow; - titleBar.Controls.Add(title); - - closeButton.Text = "×"; - closeButton.Size = new Size(44, 36); - closeButton.Location = new Point(ClientSize.Width - 52, 6); - closeButton.Anchor = AnchorStyles.Top | AnchorStyles.Right; - closeButton.FlatStyle = FlatStyle.Flat; - closeButton.FlatAppearance.BorderSize = 0; - closeButton.FlatAppearance.MouseOverBackColor = Color.FromArgb(190, 55, 65); - closeButton.BackColor = Background; - closeButton.ForeColor = Color.FromArgb(205, 207, 215); - closeButton.Font = new Font("Segoe UI", 16f); - closeButton.Cursor = Cursors.Hand; - closeButton.Click += (_, _) => Close(); - titleBar.Controls.Add(closeButton); - - var logo = new PictureBox - { - Location = new Point(42, 78), - Size = new Size(58, 58), - SizeMode = PictureBoxSizeMode.Zoom, - Image = Icon?.ToBitmap(), - BackColor = Color.Transparent - }; - Controls.Add(logo); - - var heading = new Label - { - Text = "SOCDOF installieren", - Location = new Point(118, 78), - Size = new Size(440, 36), - Font = new Font("Segoe UI Semibold", 20f, FontStyle.Bold), - ForeColor = Color.White - }; - Controls.Add(heading); - - versionLabel.Text = "Neueste Version wird gesucht …"; - versionLabel.Location = new Point(120, 117); - versionLabel.Size = new Size(440, 22); - versionLabel.Font = new Font("Segoe UI", 9.5f); - versionLabel.ForeColor = Muted; - Controls.Add(versionLabel); - - Controls.Add(new Panel { Location = new Point(36, 157), Size = new Size(548, 1), BackColor = Color.FromArgb(57, 59, 66) }); - - statusLabel.Text = "Installation wird vorbereitet …"; - statusLabel.Location = new Point(40, 181); - statusLabel.Size = new Size(530, 27); - statusLabel.Font = new Font("Segoe UI Semibold", 11f, FontStyle.Bold); - statusLabel.ForeColor = Color.FromArgb(240, 241, 246); - Controls.Add(statusLabel); - - detailLabel.Text = "SOCDOF wird sicher heruntergeladen und vorbereitet."; - detailLabel.Location = new Point(40, 211); - detailLabel.Size = new Size(540, 36); - detailLabel.Font = new Font("Segoe UI", 9f); - detailLabel.ForeColor = Muted; - Controls.Add(detailLabel); - - progress.Location = new Point(40, 257); - progress.Size = new Size(540, 5); - progress.Style = ProgressBarStyle.Marquee; - progress.MarqueeAnimationSpeed = 25; - progress.Visible = true; - progress.BackColor = Surface; - progress.ForeColor = Accent; - progress.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; - Controls.Add(progress); - - Controls.Add(new Label - { - Text = "INSTALLATIONSORDNER", - Location = new Point(40, 286), - AutoSize = true, - Font = new Font("Segoe UI Semibold", 8f, FontStyle.Bold), - ForeColor = Muted - }); - - var pathPanel = new Panel - { - Location = new Point(40, 307), - Size = new Size(540, 42), - BackColor = Surface, - Padding = new Padding(12, 0, 0, 0) - }; - Controls.Add(pathPanel); - - pathLabel.Text = installDirectory; - pathLabel.Location = new Point(12, 0); - pathLabel.Size = new Size(405, 42); - pathLabel.TextAlign = ContentAlignment.MiddleLeft; - pathLabel.Font = new Font("Segoe UI", 9f); - pathLabel.ForeColor = Color.FromArgb(224, 226, 234); - pathLabel.AutoEllipsis = true; - pathPanel.Controls.Add(pathLabel); - - changePathButton.Text = "Ändern"; - changePathButton.Location = new Point(431, 6); - changePathButton.Size = new Size(96, 30); - changePathButton.FlatStyle = FlatStyle.Flat; - changePathButton.FlatAppearance.BorderColor = Color.FromArgb(75, 78, 88); - changePathButton.FlatAppearance.MouseOverBackColor = Color.FromArgb(57, 60, 69); - changePathButton.BackColor = Surface; - changePathButton.ForeColor = Color.White; - changePathButton.Font = new Font("Segoe UI Semibold", 9f, FontStyle.Bold); - changePathButton.Cursor = Cursors.Hand; - changePathButton.Click += ChangeInstallDirectory; - pathPanel.Controls.Add(changePathButton); - - installButton.Text = "Download wird vorbereitet …"; - installButton.Location = new Point(40, 366); - installButton.Size = new Size(540, 42); - installButton.FlatStyle = FlatStyle.Flat; - installButton.FlatAppearance.BorderSize = 0; - installButton.BackColor = Accent; - installButton.ForeColor = Color.White; - installButton.Font = new Font("Segoe UI Semibold", 10f, FontStyle.Bold); - installButton.Cursor = Cursors.Hand; - installButton.Enabled = false; - installButton.Click += async (_, _) => - { - if (installed) LaunchApplication(); - else await InstallAsync(); - }; - Controls.Add(installButton); - - Controls.Add(new Label - { - Text = "STRUDELCODE · YURI", - Location = new Point(40, 411), - Size = new Size(540, 14), - TextAlign = ContentAlignment.MiddleCenter, - Font = new Font("Segoe UI", 7f, FontStyle.Bold), - ForeColor = Color.FromArgb(103, 106, 117) - }); - } - - private async Task PrepareDownloadAsync() - { - SetStatus("Neueste Version wird gesucht …", "Verbindung zu GitHub wird hergestellt.", 0, marquee: true); - installButton.Text = "Download wird vorbereitet …"; - installButton.Enabled = false; - - try - { - http.DefaultRequestHeaders.UserAgent.Clear(); - http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SOCDOF-Installer", "1.0")); - http.DefaultRequestHeaders.Accept.Clear(); - http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json")); - - using var releaseResponse = await http.GetAsync($"https://api.github.com/repos/{Repository}/releases/latest"); - releaseResponse.EnsureSuccessStatusCode(); - await using var releaseStream = await releaseResponse.Content.ReadAsStreamAsync(); - using var releaseJson = await JsonDocument.ParseAsync(releaseStream); - var root = releaseJson.RootElement; - var tag = root.TryGetProperty("tag_name", out var tagElement) ? tagElement.GetString() ?? "latest" : "latest"; - var assets = root.GetProperty("assets"); - string? downloadUrl = null; - long expectedBytes = 0; - - foreach (var asset in assets.EnumerateArray()) - { - var name = asset.GetProperty("name").GetString() ?? ""; - if (name.StartsWith("SOCDOF-Setup-", StringComparison.OrdinalIgnoreCase) && - name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) - { - downloadUrl = asset.GetProperty("browser_download_url").GetString(); - expectedBytes = asset.TryGetProperty("size", out var sizeElement) ? sizeElement.GetInt64() : 0; - break; - } - } - - if (string.IsNullOrWhiteSpace(downloadUrl)) - throw new InvalidOperationException("Die neueste GitHub-Version enthält kein passendes SOCDOF-Setup."); - - versionLabel.Text = $"Version {tag} · Windows 64-Bit"; - installerPath = Path.Combine(Path.GetTempPath(), $"SOCDOF-Setup-{Guid.NewGuid():N}.exe"); - SetStatus("SOCDOF wird heruntergeladen …", "Das Installationspaket wird im Hintergrund geladen.", 0, marquee: false); - progress.Style = ProgressBarStyle.Continuous; - progress.Value = 0; - - using var response = await http.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead); - response.EnsureSuccessStatusCode(); - var totalBytes = response.Content.Headers.ContentLength ?? expectedBytes; - await using var input = await response.Content.ReadAsStreamAsync(); - await using var output = new FileStream(installerPath, FileMode.Create, FileAccess.Write, FileShare.None, 81920, true); - var buffer = new byte[81920]; - long received = 0; - int read; - while ((read = await input.ReadAsync(buffer)) > 0) - { - await output.WriteAsync(buffer.AsMemory(0, read)); - received += read; - if (totalBytes > 0) - { - var percent = (int)Math.Clamp(received * 100L / totalBytes, 0, 100); - progress.Value = percent; - detailLabel.Text = $"Installationspaket wird heruntergeladen … {percent}%"; - } - } - await output.FlushAsync(); - - downloadReady = true; - SetStatus("Bereit zur Installation", "Wählen Sie bei Bedarf einen anderen Ordner und starten Sie die Installation.", 100, marquee: false); - installButton.Text = "Installation starten"; - installButton.Enabled = true; - } - catch (Exception ex) - { - SetStatus("Download nicht möglich", "Prüfen Sie die Internetverbindung und versuchen Sie es erneut.", 0, marquee: false); - detailLabel.Text = ex.Message.Length > 100 ? ex.Message[..100] + "…" : ex.Message; - installButton.Text = "Erneut versuchen"; - installButton.Enabled = true; - downloadReady = false; - } - } - - private void ChangeInstallDirectory(object? sender, EventArgs e) - { - using var picker = new FolderBrowserDialog - { - Description = "Wählen Sie den übergeordneten Ordner für SOCDOF.", - UseDescriptionForTitle = true, - ShowNewFolderButton = true, - SelectedPath = Path.GetDirectoryName(installDirectory) ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) - }; - if (picker.ShowDialog(this) == DialogResult.OK) - { - installDirectory = Path.Combine(picker.SelectedPath, "SOCDOF"); - pathLabel.Text = installDirectory; - } - } - - private async Task InstallAsync() - { - if (!downloadReady) - { - await PrepareDownloadAsync(); - return; - } - if (installationRunning || string.IsNullOrWhiteSpace(installerPath) || !File.Exists(installerPath)) return; - - installationRunning = true; - installButton.Enabled = false; - changePathButton.Enabled = false; - SetStatus("SOCDOF wird installiert …", "Die Anwendung wird eingerichtet. Bitte lassen Sie dieses Fenster geöffnet.", 0, marquee: true); - - try - { - Directory.CreateDirectory(installDirectory); - var startInfo = new ProcessStartInfo - { - FileName = installerPath, - Arguments = $"/S /D={installDirectory}", - UseShellExecute = false, - CreateNoWindow = true, - WorkingDirectory = Path.GetDirectoryName(installerPath) ?? Path.GetTempPath() - }; - using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Das Setup konnte nicht gestartet werden."); - await process.WaitForExitAsync(); - - if (process.ExitCode != 0) - throw new InvalidOperationException($"Das Setup wurde mit Fehlercode {process.ExitCode} beendet."); - - installed = true; - SetStatus("Installation abgeschlossen", "SOCDOF wurde erfolgreich installiert.", 100, marquee: false); - installButton.Text = "SOCDOF starten"; - installButton.Enabled = true; - changePathButton.Enabled = true; - } - catch (Exception ex) - { - SetStatus("Installation fehlgeschlagen", "Das Setup konnte nicht abgeschlossen werden.", 0, marquee: false); - detailLabel.Text = ex.Message.Length > 120 ? ex.Message[..120] + "…" : ex.Message; - installButton.Text = "Erneut versuchen"; - installButton.Enabled = true; - changePathButton.Enabled = true; - downloadReady = File.Exists(installerPath); - } - finally - { - installationRunning = false; - } - } - - private void LaunchApplication() - { - var executable = Path.Combine(installDirectory, "SOCDOF.exe"); - if (!File.Exists(executable)) - { - MessageBox.Show(this, $"Die Anwendung wurde nicht gefunden:\n{executable}", "SOCDOF", MessageBoxButtons.OK, MessageBoxIcon.Warning); - return; - } - try - { - Process.Start(new ProcessStartInfo(executable) { UseShellExecute = true }); - Close(); - } - catch (Exception ex) - { - MessageBox.Show(this, ex.Message, "SOCDOF konnte nicht gestartet werden", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - } - - private void SetStatus(string title, string detail, int percent, bool marquee) - { - statusLabel.Text = title; - detailLabel.Text = detail; - if (marquee) - { - progress.Style = ProgressBarStyle.Marquee; - progress.MarqueeAnimationSpeed = 25; - } - else - { - progress.Style = ProgressBarStyle.Continuous; - progress.MarqueeAnimationSpeed = 0; - progress.Value = Math.Clamp(percent, 0, 100); - } - } - - private static void DragWindow(object? sender, MouseEventArgs e) - { - if (e.Button != MouseButtons.Left) return; - var control = (Control?)sender; - var form = control?.FindForm(); - if (form is null) return; - ReleaseCapture(); - SendMessage(form.Handle, 0xA1, 0x2, 0); - } - - [DllImport("user32.dll")] private static extern bool ReleaseCapture(); - [DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int wParam, int lParam); - [DllImport("gdi32.dll")] private static extern IntPtr CreateRoundRectRgn(int left, int top, int right, int bottom, int width, int height); -} From c3d16e7c85db7f0d7a5329d8fe8b9e9f7fe3c767 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:37:51 +0200 Subject: [PATCH 26/34] refactor(installer): keep only lightweight framework bootstrapper --- .../SOCDOF.Bootstrapper.csproj | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 installer-bootstrapper/SOCDOF.Bootstrapper.csproj diff --git a/installer-bootstrapper/SOCDOF.Bootstrapper.csproj b/installer-bootstrapper/SOCDOF.Bootstrapper.csproj deleted file mode 100644 index 3939b8f..0000000 --- a/installer-bootstrapper/SOCDOF.Bootstrapper.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - WinExe - net8.0-windows - true - enable - enable - SOCDOF-Installer - SOCDOF.Bootstrapper - ..\public\socdof_icon.ico - true - true - win-x64 - true - true - - From 75d9527041e2cf2034500f0a3e1e5a442a74c07f Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:37:58 +0200 Subject: [PATCH 27/34] ci: validate SOCDOF app and instant-launch installer --- .../workflows/socdof-installer-validation.yml | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/socdof-installer-validation.yml diff --git a/.github/workflows/socdof-installer-validation.yml b/.github/workflows/socdof-installer-validation.yml new file mode 100644 index 0000000..8affcd9 --- /dev/null +++ b/.github/workflows/socdof-installer-validation.yml @@ -0,0 +1,39 @@ +name: Validate SOCDOF App and Installer + +on: + pull_request: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: windows-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install dependencies + run: npm install --no-package-lock + + - name: TypeScript validation + run: npm run lint + + - name: Build web application + run: npm run build + + - name: Compile instant-launch bootstrapper + shell: powershell + run: | + $msbuild = Get-ChildItem "C:\Program Files\Microsoft Visual Studio" -Filter MSBuild.exe -Recurse | Where-Object { $_.FullName -match "Current\\Bin\\MSBuild.exe$" } | Select-Object -First 1 + if (-not $msbuild) { throw "MSBuild.exe was not found on the Windows runner." } + & $msbuild.FullName installer-bootstrapper\SOCDOF.Bootstrapper.Framework.csproj /p:Configuration=Release /p:Platform=AnyCPU /m + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + if (-not (Test-Path release\bootstrapper\SOCDOF-Installer.exe)) { throw "Bootstrapper executable was not produced." } From c95336242db8319fa7038780b5a250f396514fe7 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:49:25 +0200 Subject: [PATCH 28/34] fix(installer): correct rounded window region arguments --- installer-bootstrapper/Program.Framework.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer-bootstrapper/Program.Framework.cs b/installer-bootstrapper/Program.Framework.cs index 744c910..c1dcdbe 100644 --- a/installer-bootstrapper/Program.Framework.cs +++ b/installer-bootstrapper/Program.Framework.cs @@ -56,7 +56,7 @@ public SetupWindow() ForeColor = Color.White; ShowInTaskbar = true; Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); - Region = System.Drawing.Region.FromHrgn(CreateRoundRectRgn(0, 0, Width + 1, Height + 1)); + Region = System.Drawing.Region.FromHrgn(CreateRoundRectRgn(0, 0, Width + 1, Height + 1, 18, 18)); Compose(); Shown += async delegate { await DownloadLatestAsync(); }; FormClosed += delegate { http.Dispose(); }; From 62313288b92dc522f233094704bc14a0336395b3 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:53:37 +0200 Subject: [PATCH 29/34] test(installer): validate full Windows NSIS packaging --- .github/workflows/socdof-installer-validation.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/socdof-installer-validation.yml b/.github/workflows/socdof-installer-validation.yml index 8affcd9..a02ab6c 100644 --- a/.github/workflows/socdof-installer-validation.yml +++ b/.github/workflows/socdof-installer-validation.yml @@ -29,6 +29,18 @@ jobs: - name: Build web application run: npm run build + - name: Package Windows NSIS installer + run: npx --yes electron-builder --win --x64 --config electron-builder.json --publish never + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify Windows installer artifact + shell: powershell + run: | + $setup = Get-ChildItem release -Filter 'SOCDOF-Setup-*.exe' -File | Select-Object -First 1 + if (-not $setup) { throw "Electron Builder did not produce the expected SOCDOF-Setup-*.exe artifact." } + Write-Host "Verified installer artifact: $($setup.FullName) ($($setup.Length) bytes)" + - name: Compile instant-launch bootstrapper shell: powershell run: | From 47ed7fd4828403294ffe82dc710036fe25a36954 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:54:30 +0200 Subject: [PATCH 30/34] fix(updater): select NSIS setup instead of bootstrapper --- src/lib/updateChecker.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/lib/updateChecker.ts b/src/lib/updateChecker.ts index 19eb03b..18a5b96 100644 --- a/src/lib/updateChecker.ts +++ b/src/lib/updateChecker.ts @@ -153,10 +153,12 @@ export async function checkForAppUpdates(): Promise { const cleanTag = tagName.replace(/^v/i, '').trim(); const hasUpdate = compareSemver(cleanTag, APP_VERSION) > 0; - // Find any .exe attachment asset if present - const exeAsset = Array.isArray(data.assets) - ? data.assets.find((a: any) => a.name?.toLowerCase().endsWith('.exe')) - : null; + // The in-app updater must download the actual NSIS setup, not the + // instant-launch bootstrapper (which is interactive and does not accept updater flags). + const assets = Array.isArray(data.assets) ? data.assets : []; + const exeAsset = assets.find((asset: any) => + typeof asset.name === 'string' && /^socdof-setup-.*\.exe$/i.test(asset.name) + ) || null; return { hasUpdate, From fd13581f0369920c8c3ab9edf744dcb4b8c6a108 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:54:35 +0200 Subject: [PATCH 31/34] fix(download): prefer instant-launch installer asset --- src/components/WindowsExeNotificationToast.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/WindowsExeNotificationToast.tsx b/src/components/WindowsExeNotificationToast.tsx index 322dc4b..9223c7b 100644 --- a/src/components/WindowsExeNotificationToast.tsx +++ b/src/components/WindowsExeNotificationToast.tsx @@ -50,7 +50,12 @@ export const WindowsExeNotificationToast: React.FC typeof a.name === 'string' && a.name.toLowerCase().endsWith('.exe')); + const assets = Array.isArray(data.assets) ? data.assets : []; + const exeAsset = assets.find((asset: any) => + typeof asset.name === 'string' && asset.name.toLowerCase() === 'socdof-installer.exe' + ) ?? assets.find((asset: any) => + typeof asset.name === 'string' && asset.name.toLowerCase().endsWith('.exe') + ); if (exeAsset?.browser_download_url) { setDownloadUrl(exeAsset.browser_download_url); } else if (data.html_url) { From 5f9019e32a1753b99c03b5d84a3972bbc3ff4729 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:54:39 +0200 Subject: [PATCH 32/34] docs(download): show correct installer filename --- src/components/CustomDownloadModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CustomDownloadModal.tsx b/src/components/CustomDownloadModal.tsx index 3637f3f..198ba60 100644 --- a/src/components/CustomDownloadModal.tsx +++ b/src/components/CustomDownloadModal.tsx @@ -400,7 +400,7 @@ export const CustomDownloadModal: React.FC = ({ Datei herunterladen

- Die Datei SOCDOF-Setup-{latestVersion}.exe wird in Ihrem gewohnten Windows-Download-Ordner gespeichert. + Die Datei SOCDOF-Installer.exe wird in Ihrem gewohnten Windows-Download-Ordner gespeichert.

From f2b8b3f6c3a0d96dc440156834b9e0492e7e30c1 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:55:24 +0200 Subject: [PATCH 33/34] test(installer): prepare NSIS assets and Electron tooling --- .github/workflows/socdof-installer-validation.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/socdof-installer-validation.yml b/.github/workflows/socdof-installer-validation.yml index a02ab6c..a189cf3 100644 --- a/.github/workflows/socdof-installer-validation.yml +++ b/.github/workflows/socdof-installer-validation.yml @@ -29,6 +29,12 @@ jobs: - name: Build web application run: npm run build + - name: Prepare NSIS artwork + run: node scripts/generate_nsis_bitmaps.js + + - name: Install Electron packaging tools + run: npm install --no-save --no-package-lock electron electron-builder + - name: Package Windows NSIS installer run: npx --yes electron-builder --win --x64 --config electron-builder.json --publish never env: From a6add107541085cd76bae5778032ff6196fa54f0 Mon Sep 17 00:00:00 2001 From: Strudelgame Date: Tue, 13 Oct 2026 17:56:07 +0200 Subject: [PATCH 34/34] test(installer): smoke-test fresh install and upgrade --- .../workflows/socdof-installer-validation.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/socdof-installer-validation.yml b/.github/workflows/socdof-installer-validation.yml index a189cf3..f7c1307 100644 --- a/.github/workflows/socdof-installer-validation.yml +++ b/.github/workflows/socdof-installer-validation.yml @@ -47,6 +47,23 @@ jobs: if (-not $setup) { throw "Electron Builder did not produce the expected SOCDOF-Setup-*.exe artifact." } Write-Host "Verified installer artifact: $($setup.FullName) ($($setup.Length) bytes)" + - name: Smoke-test fresh install and in-place upgrade + shell: powershell + run: | + $setup = Get-ChildItem release -Filter 'SOCDOF-Setup-*.exe' -File | Select-Object -First 1 + if (-not $setup) { throw "No SOCDOF NSIS installer is available for smoke testing." } + $installPath = Join-Path $env:RUNNER_TEMP 'SOCDOF-SmokeInstall' + $firstInstall = Start-Process -FilePath $setup.FullName -ArgumentList @('/S', "/D=$installPath") -Wait -PassThru + if ($firstInstall.ExitCode -ne 0) { throw "Fresh install failed with exit code $($firstInstall.ExitCode)." } + $appExe = Join-Path $installPath 'SOCDOF.exe' + if (-not (Test-Path $appExe)) { throw "Fresh install did not produce SOCDOF.exe at the selected install path." } + Get-Process -Name 'SOCDOF' -ErrorAction SilentlyContinue | Stop-Process -Force + $upgrade = Start-Process -FilePath $setup.FullName -ArgumentList @('/S', "/D=$installPath") -Wait -PassThru + if ($upgrade.ExitCode -ne 0) { throw "In-place upgrade failed with exit code $($upgrade.ExitCode)." } + if (-not (Test-Path $appExe)) { throw "In-place upgrade removed or failed to preserve SOCDOF.exe." } + Get-Process -Name 'SOCDOF' -ErrorAction SilentlyContinue | Stop-Process -Force + Write-Host "Fresh install and in-place upgrade passed at $installPath." + - name: Compile instant-launch bootstrapper shell: powershell run: |