feat: Implement container logs functionality in InstancesViewModel
- Added properties for managing container logs, including log entries, service filters, and auto-refresh options. - Introduced commands for refreshing logs, toggling auto-refresh, and closing the logs panel. - Implemented log fetching logic with error handling and status messages. - Integrated log display in the InstancesView with a dedicated logs panel. feat: Enhance navigation to Instances page with auto-selection - Added method to navigate to the Instances page and auto-select an instance based on abbreviation. feat: Update SettingsViewModel to load and save Bitwarden configuration - Integrated Bitwarden configuration loading from IOptions and saving to appsettings.json. - Added properties for Bitwarden instance project ID and connection status. - Updated UI to reflect Bitwarden settings and connection status. feat: Add advanced options for instance creation - Introduced a new expander in CreateInstanceView for advanced options, including purging stale volumes. feat: Improve InstanceDetailsWindow with pending setup banner - Added a banner to indicate pending setup for Xibo OAuth credentials, with editable fields for client ID and secret. fix: Update appsettings.json to include Bitwarden configuration structure - Added Bitwarden section to appsettings.json for storing configuration values. chore: Update Docker Compose template with health checks - Added health check configuration for web service in template.yml to ensure service availability. refactor: Drop AppSettings table from database - Removed AppSettings table and related migration files as part of database cleanup. feat: Create ServiceLogEntry DTO for log management - Added ServiceLogEntry class to represent individual log entries from Docker services.
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OTSSignsOrchestrator.Core.Configuration;
|
||||
using OTSSignsOrchestrator.Core.Services;
|
||||
|
||||
namespace OTSSignsOrchestrator.Desktop.ViewModels;
|
||||
@@ -65,6 +69,7 @@ public partial class SettingsViewModel : ObservableObject
|
||||
[ObservableProperty] private string _bitwardenAccessToken = string.Empty;
|
||||
[ObservableProperty] private string _bitwardenOrganizationId = string.Empty;
|
||||
[ObservableProperty] private string _bitwardenProjectId = string.Empty;
|
||||
[ObservableProperty] private string _bitwardenInstanceProjectId = string.Empty;
|
||||
|
||||
// ── Xibo Bootstrap OAuth2 ─────────────────────────────────────
|
||||
[ObservableProperty] private string _xiboBootstrapClientId = string.Empty;
|
||||
@@ -76,12 +81,34 @@ public partial class SettingsViewModel : ObservableObject
|
||||
_ = LoadAsync();
|
||||
}
|
||||
|
||||
/// <summary>Whether Bitwarden is configured and reachable.</summary>
|
||||
[ObservableProperty] private bool _isBitwardenConfigured;
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
// ── Load Bitwarden bootstrap config from IOptions<BitwardenOptions> ──
|
||||
var bwOptions = _services.GetRequiredService<IOptions<BitwardenOptions>>().Value;
|
||||
BitwardenIdentityUrl = bwOptions.IdentityUrl;
|
||||
BitwardenApiUrl = bwOptions.ApiUrl;
|
||||
BitwardenAccessToken = bwOptions.AccessToken;
|
||||
BitwardenOrganizationId = bwOptions.OrganizationId;
|
||||
BitwardenProjectId = bwOptions.ProjectId;
|
||||
BitwardenInstanceProjectId = bwOptions.InstanceProjectId;
|
||||
|
||||
IsBitwardenConfigured = !string.IsNullOrWhiteSpace(bwOptions.AccessToken)
|
||||
&& !string.IsNullOrWhiteSpace(bwOptions.OrganizationId);
|
||||
|
||||
if (!IsBitwardenConfigured)
|
||||
{
|
||||
StatusMessage = "Bitwarden is not configured. Fill in the Bitwarden section and save to get started.";
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Load all other settings from Bitwarden ──
|
||||
using var scope = _services.CreateScope();
|
||||
var svc = scope.ServiceProvider.GetRequiredService<SettingsService>();
|
||||
|
||||
@@ -127,18 +154,11 @@ public partial class SettingsViewModel : ObservableObject
|
||||
DefaultPhpUploadMaxFilesize = await svc.GetAsync(SettingsService.DefaultPhpUploadMaxFilesize, "10G");
|
||||
DefaultPhpMaxExecutionTime = await svc.GetAsync(SettingsService.DefaultPhpMaxExecutionTime, "600");
|
||||
|
||||
// Bitwarden
|
||||
BitwardenIdentityUrl = await svc.GetAsync(SettingsService.BitwardenIdentityUrl, "https://identity.bitwarden.com");
|
||||
BitwardenApiUrl = await svc.GetAsync(SettingsService.BitwardenApiUrl, "https://api.bitwarden.com");
|
||||
BitwardenAccessToken = await svc.GetAsync(SettingsService.BitwardenAccessToken, string.Empty);
|
||||
BitwardenOrganizationId = await svc.GetAsync(SettingsService.BitwardenOrganizationId, string.Empty);
|
||||
BitwardenProjectId = await svc.GetAsync(SettingsService.BitwardenProjectId, string.Empty);
|
||||
|
||||
// Xibo Bootstrap
|
||||
XiboBootstrapClientId = await svc.GetAsync(SettingsService.XiboBootstrapClientId, string.Empty);
|
||||
XiboBootstrapClientSecret = await svc.GetAsync(SettingsService.XiboBootstrapClientSecret, string.Empty);
|
||||
|
||||
StatusMessage = "Settings loaded.";
|
||||
StatusMessage = "Settings loaded from Bitwarden.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -156,8 +176,23 @@ public partial class SettingsViewModel : ObservableObject
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
// ── 1. Save Bitwarden bootstrap config to appsettings.json ──
|
||||
await SaveBitwardenConfigToFileAsync();
|
||||
|
||||
// Check if Bitwarden is now configured
|
||||
IsBitwardenConfigured = !string.IsNullOrWhiteSpace(BitwardenAccessToken)
|
||||
&& !string.IsNullOrWhiteSpace(BitwardenOrganizationId);
|
||||
|
||||
if (!IsBitwardenConfigured)
|
||||
{
|
||||
StatusMessage = "Bitwarden config saved to appsettings.json. Fill in Access Token and Org ID to enable all settings.";
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 2. Save all other settings to Bitwarden ──
|
||||
using var scope = _services.CreateScope();
|
||||
var svc = scope.ServiceProvider.GetRequiredService<SettingsService>();
|
||||
svc.InvalidateCache(); // force re-read after config change
|
||||
|
||||
var settings = new List<(string Key, string? Value, string Category, bool IsSensitive)>
|
||||
{
|
||||
@@ -203,20 +238,13 @@ public partial class SettingsViewModel : ObservableObject
|
||||
(SettingsService.DefaultPhpUploadMaxFilesize, DefaultPhpUploadMaxFilesize, SettingsService.CatDefaults, false),
|
||||
(SettingsService.DefaultPhpMaxExecutionTime, DefaultPhpMaxExecutionTime, SettingsService.CatDefaults, false),
|
||||
|
||||
// Bitwarden
|
||||
(SettingsService.BitwardenIdentityUrl, NullIfEmpty(BitwardenIdentityUrl), SettingsService.CatBitwarden, false),
|
||||
(SettingsService.BitwardenApiUrl, NullIfEmpty(BitwardenApiUrl), SettingsService.CatBitwarden, false),
|
||||
(SettingsService.BitwardenAccessToken, NullIfEmpty(BitwardenAccessToken), SettingsService.CatBitwarden, true),
|
||||
(SettingsService.BitwardenOrganizationId, NullIfEmpty(BitwardenOrganizationId), SettingsService.CatBitwarden, false),
|
||||
(SettingsService.BitwardenProjectId, NullIfEmpty(BitwardenProjectId), SettingsService.CatBitwarden, false),
|
||||
|
||||
// Xibo Bootstrap
|
||||
(SettingsService.XiboBootstrapClientId, NullIfEmpty(XiboBootstrapClientId), SettingsService.CatXibo, false),
|
||||
(SettingsService.XiboBootstrapClientSecret, NullIfEmpty(XiboBootstrapClientSecret), SettingsService.CatXibo, true),
|
||||
};
|
||||
|
||||
await svc.SaveManyAsync(settings);
|
||||
StatusMessage = "Settings saved successfully.";
|
||||
StatusMessage = "Settings saved to Bitwarden.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -238,16 +266,21 @@ public partial class SettingsViewModel : ObservableObject
|
||||
}
|
||||
|
||||
IsBusy = true;
|
||||
StatusMessage = "Testing Bitwarden Secrets Manager connection...";
|
||||
StatusMessage = "Saving Bitwarden config and testing connection...";
|
||||
try
|
||||
{
|
||||
// Save to appsettings.json first so the service picks up fresh values
|
||||
await SaveBitwardenConfigToFileAsync();
|
||||
|
||||
using var scope = _services.CreateScope();
|
||||
var bws = scope.ServiceProvider.GetRequiredService<IBitwardenSecretService>();
|
||||
var secrets = await bws.ListSecretsAsync();
|
||||
IsBitwardenConfigured = true;
|
||||
StatusMessage = $"Bitwarden connected. Found {secrets.Count} secret(s) in project.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IsBitwardenConfigured = false;
|
||||
StatusMessage = $"Bitwarden connection failed: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
@@ -324,6 +357,33 @@ public partial class SettingsViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists Bitwarden bootstrap credentials to appsettings.json so they survive restarts.
|
||||
/// </summary>
|
||||
private async Task SaveBitwardenConfigToFileAsync()
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
|
||||
var json = await File.ReadAllTextAsync(path);
|
||||
var doc = JsonNode.Parse(json, documentOptions: new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip })!;
|
||||
|
||||
var bw = doc["Bitwarden"]?.AsObject();
|
||||
if (bw == null)
|
||||
{
|
||||
bw = new JsonObject();
|
||||
doc.AsObject()["Bitwarden"] = bw;
|
||||
}
|
||||
|
||||
bw["IdentityUrl"] = BitwardenIdentityUrl;
|
||||
bw["ApiUrl"] = BitwardenApiUrl;
|
||||
bw["AccessToken"] = BitwardenAccessToken;
|
||||
bw["OrganizationId"] = BitwardenOrganizationId;
|
||||
bw["ProjectId"] = BitwardenProjectId;
|
||||
bw["InstanceProjectId"] = BitwardenInstanceProjectId;
|
||||
|
||||
var options = new JsonSerializerOptions { WriteIndented = true };
|
||||
await File.WriteAllTextAsync(path, doc.ToJsonString(options));
|
||||
}
|
||||
|
||||
private static string? NullIfEmpty(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user