From b01bf04e7f518c027c5678b7d58cac8548869044 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Sun, 6 Sep 2026 12:28:38 +0200 Subject: [PATCH] (feature): mvvm ui, appdata settings, and desktop packages --- CHANGELOG.md | 17 + CONTRIBUTING.md | 4 +- Directory.Build.props | 2 +- README.md | 21 +- src/MaksIT.LTO.Backup.Service/Program.cs | 5 +- .../ConfigurationFileService.cs | 41 +- .../UserSettingsPath.cs | 17 + src/MaksIT.LTO.Backup.UI/App.axaml.cs | 10 +- src/MaksIT.LTO.Backup.UI/Assets/icon.ico | Bin 0 -> 12528 bytes src/MaksIT.LTO.Backup.UI/Assets/icon.png | Bin 0 -> 8315 bytes src/MaksIT.LTO.Backup.UI/Assets/icon.svg | 23 + src/MaksIT.LTO.Backup.UI/ConfirmDialog.axaml | 1 + src/MaksIT.LTO.Backup.UI/DialogService.cs | 21 + src/MaksIT.LTO.Backup.UI/MainWindow.axaml | 391 +++--- src/MaksIT.LTO.Backup.UI/MainWindow.axaml.cs | 1191 +---------------- .../MaksIT.LTO.Backup.UI.csproj | 6 + src/MaksIT.LTO.Backup.UI/Themes/Dark.axaml | 8 +- .../ViewModels/MainViewModel.Monitor.cs | 129 ++ .../ViewModels/MainViewModel.Service.cs | 74 + .../ViewModels/MainViewModel.Settings.cs | 558 ++++++++ .../ViewModels/MainViewModel.cs | 676 ++++++++++ .../ViewModels/UiModels.cs | 101 ++ src/MaksIT.LTO.Backup/Program.cs | 6 +- .../engines/release/Invoke-ReleasePackage.ps1 | 1 + utils/engines/release/scriptSettings.json | 4 +- utils/modules/Engine/EngineContext.psm1 | 32 + utils/modules/Engine/Import-EngineModules.ps1 | 1 + utils/modules/Engine/PluginSupport.psm1 | 620 +++++++-- utils/modules/Engine/ReleaseSupport.psm1 | 1 + utils/modules/Engine/TestSupport.psm1 | 1 + utils/modules/Engine/VaultSupport.psm1 | 373 ++++++ utils/modules/ExternalCommandSupport.psm1 | 2 +- utils/plugins/Desktop/DesktopPackSupport.psm1 | 1037 ++++++++++++++ utils/plugins/Desktop/FlatpakPack.psm1 | 445 ++++++ utils/plugins/Desktop/WindowsInstaller.psm1 | 303 +++++ .../Desktop/brand/HyperlinkSidebarTheme.xml | 139 ++ utils/plugins/Desktop/brand/logo-64.png | Bin 0 -> 2170 bytes utils/plugins/Desktop/brand/mark.ico | Bin 0 -> 23823 bytes utils/plugins/Desktop/brand/mark.png | Bin 0 -> 21108 bytes utils/plugins/Desktop/brand/mark.svg | 27 + utils/plugins/Desktop/brand/sidebar.png | Bin 0 -> 16934 bytes .../DotNet/DotNetCleanupArtifacts.psm1 | 122 -- utils/plugins/DotNet/DotNetCreateArchive.psm1 | 43 +- utils/plugins/DotNet/DotNetNuGet.psm1 | 10 +- utils/plugins/DotNet/DotNetPublish.psm1 | 178 ++- utils/plugins/DotNet/DotNetTest.psm1 | 5 +- utils/plugins/Npm/NpmPublish.psm1 | 14 +- utils/plugins/Platform/GitHub.psm1 | 12 +- 48 files changed, 5014 insertions(+), 1658 deletions(-) create mode 100644 src/MaksIT.LTO.Backup.Shared/UserSettingsPath.cs create mode 100644 src/MaksIT.LTO.Backup.UI/Assets/icon.ico create mode 100644 src/MaksIT.LTO.Backup.UI/Assets/icon.png create mode 100644 src/MaksIT.LTO.Backup.UI/Assets/icon.svg create mode 100644 src/MaksIT.LTO.Backup.UI/DialogService.cs create mode 100644 src/MaksIT.LTO.Backup.UI/ViewModels/MainViewModel.Monitor.cs create mode 100644 src/MaksIT.LTO.Backup.UI/ViewModels/MainViewModel.Service.cs create mode 100644 src/MaksIT.LTO.Backup.UI/ViewModels/MainViewModel.Settings.cs create mode 100644 src/MaksIT.LTO.Backup.UI/ViewModels/MainViewModel.cs create mode 100644 src/MaksIT.LTO.Backup.UI/ViewModels/UiModels.cs create mode 100644 utils/modules/Engine/VaultSupport.psm1 create mode 100644 utils/plugins/Desktop/DesktopPackSupport.psm1 create mode 100644 utils/plugins/Desktop/FlatpakPack.psm1 create mode 100644 utils/plugins/Desktop/WindowsInstaller.psm1 create mode 100644 utils/plugins/Desktop/brand/HyperlinkSidebarTheme.xml create mode 100644 utils/plugins/Desktop/brand/logo-64.png create mode 100644 utils/plugins/Desktop/brand/mark.ico create mode 100644 utils/plugins/Desktop/brand/mark.png create mode 100644 utils/plugins/Desktop/brand/mark.svg create mode 100644 utils/plugins/Desktop/brand/sidebar.png delete mode 100644 utils/plugins/DotNet/DotNetCleanupArtifacts.psm1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9393f1a..d11fc93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.1.0-alpha.2] - 2026-09-06 + +Still alpha versus **0.1.0-alpha.1**. Emulator and build validated; physical Windows/Linux tape, changer, and Linux SMB staging are not hardware-proven yet. Not a production-ready line. + +### Added + +- Desktop packaging as sibling GitHub assets: Windows setup `maksit-lto-backup-{version}.exe` (Avalonia UI) and `maksit-lto-backup-{version}.flatpak` (Avalonia UI), alongside the portable `maksit-lto-backup-{version}.zip` (win-x64 console, Avalonia UI, and Worker). The installer and Flatpak are not packed inside the zip. On Windows the Flatpak bundle is built via WSL Debian. +- Faceted LTO cartridge app icon (not the brand M). + +### Changed + +- Avalonia UI is **MVVM** (`CommunityToolkit.Mvvm`): operations, settings, monitor, and service controls live in `MainViewModel`; `MainWindow` is a thin view. +- Configuration (console, UI, Worker) is written to `%AppData%/MaksIT/LTO Backup/settings.json` (WiX `installFolderName`). The shipped `configuration.json` next to the exe is seed-only. +- Dark theme uses the MAKS.IT origami blues (`#33A5CF` / `#006199`). +- Synced RepoUtils utils: ContainerRegistry JSON catalog (PascalCase Harbor / InCluster keys). + ## [0.1.0-alpha.1] - 2026-08-21 Alpha preview versus last published **0.0.2** (HEAD was console-only Windows on **net8.0**). Emulator and build validated; physical Windows/Linux tape, changer, and Linux SMB staging are not hardware-proven yet. Not a production-ready line. @@ -46,3 +62,4 @@ Alpha preview versus last published **0.0.2** (HEAD was console-only Windows on - AES-GCM encrypted descriptors, checksum verification, cartridge EEPROM (MAM) read/write. - Dependency injection, console and file logging. - Build scripts and contributor docs (`README.md`, `CONTRIBUTING.md`, `LICENSE.md`). + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 11604c1..b23fd53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ dotnet build MaksIT.LTO.slnx ### Configuration -Edit the shared file only: [`src/MaksIT.LTO.Backup.Shared/configuration.json`](src/MaksIT.LTO.Backup.Shared/configuration.json). Console, Avalonia UI, and Worker link it into their outputs. See [README.md](README.md). +Edit the shared seed only: [`src/MaksIT.LTO.Backup.Shared/configuration.json`](src/MaksIT.LTO.Backup.Shared/configuration.json). Console, Avalonia UI, and Worker link it into their outputs. Runtime saves go to `%AppData%/MaksIT/LTO Backup/settings.json`. See [README.md](README.md). ### Tests @@ -42,7 +42,7 @@ Coverage shields at the top of `README.md` are maintained by the **CoverageBadge 1. Update [CHANGELOG.md](CHANGELOG.md) and bump `` in [`Directory.Build.props`](Directory.Build.props) (`X.Y.Z` or SemVer prerelease such as `0.1.0-alpha.1`). 2. Commit on `main`, tag `v{version}` on HEAD (e.g. `v0.1.0-alpha.1`). GitHub marks hyphenated versions as prerelease. -3. Run `utils\Invoke-ReleasePackage.bat`. +3. Run `utils\Invoke-ReleasePackage.bat`. GitHub assets are the portable zip (win-x64), Windows setup exe (Avalonia UI), and Flatpak (Avalonia UI). ## Commit format diff --git a/Directory.Build.props b/Directory.Build.props index e3b44fa..f9ced42 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,6 +3,6 @@ latest enable enable - 0.1.0-alpha.1 + 0.1.0-alpha.2 diff --git a/README.md b/README.md index f90018a..c1eab9f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # MaksIT.LTO.Backup -![Line Coverage](https://img.shields.io/badge/Line%20Coverage-39.2%25-yellow) -![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-28.4%25-yellow) -![Method Coverage](https://img.shields.io/badge/Method%20Coverage-49.5%25-yellowgreen) +![Line Coverage](https://img.shields.io/badge/Line%20Coverage-39.3%25-yellow) +![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-28.7%25-yellow) +![Method Coverage](https://img.shields.io/badge/Method%20Coverage-49.7%25-yellowgreen) ![.NET](https://img.shields.io/badge/.NET-10-512BD4) ![License](https://img.shields.io/badge/License-GPLv2-blue) ![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux-0078D6) @@ -14,7 +14,7 @@ See [LICENSE.md](LICENSE.md) (GPLv2). Changes: [CHANGELOG.md](CHANGELOG.md). Con | Host | Project | Role | |------|---------|------| | Console | `MaksIT.LTO.Backup` | Interactive menu / `backup --name` CLI | -| Avalonia | `MaksIT.LTO.Backup.UI` | Code-behind operations UI (Windows + Linux; no MVVM). SimpleTheme **Dark** styles aligned with Cluster Console | +| Avalonia | `MaksIT.LTO.Backup.UI` | MVVM operations UI (Windows + Linux; CommunityToolkit.Mvvm) | | Worker | `MaksIT.LTO.Backup.Service` | Scheduled library backups (Windows service / systemd) | Device access modes: @@ -22,7 +22,7 @@ Device access modes: - **Physical** — real drive/library on **Windows** (`\\.\Tape0`, `\\.\Changer0`; admin) or **Linux** (`/dev/nst0`, `/dev/sg*`; `tape`/`disk` group) - **Emulated** — file-backed drive + library (no hardware; primary CI / no-hardware path) -> Current line is **`0.1.0-alpha.1`** (prerelease, not production-ready). Emulator coverage is the primary validation path; physical tape/changer and Linux SMB are not hardware-proven yet. Use at your own risk. +> Current line is **`0.1.0-alpha.2`** (prerelease, not production-ready). Emulator coverage is the primary validation path; physical tape/changer and Linux SMB are not hardware-proven yet. Use at your own risk. --- @@ -104,13 +104,14 @@ Install/uninstall typically require administrator (Windows) or root (Linux syste ## Configuration -One shared model and one shared JSON for the whole solution: +One shared model for the whole solution. Seed JSON ships next to each host; runtime writes go to AppData so Program Files does not need elevation. - Model: [`src/MaksIT.LTO.Backup.Shared/Models/Configuration.cs`](src/MaksIT.LTO.Backup.Shared/Models/Configuration.cs) -- File: [`src/MaksIT.LTO.Backup.Shared/configuration.json`](src/MaksIT.LTO.Backup.Shared/configuration.json) +- Seed: [`src/MaksIT.LTO.Backup.Shared/configuration.json`](src/MaksIT.LTO.Backup.Shared/configuration.json) (copied next to the exe, never written) +- Runtime: `%AppData%/MaksIT/LTO Backup/settings.json` (same folder name as WiX: `Program Files\MaksIT\LTO Backup`) - Loader / workflows: `ConfigurationFileService`, `BackupOrchestrator`, `LibraryOrchestrator` in `MaksIT.LTO.Backup.Shared` -Both console and Avalonia UI link this file into their output. Edit the Shared copy only, or use the UI **Settings** tab and click **Save configuration**. +Edit the Shared seed for factory defaults, or use the UI **Settings** tab and click **Save configuration**. `DeviceMode` selects the backend (`Physical` / `Emulated`). `Topology` selects how hardware is used: @@ -196,7 +197,7 @@ Avalonia UI (Windows or Linux): dotnet run --project .\MaksIT.LTO.Backup.UI ``` -Publish hosts (or use RepoUtils release below): +Prefer RepoUtils for a GitHub release (`utils\Invoke-ReleasePackage.bat`). Assets are siblings: portable `maksit-lto-backup-{version}.zip` (win-x64 console, Avalonia UI, Worker), Windows setup `maksit-lto-backup-{version}.exe` (Avalonia UI), and `maksit-lto-backup-{version}.flatpak` (Avalonia UI). The installer and Flatpak are not inside the zip. Manual publish: ```powershell cd src @@ -234,6 +235,8 @@ Vendored under `utils/` (from MaksIT RepoUtils community): | Test | `utils\Invoke-TestEngine.bat` | | Release | `utils\Invoke-ReleasePackage.bat` | +GitHub assets are siblings: portable `maksit-lto-backup-{version}.zip` (win-x64), Windows setup exe (Avalonia UI), and Flatpak (Avalonia UI). The installer and Flatpak are not inside the zip. On Windows the Flatpak bundle is built via WSL Debian. + Settings: - `utils/engines/test/scriptSettings.json` diff --git a/src/MaksIT.LTO.Backup.Service/Program.cs b/src/MaksIT.LTO.Backup.Service/Program.cs index 21fa0d4..343e669 100644 --- a/src/MaksIT.LTO.Backup.Service/Program.cs +++ b/src/MaksIT.LTO.Backup.Service/Program.cs @@ -10,8 +10,7 @@ namespace MaksIT.LTO.Backup.Service; internal static class Program { public static int Main(string[] args) { var basePath = AppContext.BaseDirectory; - var configurationPath = Path.Combine(basePath, "configuration.json"); - var configurationFileService = new ConfigurationFileService(configurationPath); + var configurationFileService = new ConfigurationFileService(); var service = configurationFileService.Current.Service; var serviceName = string.IsNullOrWhiteSpace(service.ServiceName) ? "MaksIT.LTO.Backup" @@ -90,7 +89,7 @@ internal static class Program { --help, -h Show this help message Service Name: {serviceName} - Config File: configuration.json (next to the executable) + Config File: {UserSettingsPath.Get(ConfigurationFileService.ProductFolder)} Note: Install/uninstall typically require administrator / root privileges. Scheduling runs only when Topology = TapeLibrary. diff --git a/src/MaksIT.LTO.Backup.Shared/ConfigurationFileService.cs b/src/MaksIT.LTO.Backup.Shared/ConfigurationFileService.cs index 2a705ef..e9e43b2 100644 --- a/src/MaksIT.LTO.Backup.Shared/ConfigurationFileService.cs +++ b/src/MaksIT.LTO.Backup.Shared/ConfigurationFileService.cs @@ -6,11 +6,15 @@ using MaksIT.LTO.Backup.Shared.Models; namespace MaksIT.LTO.Backup.Shared; public sealed class ConfigurationFileService { + public const string ProductFolder = "LTO Backup"; + public const string SeedFileName = "configuration.json"; + private static readonly JsonSerializerOptions SerializerOptions = new() { WriteIndented = true, PropertyNamingPolicy = null }; + private readonly string? _seedPath; private Configuration _current; public string FilePath { get; } @@ -18,7 +22,15 @@ public sealed class ConfigurationFileService { public Configuration Current => _current; public ConfigurationFileService(string? configurationPath = null) { - FilePath = configurationPath ?? Path.Combine(Directory.GetCurrentDirectory(), "configuration.json"); + if (!string.IsNullOrWhiteSpace(configurationPath)) { + FilePath = configurationPath; + _seedPath = null; + } + else { + FilePath = UserSettingsPath.Get(ProductFolder); + _seedPath = Path.Combine(AppContext.BaseDirectory, SeedFileName); + } + _current = LoadFromDisk(); } @@ -30,23 +42,40 @@ public sealed class ConfigurationFileService { public void Save(Configuration configuration) { ArgumentNullException.ThrowIfNull(configuration); - var root = File.Exists(FilePath) - ? JsonNode.Parse(File.ReadAllText(FilePath)) as JsonObject ?? new JsonObject() - : new JsonObject(); + var dir = Path.GetDirectoryName(FilePath); + if (!string.IsNullOrEmpty(dir)) + Directory.CreateDirectory(dir); + var root = ReadRoot(File.Exists(FilePath) ? FilePath : ResolveReadPath()) ?? []; root["Configuration"] = JsonSerializer.SerializeToNode(configuration, SerializerOptions); File.WriteAllText(FilePath, root.ToJsonString(SerializerOptions)); _current = configuration; } private Configuration LoadFromDisk() { - if (!File.Exists(FilePath)) + var path = ResolveReadPath(); + if (path is null) return new Configuration(); - using var document = JsonDocument.Parse(File.ReadAllText(FilePath)); + using var document = JsonDocument.Parse(File.ReadAllText(path)); if (!document.RootElement.TryGetProperty("Configuration", out var value)) return new Configuration(); return JsonSerializer.Deserialize(value.GetRawText(), SerializerOptions) ?? new Configuration(); } + + private static JsonObject? ReadRoot(string? path) { + if (path is null || !File.Exists(path)) + return null; + + return JsonNode.Parse(File.ReadAllText(path)) as JsonObject; + } + + private string? ResolveReadPath() { + if (File.Exists(FilePath)) + return FilePath; + if (_seedPath is not null && File.Exists(_seedPath)) + return _seedPath; + return null; + } } diff --git a/src/MaksIT.LTO.Backup.Shared/UserSettingsPath.cs b/src/MaksIT.LTO.Backup.Shared/UserSettingsPath.cs new file mode 100644 index 0000000..c0b442c --- /dev/null +++ b/src/MaksIT.LTO.Backup.Shared/UserSettingsPath.cs @@ -0,0 +1,17 @@ +namespace MaksIT.LTO.Backup.Shared; + + +/// +/// User-writable settings under AppData. The product folder must match the WiX +/// installFolderName (or Get-DesktopInstallFolderName from +/// appName + manufacturer), e.g. %AppData%/MaksIT/LTO Backup. +/// Shipped JSON next to the exe is seed-only. +/// +public static class UserSettingsPath { + public static string Get(string product, string fileName = "settings.json") => + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "MaksIT", + product, + fileName); +} diff --git a/src/MaksIT.LTO.Backup.UI/App.axaml.cs b/src/MaksIT.LTO.Backup.UI/App.axaml.cs index 84c43a7..1bf192b 100644 --- a/src/MaksIT.LTO.Backup.UI/App.axaml.cs +++ b/src/MaksIT.LTO.Backup.UI/App.axaml.cs @@ -6,6 +6,7 @@ using Avalonia.Markup.Xaml; using Avalonia.Controls.ApplicationLifetimes; using MaksIT.Core.Logging; using MaksIT.LTO.Backup.Shared; +using MaksIT.LTO.Backup.UI.ViewModels; namespace MaksIT.LTO.Backup.UI; @@ -18,18 +19,22 @@ public partial class App : Application { public override void OnFrameworkInitializationCompleted() { var basePath = AppContext.BaseDirectory; - var configurationPath = Path.Combine(basePath, "configuration.json"); + var userSettings = UserSettingsPath.Get(ConfigurationFileService.ProductFolder); _host = Host.CreateDefaultBuilder() .ConfigureAppConfiguration(builder => { builder.SetBasePath(basePath); builder.AddJsonFile("configuration.json", optional: false, reloadOnChange: true); + if (File.Exists(userSettings)) + builder.AddJsonFile(userSettings, optional: true, reloadOnChange: true); }) .ConfigureServices((_, services) => { - services.AddSingleton(_ => new ConfigurationFileService(configurationPath)); + services.AddSingleton(_ => new ConfigurationFileService()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); }) .ConfigureLogging((_, logging) => { @@ -43,6 +48,7 @@ public partial class App : Application { if (_host is null) return; + _host.Services.GetRequiredService().Dispose(); await _host.StopAsync(); _host.Dispose(); _host = null; diff --git a/src/MaksIT.LTO.Backup.UI/Assets/icon.ico b/src/MaksIT.LTO.Backup.UI/Assets/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..11212d85c9a3c48f8dc32f5b9fc8cfa37621348c GIT binary patch literal 12528 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 + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MaksIT.LTO.Backup.UI/ConfirmDialog.axaml b/src/MaksIT.LTO.Backup.UI/ConfirmDialog.axaml index f8127e5..be7894e 100644 --- a/src/MaksIT.LTO.Backup.UI/ConfirmDialog.axaml +++ b/src/MaksIT.LTO.Backup.UI/ConfirmDialog.axaml @@ -2,6 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="MaksIT.LTO.Backup.UI.ConfirmDialog" Title="Confirm" + Icon="/Assets/icon.ico" Width="440" Height="180" MinWidth="360" diff --git a/src/MaksIT.LTO.Backup.UI/DialogService.cs b/src/MaksIT.LTO.Backup.UI/DialogService.cs new file mode 100644 index 0000000..81b6ea5 --- /dev/null +++ b/src/MaksIT.LTO.Backup.UI/DialogService.cs @@ -0,0 +1,21 @@ +using Avalonia.Controls; + + +namespace MaksIT.LTO.Backup.UI; + +public interface IDialogService { + Window? Owner { get; set; } + + Task ConfirmAsync(string title, string message); +} + +public sealed class DialogService : IDialogService { + public Window? Owner { get; set; } + + public async Task ConfirmAsync(string title, string message) { + if (Owner is null) + return false; + + return await ConfirmDialog.ShowAsync(Owner, title, message); + } +} diff --git a/src/MaksIT.LTO.Backup.UI/MainWindow.axaml b/src/MaksIT.LTO.Backup.UI/MainWindow.axaml index ca33c32..215f369 100644 --- a/src/MaksIT.LTO.Backup.UI/MainWindow.axaml +++ b/src/MaksIT.LTO.Backup.UI/MainWindow.axaml @@ -3,6 +3,7 @@ xmlns:controls="using:MaksIT.LTO.Backup.UI.Controls" x:Class="MaksIT.LTO.Backup.UI.MainWindow" Title="MaksIT.LTO.Backup" + Icon="/Assets/icon.ico" Width="1100" Height="760" MinWidth="880" @@ -23,31 +24,24 @@ - + IsChecked="{Binding LiveMonitorEnabled}" /> - - - - - - -