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:
Matt Batchelder
2026-02-25 17:39:17 -05:00
parent a1c987ff21
commit 90eb649940
35 changed files with 1807 additions and 621 deletions

View File

@@ -78,6 +78,20 @@
</Border>
</Expander>
<!-- Advanced options -->
<Expander Header="Advanced options">
<Border Classes="card" Margin="0,8,0,0">
<StackPanel Spacing="8">
<CheckBox IsChecked="{Binding PurgeStaleVolumes}">
<StackPanel Spacing="2">
<TextBlock Text="Purge stale volumes before deploying" FontSize="12" />
<TextBlock Text="Removes existing Docker volumes for this stack so fresh volumes are created. Only needed if volumes were created with wrong settings." FontSize="11" Foreground="{StaticResource TextMutedBrush}" TextWrapping="Wrap" />
</StackPanel>
</CheckBox>
</StackPanel>
</Border>
</Expander>
<!-- Deploy button + progress -->
<Button Content="Deploy Instance"
Classes="accent"

View File

@@ -31,6 +31,22 @@
<ScrollViewer>
<StackPanel Spacing="16">
<!-- ═══ Pending Setup Banner ═══ -->
<Border IsVisible="{Binding IsPendingSetup}"
Background="#1F2A1A" BorderBrush="#4ADE80" BorderThickness="1"
CornerRadius="8" Padding="14,10">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="⚙" FontSize="18" VerticalAlignment="Center" Foreground="#4ADE80" />
<StackPanel>
<TextBlock Text="Pending Setup" FontSize="14" FontWeight="SemiBold"
Foreground="#4ADE80" />
<TextBlock FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"
TextWrapping="Wrap"
Text="Enter your Xibo OAuth credentials below to complete instance initialisation." />
</StackPanel>
</StackPanel>
</Border>
<!-- ═══ OTS Admin Account ═══ -->
<Border Classes="card">
<StackPanel Spacing="8">
@@ -116,27 +132,57 @@
FontSize="12" Foreground="{StaticResource TextMutedBrush}" Margin="0,0,0,6"
TextWrapping="Wrap" />
<TextBlock Text="Client ID" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0" Text="{Binding OAuthClientId}" IsReadOnly="True"
FontFamily="Consolas,monospace" />
<Button Grid.Column="1" Content="Copy" Margin="4,0,0,0"
Command="{Binding CopyOAuthClientIdCommand}" />
</Grid>
<!-- ── Pending: editable credential input ── -->
<StackPanel Spacing="8" IsVisible="{Binding IsPendingSetup}">
<TextBlock FontSize="12" Foreground="{StaticResource TextSecondaryBrush}"
TextWrapping="Wrap"
Text="Log into the Xibo CMS as xibo_admin (password: password), go to Administration → Applications, create a client_credentials app, then paste the credentials here." />
<TextBlock Text="Client Secret" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
<TextBox Grid.Column="0" Text="{Binding OAuthSecretDisplay}" IsReadOnly="True"
FontFamily="Consolas,monospace" />
<Button Grid.Column="1" Content="Show" Margin="4,0,0,0"
IsVisible="{Binding !OAuthSecretVisible}"
Command="{Binding ToggleOAuthSecretVisibilityCommand}" />
<Button Grid.Column="2" Content="Hide" Margin="4,0,0,0"
IsVisible="{Binding OAuthSecretVisible}"
Command="{Binding ToggleOAuthSecretVisibilityCommand}" />
<Button Grid.Column="3" Content="Copy" Margin="4,0,0,0"
Command="{Binding CopyOAuthSecretCommand}" />
</Grid>
<TextBlock Text="Client ID" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
<TextBox Text="{Binding InitClientId}" Watermark="OAuth2 Client ID" />
<TextBlock Text="Client Secret" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding InitClientSecret}" PasswordChar="●"
Watermark="(paste from Xibo Applications page)" />
<Button Content="Initialize Instance"
Command="{Binding InitializeCommand}"
IsEnabled="{Binding !IsBusy}"
Classes="accent"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Padding="14,10" FontSize="14"
Margin="0,8,0,0" />
</StackPanel>
<!-- ── Initialized: read-only display ── -->
<StackPanel Spacing="8" IsVisible="{Binding !IsPendingSetup}">
<TextBlock Text="Client ID" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" />
<Grid ColumnDefinitions="*,Auto">
<TextBox Grid.Column="0" Text="{Binding OAuthClientId}" IsReadOnly="True"
FontFamily="Consolas,monospace" />
<Button Grid.Column="1" Content="Copy" Margin="4,0,0,0"
Command="{Binding CopyOAuthClientIdCommand}" />
</Grid>
<TextBlock Text="Client Secret" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,4,0,0" />
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
<TextBox Grid.Column="0" Text="{Binding OAuthSecretDisplay}" IsReadOnly="True"
FontFamily="Consolas,monospace" />
<Button Grid.Column="1" Content="Show" Margin="4,0,0,0"
IsVisible="{Binding !OAuthSecretVisible}"
Command="{Binding ToggleOAuthSecretVisibilityCommand}" />
<Button Grid.Column="2" Content="Hide" Margin="4,0,0,0"
IsVisible="{Binding OAuthSecretVisible}"
Command="{Binding ToggleOAuthSecretVisibilityCommand}" />
<Button Grid.Column="3" Content="Copy" Margin="4,0,0,0"
Command="{Binding CopyOAuthSecretCommand}" />
</Grid>
</StackPanel>
</StackPanel>
</Border>

View File

@@ -1,6 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:OTSSignsOrchestrator.Desktop.ViewModels"
xmlns:dto="using:OTSSignsOrchestrator.Core.Models.DTOs"
x:Class="OTSSignsOrchestrator.Desktop.Views.InstancesView"
x:DataType="vm:InstancesViewModel">
@@ -37,45 +38,118 @@
<TextBlock DockPanel.Dock="Bottom" Text="{Binding StatusMessage}" Classes="status"
Margin="0,10,0,0" />
<!-- Services panel (shown when inspecting) -->
<Border DockPanel.Dock="Right" Width="360"
IsVisible="{Binding SelectedServices.Count}"
Classes="card" Margin="16,0,0,0">
<StackPanel Spacing="4">
<TextBlock Text="Stack Services" Classes="sectionTitle"
Foreground="{StaticResource AccentBrush}" Margin="0,0,0,10" />
<ItemsControl ItemsSource="{Binding SelectedServices}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#232336" CornerRadius="6" Padding="12,10" Margin="0,3"
BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="1">
<StackPanel Spacing="3">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" FontSize="13" />
<TextBlock Text="{Binding Image}" FontSize="11"
Foreground="{StaticResource TextMutedBrush}" />
<TextBlock Text="{Binding Replicas, StringFormat='Replicas: {0}'}"
FontSize="11" Foreground="{StaticResource TextSecondaryBrush}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<!-- Main content: split into upper (grid + services) and lower (logs) -->
<Grid RowDefinitions="*,Auto,Auto">
<!-- Instance list -->
<DataGrid ItemsSource="{Binding Instances}"
SelectedItem="{Binding SelectedInstance}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
CanUserResizeColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Stack" Binding="{Binding StackName}" Width="150" />
<DataGridTextColumn Header="Abbrev" Binding="{Binding CustomerAbbrev}" Width="70" />
<DataGridTextColumn Header="Services" Binding="{Binding ServiceCount}" Width="70" />
<DataGridTextColumn Header="Host" Binding="{Binding HostLabel}" Width="150" />
</DataGrid.Columns>
</DataGrid>
<!-- Upper area: instance list + services side panel -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
<!-- Instance list -->
<DataGrid Grid.Column="0"
ItemsSource="{Binding Instances}"
SelectedItem="{Binding SelectedInstance}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
CanUserResizeColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Stack" Binding="{Binding StackName}" Width="150" />
<DataGridTextColumn Header="Abbrev" Binding="{Binding CustomerAbbrev}" Width="70" />
<DataGridTextColumn Header="Services" Binding="{Binding ServiceCount}" Width="70" />
<DataGridTextColumn Header="Host" Binding="{Binding HostLabel}" Width="150" />
</DataGrid.Columns>
</DataGrid>
<!-- Services panel (shown when inspecting) -->
<Border Grid.Column="1" Width="360"
IsVisible="{Binding SelectedServices.Count}"
Classes="card" Margin="16,0,0,0">
<StackPanel Spacing="4">
<TextBlock Text="Stack Services" Classes="sectionTitle"
Foreground="{StaticResource AccentBrush}" Margin="0,0,0,10" />
<ItemsControl ItemsSource="{Binding SelectedServices}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#232336" CornerRadius="6" Padding="12,10" Margin="0,3"
BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="1">
<StackPanel Spacing="3">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" FontSize="13" />
<TextBlock Text="{Binding Image}" FontSize="11"
Foreground="{StaticResource TextMutedBrush}" />
<TextBlock Text="{Binding Replicas, StringFormat='Replicas: {0}'}"
FontSize="11" Foreground="{StaticResource TextSecondaryBrush}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
</Grid>
<!-- Grid splitter between instances and logs -->
<GridSplitter Grid.Row="1" Height="6" HorizontalAlignment="Stretch"
IsVisible="{Binding IsLogsPanelVisible}"
Background="Transparent" />
<!-- Container Logs Panel -->
<Border Grid.Row="2" Classes="card" Margin="0,4,0,0"
IsVisible="{Binding IsLogsPanelVisible}"
MinHeight="180" MaxHeight="400">
<DockPanel>
<!-- Logs toolbar -->
<Grid DockPanel.Dock="Top" ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,8">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8"
VerticalAlignment="Center">
<TextBlock Text="Container Logs" Classes="sectionTitle"
Foreground="{StaticResource AccentBrush}" />
<ComboBox ItemsSource="{Binding LogServiceFilter}"
SelectedItem="{Binding SelectedLogService}"
MinWidth="200" FontSize="12"
ToolTip.Tip="Filter logs by service" />
</StackPanel>
<TextBlock Grid.Column="1" Text="{Binding LogsStatusMessage}"
FontSize="11" Foreground="{StaticResource TextMutedBrush}"
VerticalAlignment="Center" HorizontalAlignment="Center" />
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6"
VerticalAlignment="Center">
<Button Content="Refresh" Command="{Binding RefreshLogsCommand}"
FontSize="11" Padding="8,4"
ToolTip.Tip="Fetch latest logs" />
<ToggleButton IsChecked="{Binding IsLogsAutoRefresh}"
Content="Auto"
FontSize="11" Padding="8,4"
ToolTip.Tip="Toggle auto-refresh (every 5 seconds)"
Command="{Binding ToggleLogsAutoRefreshCommand}" />
<Button Content="✕" Command="{Binding CloseLogsPanelCommand}"
FontSize="11" Padding="6,4"
ToolTip.Tip="Close logs panel" />
</StackPanel>
</Grid>
<!-- Log entries list -->
<Border Background="#1a1a2e" CornerRadius="4" Padding="8"
BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="1">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding LogEntries}"
x:DataType="vm:InstancesViewModel">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="dto:ServiceLogEntry">
<TextBlock Text="{Binding DisplayLine}"
FontFamily="Cascadia Mono,Consolas,Menlo,monospace"
FontSize="11" Padding="0,1"
TextWrapping="NoWrap"
Foreground="#cccccc" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</DockPanel>
</Border>
</Grid>
</DockPanel>
</UserControl>

View File

@@ -29,6 +29,59 @@
<ScrollViewer>
<StackPanel Spacing="16" Margin="0,0,16,16" MaxWidth="820">
<!-- ═══ Bitwarden Secrets Manager (Bootstrap — always shown first) ═══ -->
<Border Classes="card">
<StackPanel Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,0,4">
<Border Width="4" Height="20" CornerRadius="2" Background="#818CF8" />
<TextBlock Text="Bitwarden Secrets Manager" FontSize="16" FontWeight="SemiBold"
Foreground="#818CF8" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="All application settings are stored in Bitwarden. Configure these credentials first — they are saved to appsettings.json on disk."
FontSize="12" Foreground="{StaticResource TextMutedBrush}" Margin="0,0,0,6"
TextWrapping="Wrap" />
<Grid ColumnDefinitions="1*,12,1*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Identity URL" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenIdentityUrl}"
Watermark="https://identity.bitwarden.com" />
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="API URL" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenApiUrl}"
Watermark="https://api.bitwarden.com" />
</StackPanel>
</Grid>
<TextBlock Text="Machine Account Access Token" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenAccessToken}" PasswordChar="●"
Watermark="0.xxxxxxxx.yyyyyyy:zzzzzz" />
<TextBlock Text="Organization ID" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenOrganizationId}"
Watermark="00000000-0000-0000-0000-000000000000" />
<TextBlock Text="Project ID (required — config secrets are stored in this project)" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenProjectId}"
Watermark="00000000-0000-0000-0000-000000000000" />
<TextBlock Text="Instance Project ID (optional — instance secrets like DB passwords go here; falls back to Project ID if empty)" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" TextWrapping="Wrap" />
<TextBox Text="{Binding BitwardenInstanceProjectId}"
Watermark="00000000-0000-0000-0000-000000000000 (leave empty to use default project)" />
<Button Content="Test Bitwarden Connection"
Command="{Binding TestBitwardenConnectionCommand}"
IsEnabled="{Binding !IsBusy}"
Margin="0,6,0,0" />
</StackPanel>
</Border>
<!-- ═══ Remaining settings — disabled until Bitwarden is configured ═══ -->
<StackPanel Spacing="16" IsEnabled="{Binding IsBitwardenConfigured}">
<!-- ═══ Git Repository ═══ -->
<Border Classes="card">
<StackPanel Spacing="8">
@@ -231,51 +284,6 @@
</StackPanel>
</Border>
<!-- ═══ Bitwarden Secrets Manager ═══ -->
<Border Classes="card">
<StackPanel Spacing="8">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,0,0,4">
<Border Width="4" Height="20" CornerRadius="2" Background="#818CF8" />
<TextBlock Text="Bitwarden Secrets Manager" FontSize="16" FontWeight="SemiBold"
Foreground="#818CF8" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="Stores per-instance admin passwords and OAuth2 secrets. Uses a machine account access token."
FontSize="12" Foreground="{StaticResource TextMutedBrush}" Margin="0,0,0,6"
TextWrapping="Wrap" />
<Grid ColumnDefinitions="1*,12,1*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Identity URL" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenIdentityUrl}"
Watermark="https://identity.bitwarden.com" />
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="API URL" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenApiUrl}"
Watermark="https://api.bitwarden.com" />
</StackPanel>
</Grid>
<TextBlock Text="Machine Account Access Token" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenAccessToken}" PasswordChar="●"
Watermark="0.xxxxxxxx.yyyyyyy:zzzzzz" />
<TextBlock Text="Organization ID" FontSize="12" Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenOrganizationId}"
Watermark="00000000-0000-0000-0000-000000000000" />
<TextBlock Text="Project ID (optional — secrets are organized into this project)" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" />
<TextBox Text="{Binding BitwardenProjectId}"
Watermark="00000000-0000-0000-0000-000000000000" />
<Button Content="Test Bitwarden Connection"
Command="{Binding TestBitwardenConnectionCommand}"
IsEnabled="{Binding !IsBusy}"
Margin="0,6,0,0" />
</StackPanel>
</Border>
<!-- ═══ Xibo Bootstrap OAuth2 ═══ -->
<Border Classes="card">
<StackPanel Spacing="8">
@@ -302,6 +310,8 @@
</StackPanel>
</Border>
</StackPanel> <!-- end of IsBitwardenConfigured wrapper -->
</StackPanel>
</ScrollViewer>
</DockPanel>