From 4cfcfe52f613d321fccbe9138fc341dc60d001bf Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Sat, 7 Mar 2026 06:37:37 -0600 Subject: [PATCH 1/3] Upgrade to ElectronNET.Core 0.4.1. --- .../Nine.Infrastructure.csproj | 5 +- 4-Nine/ElectronHostHook/package-lock.json | 21 -- 4-Nine/ElectronHostHook/package.json | 9 - 4-Nine/ElectronHostHook/tsconfig.json | 17 - 4-Nine/Nine.csproj | 19 +- 4-Nine/Program.cs | 294 +++++------------- .../PublishProfiles/linux-x64.pubxml | 17 + .../Properties/PublishProfiles/win-x64.pubxml | 17 + 4-Nine/Properties/electron-builder.json | 46 +++ 4-Nine/Shared/App.razor | 2 +- .../Components/Account/Pages/Register.razor | 4 +- ...ectron.manifest.json => desktop-config.bk} | 0 12 files changed, 176 insertions(+), 275 deletions(-) delete mode 100644 4-Nine/ElectronHostHook/package-lock.json delete mode 100644 4-Nine/ElectronHostHook/package.json delete mode 100644 4-Nine/ElectronHostHook/tsconfig.json create mode 100644 4-Nine/Properties/PublishProfiles/linux-x64.pubxml create mode 100644 4-Nine/Properties/PublishProfiles/win-x64.pubxml create mode 100644 4-Nine/Properties/electron-builder.json rename 4-Nine/{electron.manifest.json => desktop-config.bk} (100%) diff --git a/1-Nine.Infrastructure/Nine.Infrastructure.csproj b/1-Nine.Infrastructure/Nine.Infrastructure.csproj index d989277..a606357 100644 --- a/1-Nine.Infrastructure/Nine.Infrastructure.csproj +++ b/1-Nine.Infrastructure/Nine.Infrastructure.csproj @@ -7,6 +7,10 @@ 0.2.0 + + + + @@ -14,7 +18,6 @@ - diff --git a/4-Nine/ElectronHostHook/package-lock.json b/4-Nine/ElectronHostHook/package-lock.json deleted file mode 100644 index eb17b0c..0000000 --- a/4-Nine/ElectronHostHook/package-lock.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "aquiis-simplestart-electron", - "version": "1.1.2", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "aquiis-simplestart-electron", - "version": "1.1.2", - "dependencies": { - "dasherize": "^2.0.0" - } - }, - "node_modules/dasherize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", - "integrity": "sha512-APql/TZ6FdLEpf2z7/X2a2zyqK8juYtqaSVqxw9mYoQ64CXkfU15AeLh8pUszT8+fnYjgm6t0aIYpWKJbnLkuA==", - "license": "MIT" - } - } -} diff --git a/4-Nine/ElectronHostHook/package.json b/4-Nine/ElectronHostHook/package.json deleted file mode 100644 index 645ccc2..0000000 --- a/4-Nine/ElectronHostHook/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "nine-electron", - "version": "1.0.0", - "description": "Nine Electron Host", - "main": "main.js", - "dependencies": { - "dasherize": "^2.0.0" - } -} diff --git a/4-Nine/ElectronHostHook/tsconfig.json b/4-Nine/ElectronHostHook/tsconfig.json deleted file mode 100644 index 4dbc186..0000000 --- a/4-Nine/ElectronHostHook/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "lib": ["ES2020"], - "outDir": "./dist", - "rootDir": "./", - "strict": false, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "resolveJsonModule": true - }, - "include": ["**/*.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/4-Nine/Nine.csproj b/4-Nine/Nine.csproj index 2c0157b..a962e49 100644 --- a/4-Nine/Nine.csproj +++ b/4-Nine/Nine.csproj @@ -2,6 +2,8 @@ net10.0 + + linux-x64 enable enable aspnet-Nine-c69b6efe-bb20-41de-8cba-044207ebdce1 @@ -14,6 +16,20 @@ 1.0.0 + + 30.4.0 + 26.0 + false + wwwroot/assets/splash.png + Assets/icon.png + co.nineapp.nine + Nine + + + + + + @@ -44,7 +60,8 @@ - + + diff --git a/4-Nine/Program.cs b/4-Nine/Program.cs index 9676204..69e9b4a 100644 --- a/4-Nine/Program.cs +++ b/4-Nine/Program.cs @@ -1,9 +1,7 @@ using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; using Nine.Core.Constants; -using Nine.Core.Entities; using Nine.Core.Interfaces; using Nine.Core.Interfaces.Services; using Nine.Extensions; @@ -24,21 +22,17 @@ SQLitePCL.Batteries_V2.Init(); SQLitePCL.raw.sqlite3_initialize(); +WebApplication app = null!; var builder = WebApplication.CreateBuilder(args); -// Configure for Electron FIRST — this sets HybridSupport.IsElectronActive, which -// HandlePendingRestore depends on to compute the correct Electron user-data DB path. -builder.WebHost.UseElectron(args); +// Configure Electron startup callback — called when Electron's socket bridge is ready. +builder.WebHost.UseElectron(args, ElectronAppReady); // CRITICAL: Handle .restore_pending BEFORE any DbContext registration. -// Must run AFTER UseElectron so HybridSupport.IsElectronActive is true. HandlePendingRestore(builder.Configuration); -// Configure URLs - use specific port for Electron -if (HybridSupport.IsElectronActive) -{ - builder.WebHost.UseUrls("http://localhost:8888"); -} +// Electron-only: always bind to the fixed port Electron expects +builder.WebHost.UseUrls("http://localhost:8888"); @@ -53,11 +47,8 @@ builder.Services.AddAntiforgery(options => { options.HeaderName = "X-CSRF-TOKEN"; - // Allow cookies over HTTP for Electron/Development - if (HybridSupport.IsElectronActive || builder.Environment.IsDevelopment()) - { - options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; - } + // Electron runs over plain HTTP — cookies must not require HTTPS + options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; }); @@ -77,15 +68,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); -// Add platform-specific infrastructure services (Database, Identity, Path services) -if (HybridSupport.IsElectronActive) -{ - builder.Services.AddElectronServices(builder.Configuration); -} -else -{ - builder.Services.AddWebServices(builder.Configuration); -} +// Electron-only: always use Electron services (user-data DB path, etc.) +builder.Services.AddElectronServices(builder.Configuration); // Configure organization-based authorization builder.Services.AddAuthorization(); @@ -187,8 +171,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(sp => { - // Pass app name to prevent keychain conflicts between different apps and modes - var appName = HybridSupport.IsElectronActive ? "Nine-Electron" : "Nine-Web"; + var appName = "Nine-Electron"; if (OperatingSystem.IsWindows()) return new WindowsKeychainService(appName); return new LinuxKeychainService(appName); @@ -210,12 +193,9 @@ var warningMinutes = config.GetValue("SessionTimeout:WarningDurationMinutes", 2); var enabled = config.GetValue("SessionTimeout:Enabled", true); - // Disable for Electron in development, or use longer timeout - if (HybridSupport.IsElectronActive) - { - timeoutMinutes = 120; // 2 hours for desktop app - enabled = false; // Typically disabled for desktop - } + // Electron desktop app: extended timeout, disabled by default + timeoutMinutes = 120; + enabled = false; service.InactivityTimeout = TimeSpan.FromMinutes(timeoutMinutes); service.WarningDuration = TimeSpan.FromMinutes(warningMinutes); @@ -227,7 +207,7 @@ // Register background service for scheduled tasks builder.Services.AddHostedService(); -var app = builder.Build(); +app = builder.Build(); // Ensure database is created and migrations are applied using (var scope = app.Services.CreateScope()) @@ -245,14 +225,18 @@ var identityContext = scope.ServiceProvider.GetRequiredService(); var backupService = scope.ServiceProvider.GetRequiredService(); - // For Electron, handle database initialization and migrations - if (HybridSupport.IsElectronActive) + // Electron-only: database initialization and migrations + try { - try - { var pathService = scope.ServiceProvider.GetRequiredService(); var dbPath = await pathService.GetDatabasePathAsync(); + // var dbPath = Path.Combine( + // Environment.GetEnvironmentVariable("XDG_CONFIG_HOME") + // ?? Path.Combine( + // Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"), + // "Nine"); + Console.WriteLine($"[Program] Beginning migrations Electron database path: {dbPath}"); // ✅ v1.0.0: Automatic migration from old Electron folder to Nine config folder @@ -471,84 +455,9 @@ } catch (Exception ex) { - app.Logger.LogError(ex, "Failed to initialize database for Electron"); - throw; - } - } - else - { - // Web mode - ensure migrations are applied - try - { - app.Logger.LogInformation("Applying database migrations for web mode"); - - // Get database path for web mode - // REMOVED: .restore_pending handling now happens BEFORE service registration - // This ensures encrypted database detection works correctly - // See HandlePendingRestore() called before AddWebServices() - - // Check if there are pending migrations for both contexts - var businessPendingCount = await dbService.GetPendingMigrationsCountAsync(); - var identityPendingCount = await dbService.GetIdentityPendingMigrationsCountAsync(); - - var isNewDatabase = businessPendingCount == 0 && identityPendingCount == 0; - - if (businessPendingCount > 0 || identityPendingCount > 0) - { - var totalCount = businessPendingCount + identityPendingCount; - app.Logger.LogInformation("Found {Count} pending migrations ({BusinessCount} business, {IdentityCount} identity)", - totalCount, businessPendingCount, identityPendingCount); - - // Create backup before migration - var backupPath = await backupService.CreatePreMigrationBackupAsync(); - if (backupPath != null) - { - app.Logger.LogInformation("Database backed up to {BackupPath}", backupPath); - } - } - - // Apply migrations to both contexts - if (identityPendingCount > 0 || businessPendingCount > 0) - { - app.Logger.LogInformation("Applying migrations ({Identity} identity, {Business} business)", - identityPendingCount, businessPendingCount); - await dbService.InitializeAsync(); - } - - app.Logger.LogInformation("Database migrations applied successfully"); - - // Update DatabaseSettings.DatabaseEncryptionEnabled flag to match actual encryption status - var encryptionDetection = scope.ServiceProvider.GetRequiredService(); - var currentSettings = await dbService.GetDatabaseSettingsAsync(); - - if (currentSettings.DatabaseEncryptionEnabled != encryptionDetection.IsEncrypted) - { - app.Logger.LogInformation( - "Updating DatabaseSettings.DatabaseEncryptionEnabled from {Old} to {New} (detected actual status)", - currentSettings.DatabaseEncryptionEnabled, - encryptionDetection.IsEncrypted); - await dbService.SetDatabaseEncryptionAsync(encryptionDetection.IsEncrypted, "System-AutoDetect"); - } - else - { - app.Logger.LogInformation( - "DatabaseSettings.DatabaseEncryptionEnabled already matches actual encryption status: {Status}", - encryptionDetection.IsEncrypted); - } - - // Create initial backup after creating a new database - if (isNewDatabase) - { - app.Logger.LogInformation("New database created, creating initial backup"); - await backupService.CreateBackupAsync("InitialSetup"); - } - } - catch (Exception ex) - { - app.Logger.LogError(ex, "Failed to apply database migrations"); + app.Logger.LogError(ex, "Failed to initialize database"); throw; } - } // Validate and update schema version var schemaService = scope.ServiceProvider.GetRequiredService(); @@ -595,39 +504,7 @@ // ✅ SECURITY: Content Security Policy and security headers app.UseSecurityHeaders(); -// ✅ SECURITY: HTTPS enforcement for production web mode -if (!HybridSupport.IsElectronActive) -{ - if (!app.Environment.IsDevelopment()) - { - // Production: MUST use HTTPS - app.UseHttpsRedirection(); - app.UseHsts(); - - // Validate HTTPS is actually configured - var httpsUrl = builder.Configuration["Kestrel:Endpoints:Https:Url"]; - if (string.IsNullOrEmpty(httpsUrl)) - { - app.Logger.LogWarning( - "HTTPS not configured in production. " + - "Configure Kestrel:Endpoints:Https in appsettings.Production.json or set ASPNETCORE_URLS environment variable."); - } - } - else - { - // Development: Optional HTTPS (for testing) - var useHttps = builder.Configuration.GetValue("Development:UseHttps", false); - if (useHttps) - { - app.UseHttpsRedirection(); - app.Logger.LogInformation("HTTPS enabled for development"); - } - else - { - app.Logger.LogInformation("Running in development without HTTPS"); - } - } -} +// Electron-only: no HTTPS redirection — app communicates over localhost HTTP app.UseAuthentication(); app.UseAuthorization(); @@ -701,32 +578,20 @@ } } -// Start the app for Electron -try -{ - app.Logger.LogInformation("Starting ASP.NET Core server..."); - await app.StartAsync(); - app.Logger.LogInformation("ASP.NET Core server started successfully"); -} -catch (Exception ex) -{ - app.Logger.LogCritical(ex, "FATAL: Failed to start ASP.NET Core server"); - Console.WriteLine($"FATAL ERROR: {ex.Message}"); - Console.WriteLine($"Stack Trace: {ex.StackTrace}"); - if (ex.InnerException != null) - { - Console.WriteLine($"Inner Exception: {ex.InnerException.Message}"); - } - Environment.Exit(1); -} +// Run the app — blocks until shutdown. ElectronAppReady fires via the callback +// registered in UseElectron() once the Electron socket bridge is fully ready. +await app.RunAsync(); -// Open Electron window -if (HybridSupport.IsElectronActive) +async Task ElectronAppReady() { + // Called by ElectronNET when Electron's socket bridge is fully ready. + // app is captured from the enclosing scope — guaranteed non-null by the time + // this fires, since Electron's ready event arrives after StartAsync completes. + // Verify backend is responding before showing window var backendUrl = "http://localhost:8888"; var isBackendReady = false; - + try { using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; @@ -738,7 +603,7 @@ { app.Logger.LogWarning(ex, "Backend health check failed, will show offline page"); } - + var window = await Electron.WindowManager.CreateWindowAsync(new ElectronNET.API.Entities.BrowserWindowOptions { Width = 1400, @@ -746,36 +611,42 @@ MinWidth = 800, MinHeight = 600, Show = false, - AutoHideMenuBar = true }); - window.RemoveMenu(); + + if (OperatingSystem.IsLinux() || OperatingSystem.IsWindows()) + { + window.SetMenuBarVisibility(false); + window.RemoveMenu(); + } window.OnReadyToShow += () => window.Show(); window.SetTitle("Nine Property Management"); - - // Load appropriate page based on backend availability + if (!isBackendReady) { app.Logger.LogWarning("Loading offline page due to backend unavailability"); window.LoadURL($"{backendUrl}/offline.html"); } - - // Open DevTools in development mode for debugging + if (app.Environment.IsDevelopment()) { window.WebContents.OpenDevTools(); app.Logger.LogInformation("DevTools opened for debugging"); } - - // Gracefully shutdown when window is closed + + // Re-register DevTools shortcut because RemoveMenu() strips default accelerators + Electron.GlobalShortcut.Register("CmdOrCtrl+Shift+I", () => + { + window.WebContents.ToggleDevTools(); + }); + window.OnClosed += () => { + Electron.GlobalShortcut.UnregisterAll(); app.Logger.LogInformation("Electron window closed, shutting down application"); Electron.App.Quit(); }; } -await app.WaitForShutdownAsync(); - // Local function to handle .restore_pending before service registration static void HandlePendingRestore(IConfiguration configuration) { @@ -793,50 +664,27 @@ static void HandlePendingRestore(IConfiguration configuration) string dbPath; - if (HybridSupport.IsElectronActive) - { - // Replicate ElectronPathService.GetDatabasePathSync() without needing DI. - var dbFileName = configuration["ApplicationSettings:DatabaseFileName"] ?? "app.db"; + // Electron-only: always use the OS user-data directory + var dbFileName = configuration["ApplicationSettings:DatabaseFileName"] ?? "app.db"; - string basePath; - if (OperatingSystem.IsWindows()) - basePath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Nine"); - else if (OperatingSystem.IsMacOS()) - basePath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - "Library", "Application Support", "Nine"); - else // Linux - basePath = Path.Combine( - Environment.GetEnvironmentVariable("XDG_CONFIG_HOME") - ?? Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"), - "Nine"); - - Directory.CreateDirectory(basePath); - dbPath = Path.Combine(basePath, dbFileName); - Console.WriteLine($"[Program.HandlePendingRestore] Electron mode — DB path: {dbPath}"); - } - else - { - // Web / non-Electron: derive path from appsettings.json connection string. - var connectionString = configuration.GetConnectionString("DefaultConnection"); - Console.WriteLine($"[Program.HandlePendingRestore] Web mode — connection string: {connectionString}"); - - if (string.IsNullOrEmpty(connectionString)) - { - Console.WriteLine("[Program.HandlePendingRestore] No connection string found, skipping"); - return; - } - - var csBuilder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder(connectionString); - dbPath = csBuilder.DataSource; - - if (!Path.IsPathRooted(dbPath)) - dbPath = Path.Combine(Directory.GetCurrentDirectory(), dbPath); - - Console.WriteLine($"[Program.HandlePendingRestore] Web mode — DB path: {dbPath}"); - } + string basePath; + if (OperatingSystem.IsWindows()) + basePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Nine"); + else if (OperatingSystem.IsMacOS()) + basePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Library", "Application Support", "Nine"); + else // Linux + basePath = Path.Combine( + Environment.GetEnvironmentVariable("XDG_CONFIG_HOME") + ?? Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"), + "Nine"); + + Directory.CreateDirectory(basePath); + dbPath = Path.Combine(basePath, dbFileName); + Console.WriteLine($"[Program.HandlePendingRestore] DB path: {dbPath}"); var stagedRestorePath = $"{dbPath}.restore_pending"; @@ -912,4 +760,4 @@ static void HandlePendingRestore(IConfiguration configuration) if (OperatingSystem.IsWindows()) Console.WriteLine($"[Program.HandlePendingRestore] HResult: 0x{ex.HResult:X8}"); } -} +} \ No newline at end of file diff --git a/4-Nine/Properties/PublishProfiles/linux-x64.pubxml b/4-Nine/Properties/PublishProfiles/linux-x64.pubxml new file mode 100644 index 0000000..3d9c845 --- /dev/null +++ b/4-Nine/Properties/PublishProfiles/linux-x64.pubxml @@ -0,0 +1,17 @@ + + + + Release + Any CPU + true + FileSystem + publish/$(Configuration)/$(TargetFramework)/$(RuntimeIdentifier)/ + FileSystem + <_TargetId>Folder + net10.0 + linux-x64 + true + true + false + + diff --git a/4-Nine/Properties/PublishProfiles/win-x64.pubxml b/4-Nine/Properties/PublishProfiles/win-x64.pubxml new file mode 100644 index 0000000..7f4c485 --- /dev/null +++ b/4-Nine/Properties/PublishProfiles/win-x64.pubxml @@ -0,0 +1,17 @@ + + + + Release + Any CPU + true + FileSystem + publish\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\ + FileSystem + <_TargetId>Folder + net10.0 + win-x64 + true + true + false + + diff --git a/4-Nine/Properties/electron-builder.json b/4-Nine/Properties/electron-builder.json new file mode 100644 index 0000000..c3871a2 --- /dev/null +++ b/4-Nine/Properties/electron-builder.json @@ -0,0 +1,46 @@ +{ + "appId": "co.nineapp.nine", + "productName": "Nine", + "copyright": "Copyright © 2026", + "buildVersion": "1.0.0", + "compression": "normal", + "files": ["**/*"], + "mac": { + "target": "dmg", + "icon": "Assets/icon.icns", + "category": "public.app-category.business" + }, + "win": { + "target": ["nsis", "portable"], + "icon": "Assets/icon.ico", + "artifactName": "${productName}-${version}-${arch}-Setup.${ext}" + }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "shortcutName": "Nine" + }, + "portable": { + "artifactName": "${productName}-${version}-${arch}-Portable.${ext}" + }, + "linux": { + "target": "AppImage", + "icon": "Assets/icon.png", + "category": "Office", + "artifactName": "${productName}-${version}-${arch}.${ext}", + "desktop": { + "entry": { + "X-AppImage-Payload-License": "MIT" + } + }, + "extraFiles": [ + { + "from": "Assets/co.nineapp.nine.appdata.xml", + "to": "usr/share/metainfo/co.nineapp.nine.appdata.xml" + } + ] + } +} diff --git a/4-Nine/Shared/App.razor b/4-Nine/Shared/App.razor index a825970..c029ceb 100644 --- a/4-Nine/Shared/App.razor +++ b/4-Nine/Shared/App.razor @@ -9,7 +9,7 @@ - + diff --git a/4-Nine/Shared/Components/Account/Pages/Register.razor b/4-Nine/Shared/Components/Account/Pages/Register.razor index 824cd8c..f560fdd 100644 --- a/4-Nine/Shared/Components/Account/Pages/Register.razor +++ b/4-Nine/Shared/Components/Account/Pages/Register.razor @@ -143,13 +143,13 @@ else -
+ @*

Use another service to register.


-
+
*@ } diff --git a/4-Nine/electron.manifest.json b/4-Nine/desktop-config.bk similarity index 100% rename from 4-Nine/electron.manifest.json rename to 4-Nine/desktop-config.bk From 979a308f4f098f61d5771ce038c51978de9838c6 Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Sat, 7 Mar 2026 06:50:42 -0600 Subject: [PATCH 2/3] Fixed issue with application path. --- 4-Nine/Services/ElectronPathService.cs | 137 ++++++++----------------- 1 file changed, 41 insertions(+), 96 deletions(-) diff --git a/4-Nine/Services/ElectronPathService.cs b/4-Nine/Services/ElectronPathService.cs index 9df5656..ca7c34d 100644 --- a/4-Nine/Services/ElectronPathService.cs +++ b/4-Nine/Services/ElectronPathService.cs @@ -1,6 +1,3 @@ -using ElectronNET.API; -using ElectronNET.API.Entities; -using Microsoft.Extensions.Configuration; using Nine.Core.Interfaces; namespace Nine.Services; @@ -19,7 +16,7 @@ public ElectronPathService(IConfiguration configuration) } /// - public bool IsActive => HybridSupport.IsElectronActive; + public bool IsActive => true; // App is Electron-only /// public async Task GetConnectionStringAsync(object configuration) @@ -32,31 +29,16 @@ public async Task GetConnectionStringAsync(object configuration) public async Task GetDatabasePathAsync() { var dbFileName = _configuration["ApplicationSettings:DatabaseFileName"] ?? "app.db"; - - if (HybridSupport.IsElectronActive) - { - var userDataPath = await GetUserDataPathAsync(); - var dbPath = Path.Combine(userDataPath, dbFileName); - - // Ensure the directory exists - var directory = Path.GetDirectoryName(dbPath); - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - return dbPath; - } - else + var userDataPath = await GetUserDataPathAsync(); + var dbPath = Path.Combine(userDataPath, dbFileName); + + var directory = Path.GetDirectoryName(dbPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { - // Fallback to local path if not in Electron mode - var dataDir = Path.Combine(Directory.GetCurrentDirectory(), "Data"); - if (!Directory.Exists(dataDir)) - { - Directory.CreateDirectory(dataDir); - } - return Path.Combine(dataDir, dbFileName); + Directory.CreateDirectory(directory); } + + return dbPath; } /// @@ -65,91 +47,54 @@ public async Task GetDatabasePathAsync() public string GetDatabasePathSync() { var dbFileName = _configuration["ApplicationSettings:DatabaseFileName"] ?? "app.db"; - - if (HybridSupport.IsElectronActive) - { - // Use OS-specific user data path without requiring Electron to be initialized - var userDataPath = GetUserDataPathSync(); - var dbPath = Path.Combine(userDataPath, dbFileName); - - // Ensure the directory exists - var directory = Path.GetDirectoryName(dbPath); - if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - return dbPath; - } - else + var userDataPath = GetUserDataPathSync(); + var dbPath = Path.Combine(userDataPath, dbFileName); + + var directory = Path.GetDirectoryName(dbPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { - // Fallback to local path if not in Electron mode - var dataDir = Path.Combine(Directory.GetCurrentDirectory(), "Data"); - if (!Directory.Exists(dataDir)) - { - Directory.CreateDirectory(dataDir); - } - return Path.Combine(dataDir, dbFileName); + Directory.CreateDirectory(directory); } + + return dbPath; } /// - public async Task GetUserDataPathAsync() + public Task GetUserDataPathAsync() { - if (HybridSupport.IsElectronActive) - { - // Use sync method to ensure consistent path resolution - // This matches the startup behavior and uses "Nine" as the app name - return GetUserDataPathSync(); - } - else - { - // Fallback for non-Electron mode - return Path.Combine(Directory.GetCurrentDirectory(), "Data"); - } + return Task.FromResult(GetUserDataPathSync()); } /// - /// Gets the user data path synchronously. + /// Gets the OS-specific user data path for the Nine app. /// private string GetUserDataPathSync() { - if (HybridSupport.IsElectronActive) + string basePath; + + if (OperatingSystem.IsWindows()) + { + basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + } + else if (OperatingSystem.IsMacOS()) { - // Determine OS-specific user data path without Electron API - string basePath; - var appName = "Nine"; - - if (OperatingSystem.IsWindows()) - { - basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - } - else if (OperatingSystem.IsMacOS()) - { - basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - "Library", "Application Support"); - } - else // Linux - { - basePath = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME") - ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); - } - - var userDataPath = Path.Combine(basePath, appName); - - // Ensure directory exists - if (!Directory.Exists(userDataPath)) - { - Directory.CreateDirectory(userDataPath); - } - - return userDataPath; + basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + "Library", "Application Support"); } - else + else // Linux { - // Fallback for non-Electron mode - return Path.Combine(Directory.GetCurrentDirectory(), "Data"); + basePath = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME") + ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); } + + var userDataPath = Path.Combine(basePath, "Nine"); + + if (!Directory.Exists(userDataPath)) + { + Directory.CreateDirectory(userDataPath); + } + + return userDataPath; } } From fba332459869c5d37195771047f0620d3762135a Mon Sep 17 00:00:00 2001 From: CIS Guru Date: Sat, 7 Mar 2026 07:10:38 -0600 Subject: [PATCH 3/3] Add runtime identifier conditionals. --- .gitignore | 3 +++ 4-Nine/Nine.csproj | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d003cd1..53a7671 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ sp_credentials.json publish_profile.xml +# Electron publish output — releases managed via GitHub Releases +4-Nine/publish/ + # Python virtual environment created per-project .venv/ diff --git a/4-Nine/Nine.csproj b/4-Nine/Nine.csproj index a962e49..8121adb 100644 --- a/4-Nine/Nine.csproj +++ b/4-Nine/Nine.csproj @@ -2,8 +2,10 @@ net10.0 - - linux-x64 + + linux-x64 + win-x64 + osx-x64 enable enable aspnet-Nine-c69b6efe-bb20-41de-8cba-044207ebdce1