diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index b49eb85c..bd5d8535 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -5842,12 +5842,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3288132732"] = "P -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T900713019"] = "Cancel" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1093747001"] = "Reason" + -- Settings UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1258653480"] = "Settings" +-- Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1378304679"] = "Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support." + -- Home UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1391791790"] = "Home" +-- AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1497084127"] = "AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support." + -- Are you sure you want to leave the chat page? All unsaved changes will be lost. UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1563130494"] = "Are you sure you want to leave the chat page? All unsaved changes will be lost." @@ -5857,12 +5866,21 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1614176092"] = "Assistants" -- Update UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1847791252"] = "Update" +-- Check for updates +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1890416390"] = "Check for updates" + +-- Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1988273622"] = "Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update." + -- Leave Chat Page UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2124749705"] = "Leave Chat Page" -- Plugins UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2222816203"] = "Plugins" +-- AI Studio cannot safely save settings in this session. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2382622618"] = "AI Studio cannot safely save settings in this session. Please check for updates or contact support." + -- An update to version {0} is available. UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2800137365"] = "An update to version {0} is available." @@ -5872,6 +5890,9 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2864211629"] = "Please wait for -- Supporters UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2929332068"] = "Supporters" +-- AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2936083926"] = "AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support." + -- Writer UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2979224202"] = "Writer" @@ -5884,6 +5905,9 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T4256323669"] = "Information" -- Chat UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T578410699"] = "Chat" +-- AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T915412625"] = "AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support." + -- Get coding and debugging support from an LLM. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1243850917"] = "Get coding and debugging support from an LLM." diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index f55c0e9f..57dc70dc 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -58,6 +58,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan private MudThemeProvider themeProvider = null!; private bool useDarkMode; private bool startupCompleted; + private bool settingsWriteProtectionWarningShown; private readonly SemaphoreSlim mandatoryInfoDialogSemaphore = new(1, 1); private IReadOnlyCollection navItems = []; @@ -127,6 +128,39 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan #endregion + private void ShowSettingsWriteProtectionWarning() + { + if(!this.SettingsManager.SettingsWriteBlocked || this.settingsWriteProtectionWarningShown) + return; + + this.settingsWriteProtectionWarningShown = true; + var reason = this.SettingsManager.SettingsWriteBlockReason; + var message = reason switch + { + SettingsWriteBlockReason.VERSION_NEWER_THAN_APP => T("Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update."), + SettingsWriteBlockReason.VERSION_MISSING => T("Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."), + SettingsWriteBlockReason.VERSION_UNKNOWN => T("AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."), + SettingsWriteBlockReason.FILE_UNREADABLE => T("AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support."), + SettingsWriteBlockReason.CURRENT_VERSION_INVALID => T("AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support."), + _ => T("AI Studio cannot safely save settings in this session. Please check for updates or contact support."), + }; + message = $"{message} {T("Reason")}: {reason}"; + + this.Snackbar.Add(message, Severity.Warning, config => + { + config.Icon = Icons.Material.Filled.WarningAmber; + config.IconSize = Size.Large; + config.VisibleStateDuration = 32_000; + config.HideTransitionDuration = 600; + config.Action = T("Check for updates"); + config.ActionVariant = Variant.Filled; + config.OnClick = async _ => + { + await this.MessageBus.SendMessage(this, Event.USER_SEARCH_FOR_UPDATE); + }; + }); + } + #region Implementation of ILang /// @@ -276,6 +310,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan case Event.PLUGINS_RELOADED: this.Lang = await this.SettingsManager.GetActiveLanguagePlugin(); I18N.Init(this.Lang); + this.ShowSettingsWriteProtectionWarning(); this.LoadNavItems(); await this.InvokeAsync(this.StateHasChanged); diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 52a97e4a..eecc3ce3 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -5844,12 +5844,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3288132732"] = "B -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T900713019"] = "Abbrechen" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1093747001"] = "Begründung" + -- Settings UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1258653480"] = "Einstellungen" +-- Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1378304679"] = "Ihre Einstellungsdatei enthält keine Versionsangabe des Einstellungsformats. Änderungen in dieser Sitzung werden nicht gespeichert, um ein Überschreiben Ihrer Einstellungen zu vermeiden. Bitte suchen Sie nach Updates oder wenden Sie sich an den Support." + -- Home UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1391791790"] = "Startseite" +-- AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1497084127"] = "AI Studio hat das aktuelle Einstellungsformat gefunden, konnte es jedoch nicht sicher laden. Änderungen in dieser Sitzung werden nicht gespeichert. Bitte suchen Sie nach Updates oder wenden Sie sich an den Support." + -- Are you sure you want to leave the chat page? All unsaved changes will be lost. UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1563130494"] = "Sind Sie sicher, dass Sie die Chat-Seite verlassen möchten? Alle nicht gespeicherten Änderungen gehen verloren." @@ -5859,12 +5868,21 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1614176092"] = "Assistenten" -- Update UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1847791252"] = "Aktualisieren" +-- Check for updates +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1890416390"] = "Nach Updates suchen" + +-- Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1988273622"] = "Ihre Einstellungen wurden mit einer neueren Version von AI Studio erstellt. Änderungen in dieser Sitzung werden nicht gespeichert. Bitte installieren oder starten Sie das neueste verfügbare Update." + -- Leave Chat Page UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2124749705"] = "Chat-Seite verlassen" -- Plugins UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2222816203"] = "Plugins" +-- AI Studio cannot safely save settings in this session. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2382622618"] = "AI Studio kann die Einstellungen in dieser Sitzung nicht sicher speichern. Bitte suchen Sie nach Updates oder wenden Sie sich an den Support." + -- An update to version {0} is available. UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2800137365"] = "Ein Update auf Version {0} ist verfügbar." @@ -5874,6 +5892,9 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2864211629"] = "Bitte warten Sie -- Supporters UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2929332068"] = "Unterstützer" +-- AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2936083926"] = "AI Studio konnte Ihre Einstellungsdatei nicht lesen. Änderungen in dieser Sitzung werden nicht gespeichert, um ein Überschreiben wiederherstellbarer Einstellungen zu vermeiden. Bitte suchen Sie nach Updates oder wenden Sie sich an den Support." + -- Writing UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2979224202"] = "Schreiben" @@ -5886,6 +5907,9 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T4256323669"] = "Information" -- Chat UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T578410699"] = "Chat" +-- AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T915412625"] = "AI Studio erkennt die Version Ihres Einstellungsformats nicht. Änderungen in dieser Sitzung werden nicht gespeichert, um zu verhindern, dass Ihre Einstellungen überschrieben werden. Bitte suchen Sie nach Updates oder wenden Sie sich an den Support." + -- Get coding and debugging support from an LLM. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1243850917"] = "Erhalten Sie Unterstützung beim Programmieren und Debuggen durch ein KI-Modell." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 112b9baf..c60b245a 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -5844,12 +5844,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T3288132732"] = "P -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::WORKSPACESELECTIONDIALOG::T900713019"] = "Cancel" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1093747001"] = "Reason" + -- Settings UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1258653480"] = "Settings" +-- Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1378304679"] = "Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support." + -- Home UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1391791790"] = "Home" +-- AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1497084127"] = "AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support." + -- Are you sure you want to leave the chat page? All unsaved changes will be lost. UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1563130494"] = "Are you sure you want to leave the chat page? All unsaved changes will be lost." @@ -5859,12 +5868,21 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1614176092"] = "Assistants" -- Update UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1847791252"] = "Update" +-- Check for updates +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1890416390"] = "Check for updates" + +-- Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T1988273622"] = "Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update." + -- Leave Chat Page UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2124749705"] = "Leave Chat Page" -- Plugins UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2222816203"] = "Plugins" +-- AI Studio cannot safely save settings in this session. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2382622618"] = "AI Studio cannot safely save settings in this session. Please check for updates or contact support." + -- An update to version {0} is available. UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2800137365"] = "An update to version {0} is available." @@ -5874,6 +5892,9 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2864211629"] = "Please wait for -- Supporters UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2929332068"] = "Supporters" +-- AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2936083926"] = "AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support." + -- Writing UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2979224202"] = "Writing" @@ -5886,6 +5907,9 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T4256323669"] = "Information" -- Chat UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T578410699"] = "Chat" +-- AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support. +UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T915412625"] = "AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support." + -- Get coding and debugging support from an LLM. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T1243850917"] = "Get coding and debugging support from an LLM." diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 3873ada0..336d4f95 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -17,6 +17,11 @@ namespace AIStudio.Settings; public sealed class SettingsManager { private const string SETTINGS_FILENAME = "settings.json"; + private const Version CURRENT_SETTINGS_VERSION = Version.V6; + + private readonly record struct SettingsVersionReadResult(Version Version, SettingsWriteBlockReason FailureReason); + + private readonly record struct CurrentSettingsReadResult(Data? SettingsData, SettingsWriteBlockReason FailureReason); private static readonly JsonSerializerOptions JSON_OPTIONS = new() { @@ -62,6 +67,16 @@ public sealed class SettingsManager /// public bool HasCompletedInitialSettingsLoad { get; private set; } + /// + /// Indicates why settings writes are blocked for the current session. + /// + public SettingsWriteBlockReason SettingsWriteBlockReason { get; private set; } = SettingsWriteBlockReason.NONE; + + /// + /// Indicates that settings writes are blocked for the current session. + /// + public bool SettingsWriteBlocked => this.SettingsWriteBlockReason is not SettingsWriteBlockReason.NONE; + /// /// The configuration data. /// @@ -87,6 +102,7 @@ public sealed class SettingsManager /// A (migrated) settings snapshot, or null if it could not be read. public async Task TryReadSettingsSnapshot() { + this.SettingsWriteBlockReason = SettingsWriteBlockReason.NONE; if(!this.IsSetUp) { this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet."); @@ -100,38 +116,175 @@ public sealed class SettingsManager return null; } - // We read the `"Version": "V3"` line to determine the version of the settings file: - await foreach (var line in File.ReadLinesAsync(settingsPath)) + var settingsVersion = await this.TryReadSettingsVersion(settingsPath); + if(settingsVersion.FailureReason is not SettingsWriteBlockReason.NONE) { - if (!line.Contains(""" - "Version": - """)) - continue; + this.BlockSettingsWrites(settingsVersion.FailureReason, "The settings file version could not be identified. Settings writes are blocked to avoid overwriting newer or unreadable settings."); + return await this.TryReadCurrentVersionBackupSnapshotForBlockedSettings(); + } - // Extract the version from the line: - var settingsVersionText = line.Split('"')[3]; + if(settingsVersion.Version > CURRENT_SETTINGS_VERSION) + { + this.BlockSettingsWrites(SettingsWriteBlockReason.VERSION_NEWER_THAN_APP, $"The settings file uses the newer version '{settingsVersion.Version}'. Settings writes are blocked to avoid overwriting newer settings."); + return await this.TryReadCurrentVersionBackupSnapshotForBlockedSettings(); + } - // Parse the version: - Enum.TryParse(settingsVersionText, out Version settingsVersion); - if(settingsVersion is Version.UNKNOWN) + Data? settingsData; + if(settingsVersion.Version < CURRENT_SETTINGS_VERSION) + { + settingsData = await this.TryReadCurrentVersionBackupSnapshot(); + if(settingsData is not null) { - this.logger.LogError("Unknown version of the settings file found."); - return new(); + this.PrepareLoadedSettings(settingsData); + await this.StoreSettingsSnapshot(settingsData, settingsPath); + await this.StoreCurrentVersionBackup(settingsData); + this.logger.LogInformation($"Restored settings from the '{GetBackupSettingsFilename(CURRENT_SETTINGS_VERSION)}' backup file."); + return settingsData; } - var settingsData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS); - - // - // We filter the enabled preview features based on the preview visibility. - // This is necessary when the app starts up: some preview features may have - // been disabled or released from the last time the app was started. - // - settingsData.App.EnabledPreviewFeatures = settingsData.App.PreviewVisibility.FilterPreviewFeatures(settingsData.App.EnabledPreviewFeatures); + this.logger.LogInformation("No valid current-version settings backup was found. Migrating the settings file."); + settingsData = SettingsMigrations.Migrate(this.logger, settingsVersion.Version, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS); + this.PrepareLoadedSettings(settingsData); + await this.StoreSettingsSnapshot(settingsData, settingsPath); + await this.StoreCurrentVersionBackup(settingsData); return settingsData; } - this.logger.LogError("Failed to read the version of the settings file."); - return new(); + var currentSettings = await this.TryDeserializeCurrentSettings(settingsPath, "settings file"); + if(currentSettings.FailureReason is not SettingsWriteBlockReason.NONE) + { + this.BlockSettingsWrites(currentSettings.FailureReason, "The current settings file could not be safely loaded. Settings writes are blocked to avoid overwriting recoverable settings."); + return await this.TryReadCurrentVersionBackupSnapshotForBlockedSettings(); + } + + settingsData = currentSettings.SettingsData!; + this.PrepareLoadedSettings(settingsData); + await this.StoreCurrentVersionBackup(settingsData); + return settingsData; + } + + private async Task TryReadSettingsVersion(string settingsPath) + { + try + { + await using var settingsStream = File.OpenRead(settingsPath); + using var settingsDocument = await JsonDocument.ParseAsync(settingsStream); + if(!settingsDocument.RootElement.TryGetProperty("Version", out var versionElement)) + { + this.logger.LogError($"Failed to read the version of the settings file '{settingsPath}'."); + return new(Version.UNKNOWN, SettingsWriteBlockReason.VERSION_MISSING); + } + + if(versionElement.ValueKind is JsonValueKind.String && versionElement.GetString() is { } versionText) + { + if(Enum.TryParse(versionText, out Version stringVersion) && Enum.IsDefined(stringVersion) && stringVersion is not Version.UNKNOWN) + return new(stringVersion, SettingsWriteBlockReason.NONE); + + if(versionText.StartsWith('V') && int.TryParse(versionText[1..], out var futureVersion) && futureVersion > (int)CURRENT_SETTINGS_VERSION) + return new((Version)futureVersion, SettingsWriteBlockReason.NONE); + + if(int.TryParse(versionText, out var numericStringVersion) && numericStringVersion > (int)CURRENT_SETTINGS_VERSION) + return new((Version)numericStringVersion, SettingsWriteBlockReason.NONE); + } + + if(versionElement.ValueKind is JsonValueKind.Number && versionElement.TryGetInt32(out var numericVersion) && numericVersion > (int)Version.UNKNOWN && (Enum.IsDefined(typeof(Version), numericVersion) || numericVersion > (int)CURRENT_SETTINGS_VERSION)) + return new((Version)numericVersion, SettingsWriteBlockReason.NONE); + } + catch(Exception e) + { + this.logger.LogError(e, $"Failed to read the version of the settings file '{settingsPath}'."); + return new(Version.UNKNOWN, SettingsWriteBlockReason.FILE_UNREADABLE); + } + + return new(Version.UNKNOWN, SettingsWriteBlockReason.VERSION_UNKNOWN); + } + + private async Task TryReadCurrentVersionBackupSnapshot() + { + var backupSettingsPath = GetBackupSettingsPath(CURRENT_SETTINGS_VERSION); + if(!File.Exists(backupSettingsPath)) + { + this.logger.LogInformation($"The settings backup file '{backupSettingsPath}' does not exist."); + return null; + } + + var backupVersion = await this.TryReadSettingsVersion(backupSettingsPath); + if(backupVersion.FailureReason is not SettingsWriteBlockReason.NONE) + { + this.logger.LogWarning($"The settings backup file '{backupSettingsPath}' could not be used because its version could not be identified. Reason: '{backupVersion.FailureReason}'."); + return null; + } + + if(backupVersion.Version != CURRENT_SETTINGS_VERSION) + { + this.logger.LogWarning($"The settings backup file '{backupSettingsPath}' uses version '{backupVersion.Version}' instead of '{CURRENT_SETTINGS_VERSION}'."); + return null; + } + + var backupSettings = await this.TryDeserializeCurrentSettings(backupSettingsPath, "settings backup file"); + if(backupSettings.FailureReason is not SettingsWriteBlockReason.NONE) + { + this.logger.LogWarning($"The settings backup file '{backupSettingsPath}' could not be used. Reason: '{backupSettings.FailureReason}'."); + return null; + } + + return backupSettings.SettingsData; + } + + private async Task TryReadCurrentVersionBackupSnapshotForBlockedSettings() + { + var settingsData = await this.TryReadCurrentVersionBackupSnapshot(); + if(settingsData is null) + { + this.logger.LogWarning($"No valid current-version settings backup was found while settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'."); + return null; + } + + this.PrepareLoadedSettings(settingsData); + this.logger.LogWarning($"Loaded settings from the '{GetBackupSettingsFilename(CURRENT_SETTINGS_VERSION)}' backup file while settings writes remain blocked. Reason: '{this.SettingsWriteBlockReason}'."); + return settingsData; + } + + private async Task TryDeserializeCurrentSettings(string settingsPath, string sourceDescription) + { + try + { + var settingsData = JsonSerializer.Deserialize(await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS); + if(settingsData is null) + { + this.logger.LogError($"Failed to parse the {sourceDescription} '{settingsPath}'."); + return new(null, SettingsWriteBlockReason.CURRENT_VERSION_INVALID); + } + + if(settingsData.Version != CURRENT_SETTINGS_VERSION) + { + this.logger.LogError($"The {sourceDescription} '{settingsPath}' uses version '{settingsData.Version}' instead of '{CURRENT_SETTINGS_VERSION}'."); + return new(null, SettingsWriteBlockReason.CURRENT_VERSION_INVALID); + } + + return new(settingsData, SettingsWriteBlockReason.NONE); + } + catch(Exception e) + { + this.logger.LogError(e, $"Failed to parse the {sourceDescription} '{settingsPath}'."); + return new(null, SettingsWriteBlockReason.FILE_UNREADABLE); + } + } + + private void BlockSettingsWrites(SettingsWriteBlockReason reason, string message) + { + this.SettingsWriteBlockReason = reason; + this.logger.LogError($"{message} Reason: '{reason}'."); + } + + private void PrepareLoadedSettings(Data settingsData) + { + // + // We filter the enabled preview features based on the preview visibility. + // This is necessary when the app starts up: some preview features may have + // been disabled or released from the last time the app was started. + // + settingsData.App.EnabledPreviewFeatures = settingsData.App.PreviewVisibility.FilterPreviewFeatures(settingsData.App.EnabledPreviewFeatures); } /// @@ -145,19 +298,48 @@ public sealed class SettingsManager return; } + if(this.SettingsWriteBlocked) + { + this.logger.LogWarning($"Cannot store settings, because settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'."); + return; + } + var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME); + await this.StoreSettingsSnapshot(this.ConfigurationData, settingsPath); + await this.StoreCurrentVersionBackup(this.ConfigurationData); + } + + private static string GetBackupSettingsFilename(Version version) => $"settings.{version.ToString().ToLowerInvariant()}.json"; + + private static string GetBackupSettingsPath(Version version) => Path.Combine(ConfigDirectory!, GetBackupSettingsFilename(version)); + + private async Task StoreCurrentVersionBackup(Data settingsData) + { + if(settingsData.Version != CURRENT_SETTINGS_VERSION) + { + this.logger.LogWarning($"Skipping settings backup because the settings version '{settingsData.Version}' is not the current version '{CURRENT_SETTINGS_VERSION}'."); + return; + } + + var backupSettingsPath = GetBackupSettingsPath(CURRENT_SETTINGS_VERSION); + await this.StoreSettingsSnapshot(settingsData, backupSettingsPath); + this.logger.LogInformation($"Stored the settings backup file '{backupSettingsPath}'."); + } + + private async Task StoreSettingsSnapshot(Data settingsData, string settingsPath) + { if(!Directory.Exists(ConfigDirectory)) { this.logger.LogInformation("Creating the configuration directory."); Directory.CreateDirectory(ConfigDirectory!); } - var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS); + var settingsJson = JsonSerializer.Serialize(settingsData, JSON_OPTIONS); var tempFile = Path.GetTempFileName(); await File.WriteAllTextAsync(tempFile, settingsJson); File.Move(tempFile, settingsPath, true); - this.logger.LogInformation("Stored the settings to the file system."); + this.logger.LogInformation($"Stored the settings to '{settingsPath}'."); } public void InjectSpellchecking(Dictionary attributes) => attributes["spellcheck"] = this.ConfigurationData.App.EnableSpellchecking ? "true" : "false"; diff --git a/app/MindWork AI Studio/Settings/SettingsWriteBlockReason.cs b/app/MindWork AI Studio/Settings/SettingsWriteBlockReason.cs new file mode 100644 index 00000000..d21285b0 --- /dev/null +++ b/app/MindWork AI Studio/Settings/SettingsWriteBlockReason.cs @@ -0,0 +1,11 @@ +namespace AIStudio.Settings; + +public enum SettingsWriteBlockReason +{ + NONE, + VERSION_MISSING, + VERSION_UNKNOWN, + VERSION_NEWER_THAN_APP, + FILE_UNREADABLE, + CURRENT_VERSION_INVALID, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md index 1af7154e..8e26f706 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md @@ -7,4 +7,5 @@ - Changed provider confidence settings to appear in their own settings panel, because they apply to LLM, embedding, and transcription providers. - Fixed chat provider, profile, and template selections not updating live after configuration plugins were changed. - Fixed organization-managed chat templates not showing the correct icon in the chat template selection menu. +- Fixed personal settings sometimes being lost after a settings-format upgrade when an older app version was started again. AI Studio now keeps versioned settings backups, restores the latest compatible backup when needed, and warns users when settings cannot be saved safely. - Fixed self-hosted provider API keys sometimes being stored under a localized name. AI Studio now uses a stable key name, keeps correct entries working, and automatically migrates known localized entries for LLM, transcription, and embedding providers. Organizations using configuration plugins do not need to change their plugins; affected users who still see an invalid API key warning should open the provider, transcription, or embedding settings and update the API key once. Thanks, Tim & Eric, for the detailed bug report and testing help. \ No newline at end of file