mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-06-27 15:36:26 +00:00
Improved settings safety with write-block reasons and warnings
This commit is contained in:
parent
9da669ec39
commit
6930c4c8ee
@ -107,6 +107,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
|||||||
// Set the snackbar for the update service:
|
// Set the snackbar for the update service:
|
||||||
UpdateService.SetBlazorDependencies(this.Snackbar);
|
UpdateService.SetBlazorDependencies(this.Snackbar);
|
||||||
TemporaryChatService.Initialize();
|
TemporaryChatService.Initialize();
|
||||||
|
this.ShowSettingsWriteProtectionWarning();
|
||||||
|
|
||||||
// Should the navigation bar be open by default?
|
// Should the navigation bar be open by default?
|
||||||
if(this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
|
if(this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
|
||||||
@ -127,6 +128,38 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
private void ShowSettingsWriteProtectionWarning()
|
||||||
|
{
|
||||||
|
if(!this.SettingsManager.SettingsWriteBlocked)
|
||||||
|
return;
|
||||||
|
|
||||||
|
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<bool>(this, Event.USER_SEARCH_FOR_UPDATE);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#region Implementation of ILang
|
#region Implementation of ILang
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -19,6 +19,10 @@ public sealed class SettingsManager
|
|||||||
private const string SETTINGS_FILENAME = "settings.json";
|
private const string SETTINGS_FILENAME = "settings.json";
|
||||||
private const Version CURRENT_SETTINGS_VERSION = Version.V6;
|
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()
|
private static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
||||||
{
|
{
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
@ -27,7 +31,6 @@ public sealed class SettingsManager
|
|||||||
|
|
||||||
private readonly ILogger<SettingsManager> logger;
|
private readonly ILogger<SettingsManager> logger;
|
||||||
private readonly RustService rustService;
|
private readonly RustService rustService;
|
||||||
private bool settingsWriteBlocked;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The settings manager.
|
/// The settings manager.
|
||||||
@ -64,6 +67,16 @@ public sealed class SettingsManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasCompletedInitialSettingsLoad { get; private set; }
|
public bool HasCompletedInitialSettingsLoad { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates why settings writes are blocked for the current session.
|
||||||
|
/// </summary>
|
||||||
|
public SettingsWriteBlockReason SettingsWriteBlockReason { get; private set; } = SettingsWriteBlockReason.NONE;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that settings writes are blocked for the current session.
|
||||||
|
/// </summary>
|
||||||
|
public bool SettingsWriteBlocked => this.SettingsWriteBlockReason is not SettingsWriteBlockReason.NONE;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The configuration data.
|
/// The configuration data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -89,7 +102,7 @@ public sealed class SettingsManager
|
|||||||
/// <returns>A (migrated) settings snapshot, or null if it could not be read.</returns>
|
/// <returns>A (migrated) settings snapshot, or null if it could not be read.</returns>
|
||||||
public async Task<Data?> TryReadSettingsSnapshot()
|
public async Task<Data?> TryReadSettingsSnapshot()
|
||||||
{
|
{
|
||||||
this.settingsWriteBlocked = false;
|
this.SettingsWriteBlockReason = SettingsWriteBlockReason.NONE;
|
||||||
if(!this.IsSetUp)
|
if(!this.IsSetUp)
|
||||||
{
|
{
|
||||||
this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet.");
|
this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet.");
|
||||||
@ -104,22 +117,20 @@ public sealed class SettingsManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
var settingsVersion = await this.TryReadSettingsVersion(settingsPath);
|
var settingsVersion = await this.TryReadSettingsVersion(settingsPath);
|
||||||
if(settingsVersion is Version.UNKNOWN)
|
if(settingsVersion.FailureReason is not SettingsWriteBlockReason.NONE)
|
||||||
{
|
{
|
||||||
this.logger.LogError("Unknown version of the settings file found. Settings writes are blocked to avoid overwriting newer or unreadable settings.");
|
this.BlockSettingsWrites(settingsVersion.FailureReason, "The settings file version could not be identified. Settings writes are blocked to avoid overwriting newer or unreadable settings.");
|
||||||
this.settingsWriteBlocked = true;
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(settingsVersion > CURRENT_SETTINGS_VERSION)
|
if(settingsVersion.Version > CURRENT_SETTINGS_VERSION)
|
||||||
{
|
{
|
||||||
this.logger.LogError($"The settings file uses the newer version '{settingsVersion}'. Settings writes are blocked to avoid overwriting newer settings.");
|
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.");
|
||||||
this.settingsWriteBlocked = true;
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Data? settingsData;
|
Data? settingsData;
|
||||||
if(settingsVersion < CURRENT_SETTINGS_VERSION)
|
if(settingsVersion.Version < CURRENT_SETTINGS_VERSION)
|
||||||
{
|
{
|
||||||
settingsData = await this.TryReadCurrentVersionBackupSnapshot();
|
settingsData = await this.TryReadCurrentVersionBackupSnapshot();
|
||||||
if(settingsData is not null)
|
if(settingsData is not null)
|
||||||
@ -132,26 +143,27 @@ public sealed class SettingsManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.logger.LogInformation("No valid current-version settings backup was found. Migrating the settings file.");
|
this.logger.LogInformation("No valid current-version settings backup was found. Migrating the settings file.");
|
||||||
settingsData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
|
settingsData = SettingsMigrations.Migrate(this.logger, settingsVersion.Version, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
|
||||||
this.PrepareLoadedSettings(settingsData);
|
this.PrepareLoadedSettings(settingsData);
|
||||||
await this.StoreSettingsSnapshot(settingsData, settingsPath);
|
await this.StoreSettingsSnapshot(settingsData, settingsPath);
|
||||||
await this.StoreCurrentVersionBackup(settingsData);
|
await this.StoreCurrentVersionBackup(settingsData);
|
||||||
return settingsData;
|
return settingsData;
|
||||||
}
|
}
|
||||||
|
|
||||||
settingsData = await this.TryDeserializeCurrentSettings(settingsPath, "settings file");
|
var currentSettings = await this.TryDeserializeCurrentSettings(settingsPath, "settings file");
|
||||||
if(settingsData is null)
|
if(currentSettings.FailureReason is not SettingsWriteBlockReason.NONE)
|
||||||
{
|
{
|
||||||
this.settingsWriteBlocked = true;
|
this.BlockSettingsWrites(currentSettings.FailureReason, "The current settings file could not be safely loaded. Settings writes are blocked to avoid overwriting recoverable settings.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settingsData = currentSettings.SettingsData!;
|
||||||
this.PrepareLoadedSettings(settingsData);
|
this.PrepareLoadedSettings(settingsData);
|
||||||
await this.StoreCurrentVersionBackup(settingsData);
|
await this.StoreCurrentVersionBackup(settingsData);
|
||||||
return settingsData;
|
return settingsData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Version> TryReadSettingsVersion(string settingsPath)
|
private async Task<SettingsVersionReadResult> TryReadSettingsVersion(string settingsPath)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -160,21 +172,31 @@ public sealed class SettingsManager
|
|||||||
if(!settingsDocument.RootElement.TryGetProperty("Version", out var versionElement))
|
if(!settingsDocument.RootElement.TryGetProperty("Version", out var versionElement))
|
||||||
{
|
{
|
||||||
this.logger.LogError($"Failed to read the version of the settings file '{settingsPath}'.");
|
this.logger.LogError($"Failed to read the version of the settings file '{settingsPath}'.");
|
||||||
return Version.UNKNOWN;
|
return new(Version.UNKNOWN, SettingsWriteBlockReason.VERSION_MISSING);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(versionElement.ValueKind is JsonValueKind.String && versionElement.GetString() is { } versionText && Enum.TryParse(versionText, out Version stringVersion))
|
if(versionElement.ValueKind is JsonValueKind.String && versionElement.GetString() is { } versionText)
|
||||||
return stringVersion;
|
{
|
||||||
|
if(Enum.TryParse(versionText, out Version stringVersion) && Enum.IsDefined(typeof(Version), stringVersion) && stringVersion is not Version.UNKNOWN)
|
||||||
|
return new(stringVersion, SettingsWriteBlockReason.NONE);
|
||||||
|
|
||||||
if(versionElement.ValueKind is JsonValueKind.Number && versionElement.TryGetInt32(out var numericVersion) && Enum.IsDefined(typeof(Version), numericVersion))
|
if(versionText.StartsWith('V') && int.TryParse(versionText[1..], out var futureVersion) && futureVersion > (int)CURRENT_SETTINGS_VERSION)
|
||||||
return (Version)numericVersion;
|
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)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
this.logger.LogError(e, $"Failed to read the version of the settings file '{settingsPath}'.");
|
this.logger.LogError(e, $"Failed to read the version of the settings file '{settingsPath}'.");
|
||||||
|
return new(Version.UNKNOWN, SettingsWriteBlockReason.FILE_UNREADABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Version.UNKNOWN;
|
return new(Version.UNKNOWN, SettingsWriteBlockReason.VERSION_UNKNOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Data?> TryReadCurrentVersionBackupSnapshot()
|
private async Task<Data?> TryReadCurrentVersionBackupSnapshot()
|
||||||
@ -187,16 +209,29 @@ public sealed class SettingsManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
var backupVersion = await this.TryReadSettingsVersion(backupSettingsPath);
|
var backupVersion = await this.TryReadSettingsVersion(backupSettingsPath);
|
||||||
if(backupVersion != CURRENT_SETTINGS_VERSION)
|
if(backupVersion.FailureReason is not SettingsWriteBlockReason.NONE)
|
||||||
{
|
{
|
||||||
this.logger.LogWarning($"The settings backup file '{backupSettingsPath}' uses version '{backupVersion}' instead of '{CURRENT_SETTINGS_VERSION}'.");
|
this.logger.LogWarning($"The settings backup file '{backupSettingsPath}' could not be used because its version could not be identified. Reason: '{backupVersion.FailureReason}'.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.TryDeserializeCurrentSettings(backupSettingsPath, "settings backup file");
|
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<Data?> TryDeserializeCurrentSettings(string settingsPath, string sourceDescription)
|
private async Task<CurrentSettingsReadResult> TryDeserializeCurrentSettings(string settingsPath, string sourceDescription)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -204,24 +239,30 @@ public sealed class SettingsManager
|
|||||||
if(settingsData is null)
|
if(settingsData is null)
|
||||||
{
|
{
|
||||||
this.logger.LogError($"Failed to parse the {sourceDescription} '{settingsPath}'.");
|
this.logger.LogError($"Failed to parse the {sourceDescription} '{settingsPath}'.");
|
||||||
return null;
|
return new(null, SettingsWriteBlockReason.CURRENT_VERSION_INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(settingsData.Version != CURRENT_SETTINGS_VERSION)
|
if(settingsData.Version != CURRENT_SETTINGS_VERSION)
|
||||||
{
|
{
|
||||||
this.logger.LogError($"The {sourceDescription} '{settingsPath}' uses version '{settingsData.Version}' instead of '{CURRENT_SETTINGS_VERSION}'.");
|
this.logger.LogError($"The {sourceDescription} '{settingsPath}' uses version '{settingsData.Version}' instead of '{CURRENT_SETTINGS_VERSION}'.");
|
||||||
return null;
|
return new(null, SettingsWriteBlockReason.CURRENT_VERSION_INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
return settingsData;
|
return new(settingsData, SettingsWriteBlockReason.NONE);
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
this.logger.LogError(e, $"Failed to parse the {sourceDescription} '{settingsPath}'.");
|
this.logger.LogError(e, $"Failed to parse the {sourceDescription} '{settingsPath}'.");
|
||||||
return null;
|
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)
|
private void PrepareLoadedSettings(Data settingsData)
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
@ -243,9 +284,9 @@ public sealed class SettingsManager
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.settingsWriteBlocked)
|
if(this.SettingsWriteBlocked)
|
||||||
{
|
{
|
||||||
this.logger.LogWarning("Cannot store settings, because the loaded settings file uses an unknown or unreadable version.");
|
this.logger.LogWarning($"Cannot store settings, because settings writes are blocked. Reason: '{this.SettingsWriteBlockReason}'.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
app/MindWork AI Studio/Settings/SettingsWriteBlockReason.cs
Normal file
11
app/MindWork AI Studio/Settings/SettingsWriteBlockReason.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
namespace AIStudio.Settings;
|
||||||
|
|
||||||
|
public enum SettingsWriteBlockReason
|
||||||
|
{
|
||||||
|
NONE,
|
||||||
|
VERSION_MISSING,
|
||||||
|
VERSION_UNKNOWN,
|
||||||
|
VERSION_NEWER_THAN_APP,
|
||||||
|
FILE_UNREADABLE,
|
||||||
|
CURRENT_VERSION_INVALID,
|
||||||
|
}
|
||||||
@ -7,5 +7,5 @@
|
|||||||
- Changed provider confidence settings to appear in their own settings panel, because they apply to LLM, embedding, and transcription providers.
|
- 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 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 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 and restores them automatically when needed.
|
- 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 them automatically 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.
|
- 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.
|
||||||
Loading…
Reference in New Issue
Block a user