feat(ui): Online Inbox settings tab + auth-code/PKCE login

New Settings tab: enable toggle, config fields, sign-in/out + status.
OnlineLoginService runs the PKCE loopback flow (Duende.IdentityModel.OidcClient
7.1.0), opens the system browser, captures the callback, hands the refresh
token to the Worker. en/de localized. Fixes: loopback callback URL built from
host:port base (avoids doubled redirect path); PollIntervalSeconds threaded
through the state DTO so it loads instead of resetting to 60.

Visual layout + the live sign-in round-trip need manual verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-10 11:02:14 +02:00
parent 17c7ff517a
commit 80a2de6c74
11 changed files with 415 additions and 3 deletions

View File

@@ -0,0 +1,121 @@
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
public sealed partial class OnlineInboxSettingsViewModel : ViewModelBase
{
private readonly IWorkerClient _worker;
private readonly IOnlineLoginService _loginService;
[ObservableProperty] private bool _enabled;
[ObservableProperty] private string _apiBaseUrl = "";
[ObservableProperty] private string _authority = "";
[ObservableProperty] private string _clientId = "";
[ObservableProperty] private string _scopes = "openid offline_access";
[ObservableProperty] private string _redirectUri = "http://localhost:8765/callback";
[ObservableProperty] private int _pollIntervalSeconds = 60;
[ObservableProperty] private bool _signedIn;
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private string _statusMessage = "";
public OnlineInboxSettingsViewModel(IWorkerClient worker, IOnlineLoginService loginService)
{
_worker = worker;
_loginService = loginService;
}
public async Task LoadAsync()
{
IsBusy = true;
StatusMessage = "";
try
{
var dto = await _worker.GetOnlineInboxStateAsync();
if (dto is null)
{
StatusMessage = Loc.T("vm.onlineInbox.workerOffline");
return;
}
Enabled = dto.Enabled;
ApiBaseUrl = dto.ApiBaseUrl;
Authority = dto.Authority;
ClientId = dto.ClientId;
Scopes = dto.Scopes;
RedirectUri = dto.RedirectUri;
SignedIn = dto.SignedIn;
PollIntervalSeconds = dto.PollIntervalSeconds;
}
finally { IsBusy = false; }
}
[RelayCommand]
private async Task Save()
{
IsBusy = true;
StatusMessage = "";
try
{
await _worker.SetOnlineInboxConfigAsync(new OnlineInboxConfigInputDto(
Enabled,
ApiBaseUrl,
PollIntervalSeconds,
Authority,
ClientId,
Scopes,
RedirectUri));
StatusMessage = Loc.T("vm.onlineInbox.saved");
}
catch (Exception ex)
{
StatusMessage = Loc.T("vm.onlineInbox.saveFailed", ex.Message);
}
finally { IsBusy = false; }
}
[RelayCommand]
private async Task SignIn()
{
IsBusy = true;
StatusMessage = "";
try
{
var result = await _loginService.LoginAsync(Authority, ClientId, Scopes, RedirectUri);
if (!result.Success)
{
StatusMessage = Loc.T("vm.onlineInbox.signInFailed", result.Error ?? "Unknown error");
return;
}
await _worker.SetOnlineInboxAuthAsync(result.RefreshToken!);
SignedIn = true;
StatusMessage = Loc.T("vm.onlineInbox.signedIn");
}
catch (Exception ex)
{
StatusMessage = Loc.T("vm.onlineInbox.signInFailed", ex.Message);
}
finally { IsBusy = false; }
}
[RelayCommand]
private async Task SignOut()
{
IsBusy = true;
StatusMessage = "";
try
{
await _worker.ClearOnlineInboxAuthAsync();
SignedIn = false;
StatusMessage = Loc.T("vm.onlineInbox.signedOut");
}
catch (Exception ex)
{
StatusMessage = Loc.T("vm.onlineInbox.signOutFailed", ex.Message);
}
finally { IsBusy = false; }
}
}