diff --git a/.template-cache/2dc03e2b2b45fef3 b/.template-cache/2dc03e2b2b45fef3
index eaf06cf..0cc0da3 160000
--- a/.template-cache/2dc03e2b2b45fef3
+++ b/.template-cache/2dc03e2b2b45fef3
@@ -1 +1 @@
-Subproject commit eaf06cf6247b58eb638f4d2ebc774f607b3d2fd7
+Subproject commit 0cc0da3c6e596f18c5697f2eb11450d72a18f7c3
diff --git a/OTSSignsOrchestrator.Core/Models/DTOs/AuthentikGroupItem.cs b/OTSSignsOrchestrator.Core/Models/DTOs/AuthentikGroupItem.cs
new file mode 100644
index 0000000..6a9c94b
--- /dev/null
+++ b/OTSSignsOrchestrator.Core/Models/DTOs/AuthentikGroupItem.cs
@@ -0,0 +1,18 @@
+namespace OTSSignsOrchestrator.Core.Models.DTOs;
+
+///
+/// Represents an Authentik group for display and sync operations.
+///
+public class AuthentikGroupItem
+{
+ public string Pk { get; set; } = string.Empty;
+ public string Name { get; set; } = string.Empty;
+ public int MemberCount { get; set; }
+
+ /// Display text for UI: "Group Name (N members)".
+ public string DisplayText => MemberCount > 0
+ ? $"{Name} ({MemberCount} member{(MemberCount == 1 ? "" : "s")})"
+ : Name;
+
+ public override string ToString() => DisplayText;
+}
diff --git a/OTSSignsOrchestrator.Core/Services/AuthentikService.cs b/OTSSignsOrchestrator.Core/Services/AuthentikService.cs
index 8ea6b22..c92a111 100644
--- a/OTSSignsOrchestrator.Core/Services/AuthentikService.cs
+++ b/OTSSignsOrchestrator.Core/Services/AuthentikService.cs
@@ -95,7 +95,7 @@ public class AuthentikService : IAuthentikService
}
// ── 4. Create application linked to provider ──────────────────
- await CreateApplicationAsync(client, baseUrl, instanceAbbrev, slug, providerId, ct);
+ await CreateApplicationAsync(client, baseUrl, instanceAbbrev, slug, providerId, instanceBaseUrl, ct);
}
// ── 5. Ensure provider has a signing keypair (required for metadata) ──
@@ -175,6 +175,62 @@ public class AuthentikService : IAuthentikService
}).OrderBy(k => k.Name).ToList() ?? new();
}
+ ///
+ public async Task> ListGroupsAsync(
+ string? overrideUrl = null, string? overrideApiKey = null,
+ CancellationToken ct = default)
+ {
+ var (_, client) = await CreateAuthenticatedClientAsync(overrideUrl, overrideApiKey);
+
+ var groups = new List();
+ var nextUrl = "/api/v3/core/groups/?page_size=200";
+
+ while (!string.IsNullOrEmpty(nextUrl))
+ {
+ var resp = await client.GetAsync(nextUrl, ct);
+ resp.EnsureSuccessStatusCode();
+
+ var body = await resp.Content.ReadAsStringAsync(ct);
+ using var doc = JsonDocument.Parse(body);
+ var root = doc.RootElement;
+
+ if (root.TryGetProperty("results", out var results) && results.ValueKind == JsonValueKind.Array)
+ {
+ foreach (var g in results.EnumerateArray())
+ {
+ var pk = g.TryGetProperty("pk", out var pkProp) ? pkProp.GetString() ?? "" : "";
+ var name = g.TryGetProperty("name", out var nProp) ? nProp.GetString() ?? "" : "";
+ var memberCount = g.TryGetProperty("users_obj", out var usersObj) && usersObj.ValueKind == JsonValueKind.Array
+ ? usersObj.GetArrayLength()
+ : (g.TryGetProperty("users", out var users) && users.ValueKind == JsonValueKind.Array
+ ? users.GetArrayLength()
+ : 0);
+
+ // Skip Authentik built-in groups (authentik Admins, etc.)
+ if (!string.IsNullOrEmpty(name) && !name.StartsWith("authentik ", StringComparison.OrdinalIgnoreCase))
+ {
+ groups.Add(new AuthentikGroupItem
+ {
+ Pk = pk,
+ Name = name,
+ MemberCount = memberCount,
+ });
+ }
+ }
+ }
+
+ // Handle pagination
+ nextUrl = root.TryGetProperty("pagination", out var pagination) &&
+ pagination.TryGetProperty("next", out var nextProp) &&
+ nextProp.ValueKind == JsonValueKind.Number
+ ? $"/api/v3/core/groups/?page_size=200&page={nextProp.GetInt32()}"
+ : null;
+ }
+
+ _logger.LogInformation("[Authentik] Found {Count} group(s)", groups.Count);
+ return groups.OrderBy(g => g.Name).ToList();
+ }
+
// ─────────────────────────────────────────────────────────────────────────
// HTTP client setup
// ─────────────────────────────────────────────────────────────────────────
@@ -640,7 +696,7 @@ public class AuthentikService : IAuthentikService
private async Task CreateApplicationAsync(
HttpClient client, string baseUrl,
string abbrev, string slug, int providerId,
- CancellationToken ct)
+ string instanceBaseUrl, CancellationToken ct)
{
_logger.LogInformation("[Authentik] Creating application '{Slug}' linked to provider {ProviderId}", slug, providerId);
@@ -649,6 +705,7 @@ public class AuthentikService : IAuthentikService
["name"] = $"OTS Signs — {abbrev.ToUpperInvariant()}",
["slug"] = slug,
["provider"] = providerId,
+ ["meta_launch_url"] = instanceBaseUrl.TrimEnd('/'),
};
var jsonBody = JsonSerializer.Serialize(payload);
diff --git a/OTSSignsOrchestrator.Core/Services/IAuthentikService.cs b/OTSSignsOrchestrator.Core/Services/IAuthentikService.cs
index fbb84f7..f98fb1d 100644
--- a/OTSSignsOrchestrator.Core/Services/IAuthentikService.cs
+++ b/OTSSignsOrchestrator.Core/Services/IAuthentikService.cs
@@ -42,4 +42,13 @@ public interface IAuthentikService
string? overrideUrl = null,
string? overrideApiKey = null,
CancellationToken ct = default);
+
+ ///
+ /// Returns all groups from Authentik, optionally filtered to those with
+ /// at least one member. Used for syncing groups to Xibo instances.
+ ///
+ Task> ListGroupsAsync(
+ string? overrideUrl = null,
+ string? overrideApiKey = null,
+ CancellationToken ct = default);
}
diff --git a/OTSSignsOrchestrator.Core/Services/PostInstanceInitService.cs b/OTSSignsOrchestrator.Core/Services/PostInstanceInitService.cs
index eea95d2..1bf5d27 100644
--- a/OTSSignsOrchestrator.Core/Services/PostInstanceInitService.cs
+++ b/OTSSignsOrchestrator.Core/Services/PostInstanceInitService.cs
@@ -460,6 +460,11 @@ public class PostInstanceInitService
samlConfig != null
? $" (Authentik provider={samlConfig.ProviderId})"
: " (without Authentik — needs manual IdP config)");
+
+ // ── 5. Sync Authentik groups to Xibo ──────────────────────────────
+ // Pre-create Authentik groups as Xibo user groups so they're available
+ // immediately (before any user logs in via SSO).
+ await SyncGroupsFromAuthentikAsync(abbrev, instanceUrl, settings, ct);
}
catch (Exception ex)
{
@@ -469,6 +474,89 @@ public class PostInstanceInitService
}
}
+ ///
+ /// Fetches all groups from Authentik and creates matching user groups in the
+ /// specified Xibo instance. Groups that already exist in Xibo are skipped.
+ /// This ensures that groups are available in Xibo for permission assignment
+ /// before any user logs in via SAML SSO.
+ ///
+ public async Task SyncGroupsFromAuthentikAsync(
+ string abbrev,
+ string instanceUrl,
+ SettingsService settings,
+ CancellationToken ct = default)
+ {
+ var synced = 0;
+ try
+ {
+ _logger.LogInformation("[GroupSync] Syncing Authentik groups to Xibo for {Abbrev}", abbrev);
+
+ using var scope = _services.CreateScope();
+ var authentik = scope.ServiceProvider.GetRequiredService();
+ var xibo = scope.ServiceProvider.GetRequiredService();
+
+ // ── 1. Fetch groups from Authentik ────────────────────────────────
+ var authentikGroups = await authentik.ListGroupsAsync(ct: ct);
+ if (authentikGroups.Count == 0)
+ {
+ _logger.LogInformation("[GroupSync] No groups found in Authentik — nothing to sync");
+ return 0;
+ }
+
+ _logger.LogInformation("[GroupSync] Found {Count} Authentik group(s) to sync", authentikGroups.Count);
+
+ // ── 2. Authenticate to Xibo ───────────────────────────────────────
+ var oauthClientId = await settings.GetAsync(SettingsService.InstanceOAuthClientId(abbrev));
+ var oauthSecretId = await settings.GetAsync(SettingsService.InstanceOAuthSecretId(abbrev));
+
+ if (string.IsNullOrWhiteSpace(oauthClientId) || string.IsNullOrWhiteSpace(oauthSecretId))
+ {
+ _logger.LogWarning("[GroupSync] No OAuth credentials for {Abbrev} — cannot sync groups", abbrev);
+ return 0;
+ }
+
+ var bws = scope.ServiceProvider.GetRequiredService();
+ var oauthSecret = await bws.GetSecretAsync(oauthSecretId);
+ var accessToken = await xibo.LoginAsync(instanceUrl, oauthClientId, oauthSecret.Value);
+
+ // ── 3. List existing Xibo groups ──────────────────────────────────
+ var existingGroups = await xibo.ListUserGroupsAsync(instanceUrl, accessToken);
+ var existingNames = new HashSet(
+ existingGroups.Select(g => g.Group),
+ StringComparer.OrdinalIgnoreCase);
+
+ // ── 4. Create missing groups in Xibo ──────────────────────────────
+ foreach (var group in authentikGroups)
+ {
+ if (existingNames.Contains(group.Name))
+ {
+ _logger.LogDebug("[GroupSync] Group '{Name}' already exists in Xibo", group.Name);
+ continue;
+ }
+
+ try
+ {
+ var groupId = await xibo.CreateUserGroupAsync(instanceUrl, accessToken, group.Name);
+ _logger.LogInformation("[GroupSync] Created Xibo group '{Name}' (id={Id})", group.Name, groupId);
+ synced++;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "[GroupSync] Failed to create Xibo group '{Name}'", group.Name);
+ }
+ }
+
+ _logger.LogInformation("[GroupSync] Sync complete for {Abbrev}: {Synced} group(s) created", abbrev, synced);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "[GroupSync] Group sync failed for {Abbrev}: {Message}", abbrev, ex.Message);
+ // Don't rethrow — group sync failure should not block other operations
+ }
+
+ return synced;
+ }
+
// ─────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────
diff --git a/OTSSignsOrchestrator.Core/Services/XiboApiService.cs b/OTSSignsOrchestrator.Core/Services/XiboApiService.cs
index 5e0a078..b37cb10 100644
--- a/OTSSignsOrchestrator.Core/Services/XiboApiService.cs
+++ b/OTSSignsOrchestrator.Core/Services/XiboApiService.cs
@@ -321,6 +321,60 @@ public class XiboApiService
// User groups
// ─────────────────────────────────────────────────────────────────────────
+ ///
+ /// Lists all user groups in the Xibo instance and returns their names and IDs.
+ ///
+ public async Task> ListUserGroupsAsync(
+ string instanceUrl,
+ string accessToken)
+ {
+ var client = _httpClientFactory.CreateClient("XiboApi");
+ var baseUrl = instanceUrl.TrimEnd('/');
+
+ SetBearer(client, accessToken);
+
+ var response = await client.GetAsync($"{baseUrl}/api/group");
+ await EnsureSuccessAsync(response, "list Xibo user groups");
+
+ using var doc = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
+ var groups = new List();
+
+ foreach (var el in doc.RootElement.EnumerateArray())
+ {
+ groups.Add(new XiboGroupInfo
+ {
+ GroupId = el.GetProperty("groupId").GetInt32(),
+ Group = el.GetProperty("group").GetString() ?? "",
+ });
+ }
+
+ return groups;
+ }
+
+ ///
+ /// Finds an existing Xibo group by name or creates it if it doesn't exist.
+ /// Returns the group ID.
+ ///
+ public async Task GetOrCreateUserGroupAsync(
+ string instanceUrl,
+ string accessToken,
+ string groupName)
+ {
+ // Try to find existing group first
+ var existing = await ListUserGroupsAsync(instanceUrl, accessToken);
+ var match = existing.FirstOrDefault(g =>
+ string.Equals(g.Group, groupName, StringComparison.OrdinalIgnoreCase));
+
+ if (match != null)
+ {
+ _logger.LogDebug("Xibo group '{Name}' already exists (id={Id})", groupName, match.GroupId);
+ return match.GroupId;
+ }
+
+ // Create new group
+ return await CreateUserGroupAsync(instanceUrl, accessToken, groupName);
+ }
+
///
/// Creates a new user group and returns its numeric group ID.
///
@@ -529,6 +583,12 @@ public class XiboTestResult
public int HttpStatus { get; set; }
}
+public class XiboGroupInfo
+{
+ public int GroupId { get; set; }
+ public string Group { get; set; } = string.Empty;
+}
+
public class XiboAuthException : Exception
{
public int HttpStatus { get; }
diff --git a/OTSSignsOrchestrator.Desktop/ViewModels/InstanceDetailsViewModel.cs b/OTSSignsOrchestrator.Desktop/ViewModels/InstanceDetailsViewModel.cs
index 32955a1..e85c59b 100644
--- a/OTSSignsOrchestrator.Desktop/ViewModels/InstanceDetailsViewModel.cs
+++ b/OTSSignsOrchestrator.Desktop/ViewModels/InstanceDetailsViewModel.cs
@@ -2,6 +2,7 @@ using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.DependencyInjection;
+using OTSSignsOrchestrator.Core.Models.DTOs;
using OTSSignsOrchestrator.Core.Models.Entities;
using OTSSignsOrchestrator.Core.Services;
using OTSSignsOrchestrator.Desktop.Models;
@@ -49,7 +50,15 @@ public partial class InstanceDetailsViewModel : ObservableObject
[ObservableProperty] private bool _isPendingSetup;
[ObservableProperty] private string _initClientId = string.Empty;
[ObservableProperty] private string _initClientSecret = string.Empty;
+ // ── Services (for restart) ─────────────────────────────────────────────────────
+ [ObservableProperty] private ObservableCollection _stackServices = new();
+ [ObservableProperty] private bool _isLoadingServices;
+ ///
+ /// Callback the View wires up to show a confirmation dialog.
+ /// Parameters: (title, message) → returns true if the user confirmed.
+ ///
+ public Func>? ConfirmAsync { get; set; }
// Cached instance — needed by InitializeCommand to reload after setup
private LiveStackItem? _currentInstance;
public InstanceDetailsViewModel(IServiceProvider services)
@@ -111,6 +120,9 @@ public partial class InstanceDetailsViewModel : ObservableObject
InitClientId = string.Empty;
InitClientSecret = string.Empty;
}
+
+ // ── Load stack services ───────────────────────────────────────
+ await LoadServicesAsync();
}
catch (Exception ex)
{
@@ -213,6 +225,64 @@ public partial class InstanceDetailsViewModel : ObservableObject
// Rotation
// ─────────────────────────────────────────────────────────────────────────
+ [RelayCommand]
+ private async Task RestartServiceAsync(ServiceInfo? service)
+ {
+ if (service is null || _currentInstance is null) return;
+
+ if (ConfirmAsync is not null)
+ {
+ var confirmed = await ConfirmAsync(
+ "Restart Service",
+ $"Are you sure you want to restart '{service.Name}'?\n\nThis will force-update the service, causing its tasks to be recreated.");
+ if (!confirmed) return;
+ }
+
+ IsBusy = true;
+ StatusMessage = $"Restarting service '{service.Name}'...";
+ try
+ {
+ var dockerCli = _services.GetRequiredService();
+ var ok = await dockerCli.ForceUpdateServiceAsync(service.Name);
+ StatusMessage = ok
+ ? $"Service '{service.Name}' restarted successfully."
+ : $"Failed to restart service '{service.Name}'.";
+
+ // Refresh service list to show updated replica status
+ await LoadServicesAsync();
+ }
+ catch (Exception ex)
+ {
+ StatusMessage = $"Error restarting service: {ex.Message}";
+ }
+ finally
+ {
+ IsBusy = false;
+ }
+ }
+
+ private async Task LoadServicesAsync()
+ {
+ if (_currentInstance is null) return;
+
+ IsLoadingServices = true;
+ try
+ {
+ var dockerCli = _services.GetRequiredService();
+ dockerCli.SetHost(_currentInstance.Host);
+ var services = await dockerCli.InspectStackServicesAsync(_currentInstance.StackName);
+ StackServices = new ObservableCollection(services);
+ }
+ catch (Exception ex)
+ {
+ StatusMessage = $"Error loading services: {ex.Message}";
+ }
+ finally
+ {
+ IsLoadingServices = false;
+ }
+ }
+
[RelayCommand]
private async Task RotateAdminPasswordAsync()
{
diff --git a/OTSSignsOrchestrator.Desktop/ViewModels/InstancesViewModel.cs b/OTSSignsOrchestrator.Desktop/ViewModels/InstancesViewModel.cs
index 19dc11e..46a739d 100644
--- a/OTSSignsOrchestrator.Desktop/ViewModels/InstancesViewModel.cs
+++ b/OTSSignsOrchestrator.Desktop/ViewModels/InstancesViewModel.cs
@@ -48,6 +48,12 @@ public partial class InstancesViewModel : ObservableObject
/// Raised when the instance details modal should be opened for the given ViewModel.
public event Action? OpenDetailsRequested;
+ ///
+ /// Callback the View wires up to show a confirmation dialog.
+ /// Parameters: (title, message) → returns true if the user confirmed.
+ ///
+ public Func>? ConfirmAsync { get; set; }
+
private string? _pendingSelectAbbrev;
public InstancesViewModel(IServiceProvider services)
@@ -245,6 +251,79 @@ public partial class InstancesViewModel : ObservableObject
_logRefreshTimer = null;
}
+ // ── Restart Commands ────────────────────────────────────────────────
+
+ [RelayCommand]
+ private async Task RestartStackAsync()
+ {
+ if (SelectedInstance == null) return;
+
+ if (ConfirmAsync is not null)
+ {
+ var confirmed = await ConfirmAsync(
+ "Restart Stack",
+ $"Are you sure you want to restart all services in '{SelectedInstance.StackName}'?\n\nThis will force-update every service in the stack, causing brief downtime.");
+ if (!confirmed) return;
+ }
+
+ IsBusy = true;
+ StatusMessage = $"Restarting all services in '{SelectedInstance.StackName}'...";
+ try
+ {
+ var dockerCli = _services.GetRequiredService();
+ dockerCli.SetHost(SelectedInstance.Host);
+
+ var services = await dockerCli.InspectStackServicesAsync(SelectedInstance.StackName);
+ var failures = new List();
+
+ for (var i = 0; i < services.Count; i++)
+ {
+ var svc = services[i];
+ StatusMessage = $"Restarting service {i + 1}/{services.Count}: {svc.Name}...";
+ var ok = await dockerCli.ForceUpdateServiceAsync(svc.Name);
+ if (!ok) failures.Add(svc.Name);
+ }
+
+ StatusMessage = failures.Count == 0
+ ? $"All {services.Count} service(s) in '{SelectedInstance.StackName}' restarted successfully."
+ : $"Restarted with errors — failed services: {string.Join(", ", failures)}";
+ }
+ catch (Exception ex) { StatusMessage = $"Error restarting stack: {ex.Message}"; }
+ finally { IsBusy = false; }
+ }
+
+ [RelayCommand]
+ private async Task RestartServiceAsync(ServiceInfo? service)
+ {
+ if (service is null || SelectedInstance is null) return;
+
+ if (ConfirmAsync is not null)
+ {
+ var confirmed = await ConfirmAsync(
+ "Restart Service",
+ $"Are you sure you want to restart '{service.Name}'?\n\nThis will force-update the service, causing its tasks to be recreated.");
+ if (!confirmed) return;
+ }
+
+ IsBusy = true;
+ StatusMessage = $"Restarting service '{service.Name}'...";
+ try
+ {
+ var dockerCli = _services.GetRequiredService();
+ dockerCli.SetHost(SelectedInstance.Host);
+ var ok = await dockerCli.ForceUpdateServiceAsync(service.Name);
+ StatusMessage = ok
+ ? $"Service '{service.Name}' restarted successfully."
+ : $"Failed to restart service '{service.Name}'.";
+
+ // Refresh services to show updated replica status
+ var services = await dockerCli.InspectStackServicesAsync(SelectedInstance.StackName);
+ SelectedServices = new ObservableCollection(services);
+ }
+ catch (Exception ex) { StatusMessage = $"Error restarting service: {ex.Message}"; }
+ finally { IsBusy = false; }
+ }
+
[RelayCommand]
private async Task DeleteInstanceAsync()
{
diff --git a/OTSSignsOrchestrator.Desktop/ViewModels/SettingsViewModel.cs b/OTSSignsOrchestrator.Desktop/ViewModels/SettingsViewModel.cs
index e8add32..2ea0d1b 100644
--- a/OTSSignsOrchestrator.Desktop/ViewModels/SettingsViewModel.cs
+++ b/OTSSignsOrchestrator.Desktop/ViewModels/SettingsViewModel.cs
@@ -135,6 +135,7 @@ public partial class SettingsViewModel : ObservableObject
// ── Load all other settings from Bitwarden ──
using var scope = _services.CreateScope();
var svc = scope.ServiceProvider.GetRequiredService();
+ svc.InvalidateCache();
// Git
GitRepoUrl = await svc.GetAsync(SettingsService.GitRepoUrl, string.Empty);
diff --git a/OTSSignsOrchestrator.Desktop/Views/ConfirmationDialog.axaml b/OTSSignsOrchestrator.Desktop/Views/ConfirmationDialog.axaml
new file mode 100644
index 0000000..3590d0a
--- /dev/null
+++ b/OTSSignsOrchestrator.Desktop/Views/ConfirmationDialog.axaml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTSSignsOrchestrator.Desktop/Views/ConfirmationDialog.axaml.cs b/OTSSignsOrchestrator.Desktop/Views/ConfirmationDialog.axaml.cs
new file mode 100644
index 0000000..b873605
--- /dev/null
+++ b/OTSSignsOrchestrator.Desktop/Views/ConfirmationDialog.axaml.cs
@@ -0,0 +1,50 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace OTSSignsOrchestrator.Desktop.Views;
+
+///
+/// A simple Yes/No confirmation dialog that can be shown modally.
+/// Use for a convenient one-liner.
+///
+public partial class ConfirmationDialog : Window
+{
+ public bool Result { get; private set; }
+
+ public ConfirmationDialog()
+ {
+ InitializeComponent();
+ }
+
+ public ConfirmationDialog(string title, string message) : this()
+ {
+ TitleText.Text = title;
+ MessageText.Text = message;
+ Title = title;
+
+ ConfirmButton.Click += OnConfirmClicked;
+ CancelButton.Click += OnCancelClicked;
+ }
+
+ private void OnConfirmClicked(object? sender, RoutedEventArgs e)
+ {
+ Result = true;
+ Close();
+ }
+
+ private void OnCancelClicked(object? sender, RoutedEventArgs e)
+ {
+ Result = false;
+ Close();
+ }
+
+ ///
+ /// Shows a modal confirmation dialog and returns true if the user confirmed.
+ ///
+ public static async Task ShowAsync(Window owner, string title, string message)
+ {
+ var dialog = new ConfirmationDialog(title, message);
+ await dialog.ShowDialog(owner);
+ return dialog.Result;
+ }
+}
diff --git a/OTSSignsOrchestrator.Desktop/Views/InstanceDetailsWindow.axaml b/OTSSignsOrchestrator.Desktop/Views/InstanceDetailsWindow.axaml
index a63be92..07c3f49 100644
--- a/OTSSignsOrchestrator.Desktop/Views/InstanceDetailsWindow.axaml
+++ b/OTSSignsOrchestrator.Desktop/Views/InstanceDetailsWindow.axaml
@@ -1,11 +1,13 @@
@@ -186,6 +188,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTSSignsOrchestrator.Desktop/Views/InstanceDetailsWindow.axaml.cs b/OTSSignsOrchestrator.Desktop/Views/InstanceDetailsWindow.axaml.cs
index 15e38b1..cf25362 100644
--- a/OTSSignsOrchestrator.Desktop/Views/InstanceDetailsWindow.axaml.cs
+++ b/OTSSignsOrchestrator.Desktop/Views/InstanceDetailsWindow.axaml.cs
@@ -1,4 +1,5 @@
using Avalonia.Controls;
+using OTSSignsOrchestrator.Desktop.ViewModels;
namespace OTSSignsOrchestrator.Desktop.Views;
@@ -7,5 +8,15 @@ public partial class InstanceDetailsWindow : Window
public InstanceDetailsWindow()
{
InitializeComponent();
+ DataContextChanged += OnDataContextChanged;
+ }
+
+ private void OnDataContextChanged(object? sender, EventArgs e)
+ {
+ if (DataContext is InstanceDetailsViewModel vm)
+ {
+ vm.ConfirmAsync = async (title, message) =>
+ await ConfirmationDialog.ShowAsync(this, title, message);
+ }
}
}
diff --git a/OTSSignsOrchestrator.Desktop/Views/InstancesView.axaml b/OTSSignsOrchestrator.Desktop/Views/InstancesView.axaml
index e2823c9..d9a4aad 100644
--- a/OTSSignsOrchestrator.Desktop/Views/InstancesView.axaml
+++ b/OTSSignsOrchestrator.Desktop/Views/InstancesView.axaml
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:OTSSignsOrchestrator.Desktop.ViewModels"
xmlns:dto="using:OTSSignsOrchestrator.Core.Models.DTOs"
+ xmlns:svc="using:OTSSignsOrchestrator.Core.Services"
x:Class="OTSSignsOrchestrator.Desktop.Views.InstancesView"
x:DataType="vm:InstancesViewModel">
@@ -21,6 +22,9 @@
IsEnabled="{Binding !IsBusy}"
ToolTip.Tip="View credentials and manage this instance." />
+
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/OTSSignsOrchestrator.Desktop/Views/InstancesView.axaml.cs b/OTSSignsOrchestrator.Desktop/Views/InstancesView.axaml.cs
index e2d7300..41a422a 100644
--- a/OTSSignsOrchestrator.Desktop/Views/InstancesView.axaml.cs
+++ b/OTSSignsOrchestrator.Desktop/Views/InstancesView.axaml.cs
@@ -21,7 +21,17 @@ public partial class InstancesView : UserControl
_vm = DataContext as InstancesViewModel;
if (_vm is not null)
+ {
_vm.OpenDetailsRequested += OnOpenDetailsRequested;
+ _vm.ConfirmAsync = ShowConfirmationAsync;
+ }
+ }
+
+ private async Task ShowConfirmationAsync(string title, string message)
+ {
+ var owner = TopLevel.GetTopLevel(this) as Window;
+ if (owner is null) return false;
+ return await ConfirmationDialog.ShowAsync(owner, title, message);
}
private async void OnOpenDetailsRequested(InstanceDetailsViewModel detailsVm)
diff --git a/otssigns-desktop.db-shm b/otssigns-desktop.db-shm
deleted file mode 100644
index 413461e..0000000
Binary files a/otssigns-desktop.db-shm and /dev/null differ
diff --git a/otssigns-desktop.db-wal b/otssigns-desktop.db-wal
deleted file mode 100644
index d5a45f2..0000000
Binary files a/otssigns-desktop.db-wal and /dev/null differ
diff --git a/templates/settings-custom.php.template b/templates/settings-custom.php.template
index 167b1e9..d70f7de 100644
--- a/templates/settings-custom.php.template
+++ b/templates/settings-custom.php.template
@@ -15,8 +15,8 @@ $samlSettings = [
],
'group' => 'Users',
'matchGroups' => [
- 'enabled' => false,
- 'attribute' => null,
+ 'enabled' => true,
+ 'attribute' => 'http://schemas.goauthentik.io/2021/02/saml/groups',
'extractionRegEx' => null,
],
],
diff --git a/templates/template.yml b/templates/template.yml
index 29b3635..222872b 100644
--- a/templates/template.yml
+++ b/templates/template.yml
@@ -38,12 +38,6 @@ services:
- {{ABBREV}}-cms-ca-certs:/var/www/cms/ca-certs
ports:
- "{{HOST_HTTP_PORT}}:80"
- healthcheck:
- test: ["CMD-SHELL", "curl -fsS --max-time 5 http://web:80/about | grep -Eo 'v?[0-9]+(\\.[0-9]+)+' >/dev/null || exit 1"]
- interval: 10s
- timeout: 5s
- retries: 5
- start_period: 30s
networks:
{{ABBREV}}-net:
aliases: