@using RackPeek.Domain.Git @using RackPeek.Domain.Git.UseCases @inject InitRepoUseCase InitRepo @inject CommitAllUseCase CommitAll @inject RestoreAllUseCase RestoreAll @inject PushUseCase PushUseCase @inject PullUseCase PullUseCase @inject AddRemoteUseCase AddRemoteUseCase @inject IGitRepository GitRepo @implements IDisposable @if (_status == GitRepoStatus.NotAvailable) {
@if (_confirmInit) { Enable git tracking? } else { } @if (_errorMessage is not null) { @_errorMessage }
} else {
@* Branch name — clickable to open history *@ @if (!string.IsNullOrEmpty(_branch)) { } @if (_status == GitRepoStatus.Clean) { Saved } else if (_status == GitRepoStatus.Dirty) { @* Save button with dropdown toggle *@
@if (_showDropdown) {
@if (!_confirmDiscard) { } else {
Sure?
}
}
} @* Sync / Remote section *@ @if (!_hasRemote) {
@if (_showAddRemote) { } else { }
} else if (_hasRemote) {
@if (_showSyncDropdown) {
@if (_syncStatus.Error is not null) {
Fetch failed
} else if (_syncStatus.Ahead > 0 || _syncStatus.Behind > 0) {
@if (_syncStatus.Ahead > 0) { ↑@_syncStatus.Ahead ahead } @if (_syncStatus.Ahead > 0 && _syncStatus.Behind > 0) { · } @if (_syncStatus.Behind > 0) { ↓@_syncStatus.Behind behind }
} else {
Up to date
}
}
} @if (_errorMessage is not null) { @_errorMessage }
@* Dropdown backdrop — closes all dropdowns on outside click *@ @if (_showDropdown || _showSyncDropdown) {
} @* Diff Modal *@ @if (_showDiff) {
Changes @_changedFiles.Length file(s)
@if (_changedFiles.Length > 0) {
@foreach (var file in _changedFiles) { var status = file.Length >= 2 ? file[..2].Trim() : "?"; var name = file.Length >= 3 ? file[3..] : file; var color = status switch { "M" => "text-amber-400", "A" or "?" => "text-emerald-400", "D" => "text-red-400", _ => "text-zinc-400" }; @status @name }
}
@((MarkupString)FormatDiff(_diffContent))
} @* History Modal *@ @if (_showHistory) {
History @_branch @_logEntries.Length commits
@if (_logEntries.Length == 0) {
No commits yet.
} else {
@foreach (var entry in _logEntries) {
@entry.Hash @entry.Message
@entry.Author @entry.Date
}
}
} } @code { private GitRepoStatus _status = GitRepoStatus.NotAvailable; private string _branch = string.Empty; private bool _isCommitting; private bool _isRestoring; private bool _confirmDiscard; private bool _showDropdown; private string? _errorMessage; private PeriodicTimer? _timer; private CancellationTokenSource? _cts; private bool _showDiff; private string _diffContent = string.Empty; private string[] _changedFiles = []; private bool _showHistory; private GitLogEntry[] _logEntries = []; private bool _hasRemote; private GitSyncStatus _syncStatus = new(0, 0, false); private bool _showSyncDropdown; private bool _isFetching; private bool _isPushing; private bool _isPulling; private bool _isInitializing; private bool _confirmInit; private bool _showAddRemote; private string _remoteUrl = string.Empty; private bool _isBusy => _isCommitting || _isRestoring || _isSyncing; private bool _isSyncing => _isPushing || _isPulling || _isFetching; protected override async Task OnInitializedAsync() { _status = GitRepo.GetStatus(); if (_status == GitRepoStatus.NotAvailable) return; _branch = GitRepo.GetCurrentBranch(); _hasRemote = GitRepo.HasRemote(); _cts?.Cancel(); _timer?.Dispose(); _cts = new CancellationTokenSource(); _timer = new PeriodicTimer(TimeSpan.FromSeconds(5)); _ = PollStatusAsync(_cts.Token); await Task.CompletedTask; } private async Task PollStatusAsync(CancellationToken ct) { try { while (_timer != null && await _timer.WaitForNextTickAsync(ct)) { if (_isBusy) continue; var newStatus = GitRepo.GetStatus(); if (newStatus != _status) { _status = newStatus; _confirmDiscard = false; _showDropdown = false; await InvokeAsync(StateHasChanged); } } } catch (OperationCanceledException) { } } private async Task InitRepoAsync() { _errorMessage = null; _isInitializing = true; try { var error = await InitRepo.ExecuteAsync(); if (error is not null) { _errorMessage = error; return; } _status = GitRepo.GetStatus(); _branch = GitRepo.GetCurrentBranch(); _hasRemote = GitRepo.HasRemote(); _cts?.Cancel(); _timer?.Dispose(); _cts = new CancellationTokenSource(); _timer = new PeriodicTimer(TimeSpan.FromSeconds(5)); _ = PollStatusAsync(_cts.Token); } catch (Exception ex) { _errorMessage = $"Init error: {ex.Message}"; } finally { _isInitializing = false; } } private void CancelAddRemote() { _showAddRemote = false; _remoteUrl = string.Empty; } private async Task AddRemoteAsync() { _errorMessage = null; try { var error = await AddRemoteUseCase.ExecuteAsync(_remoteUrl); if (error is not null) { _errorMessage = error; return; } _hasRemote = true; _showAddRemote = false; _remoteUrl = string.Empty; } catch (Exception ex) { _errorMessage = $"Remote error: {ex.Message}"; } } private async Task CommitAsync() { _errorMessage = null; _isCommitting = true; _confirmDiscard = false; _showDropdown = false; try { var error = await CommitAll.ExecuteAsync( $"rackpeek: save config {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC"); if (error is not null) _errorMessage = error; _status = GitRepo.GetStatus(); } catch (Exception ex) { _errorMessage = $"Unexpected error: {ex.Message}"; } finally { _isCommitting = false; } } private void ToggleDropdown() { _showDropdown = !_showDropdown; _showSyncDropdown = false; _confirmDiscard = false; } private void ToggleSyncAsync() { if (_showSyncDropdown) { _showSyncDropdown = false; return; } _showDropdown = false; _errorMessage = null; _isFetching = true; _showSyncDropdown = true; try { _syncStatus = GitRepo.FetchAndGetSyncStatus(); if (_syncStatus.Error is not null) _errorMessage = _syncStatus.Error; } catch (Exception ex) { _errorMessage = $"Fetch error: {ex.Message}"; } finally { _isFetching = false; } } private void CloseAllDropdowns() { _showDropdown = false; _showSyncDropdown = false; _confirmDiscard = false; } private void OpenDiffAsync() { _showDropdown = false; _changedFiles = GitRepo.GetChangedFiles(); _diffContent = GitRepo.GetDiff(); _showDiff = true; } private void CloseDiff() { _showDiff = false; _diffContent = string.Empty; _changedFiles = []; } private void ToggleHistoryAsync() { if (_showHistory) { CloseHistory(); return; } _showDropdown = false; _showSyncDropdown = false; _logEntries = GitRepo.GetLog(20); _showHistory = true; } private void CloseHistory() { _showHistory = false; _logEntries = []; } private async Task DiscardAsync() { _errorMessage = null; _isRestoring = true; _confirmDiscard = false; _showDropdown = false; try { var error = await RestoreAll.ExecuteAsync(); if (error is not null) _errorMessage = error; _status = GitRepo.GetStatus(); } catch (Exception ex) { _errorMessage = $"Unexpected error: {ex.Message}"; } finally { _isRestoring = false; } } private async Task PushAsync() { _errorMessage = null; _isPushing = true; try { var error = await PushUseCase.ExecuteAsync(); if (error is not null) _errorMessage = error; _syncStatus = GitRepo.FetchAndGetSyncStatus(); } catch (Exception ex) { _errorMessage = $"Push error: {ex.Message}"; } finally { _isPushing = false; } } private async Task PullAsync() { _errorMessage = null; _isPulling = true; try { var error = await PullUseCase.ExecuteAsync(); if (error is not null) _errorMessage = error; _syncStatus = GitRepo.FetchAndGetSyncStatus(); _status = GitRepo.GetStatus(); } catch (Exception ex) { _errorMessage = $"Pull error: {ex.Message}"; } finally { _isPulling = false; } } private static string FormatDiff(string diff) { if (string.IsNullOrWhiteSpace(diff)) return "No diff available"; var lines = diff.Split('\n'); var sb = new System.Text.StringBuilder(); foreach (var line in lines) { var escaped = System.Net.WebUtility.HtmlEncode(line); if (line.StartsWith('+') && !line.StartsWith("+++")) sb.AppendLine($"{escaped}"); else if (line.StartsWith('-') && !line.StartsWith("---")) sb.AppendLine($"{escaped}"); else if (line.StartsWith("@@")) sb.AppendLine($"{escaped}"); else if (line.StartsWith("diff ")) sb.AppendLine($"{escaped}"); else sb.AppendLine($"{escaped}"); } return sb.ToString(); } public void Dispose() { _cts?.Cancel(); _cts?.Dispose(); _timer?.Dispose(); } }