Extend uploader profiles, filters, and diagnostics
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-13 11:26:04 +01:00
parent 53094b8d36
commit 2089251a92
5 changed files with 324 additions and 6 deletions

View File

@@ -61,6 +61,13 @@
<Border x:Name="AdvancedPanel" Padding="12" Classes="card accent" IsVisible="False">
<StackPanel Spacing="6">
<TextBlock Text="Erweiterte Einstellungen" FontWeight="SemiBold" />
<ToggleSwitch x:Name="SettingsUnlockToggle" Content="Einstellungen entsperren" Checked="SettingsUnlockToggle_Changed" Unchecked="SettingsUnlockToggle_Changed" />
<TextBlock Text="Profile" />
<ComboBox x:Name="ProfilesBox" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Name="LoadProfileButton" Content="Profil laden" Click="LoadProfileButton_Click" Classes="secondary" />
<Button x:Name="SaveProfileButton" Content="Profil speichern" Click="SaveProfileButton_Click" Classes="secondary" />
</StackPanel>
<TextBlock Text="Basis-URL" />
<TextBox x:Name="BaseUrlBox" Watermark="https://fotospiel.app" />
<TextBlock Text="Max. parallele Uploads" />
@@ -75,6 +82,12 @@
<TextBox x:Name="IncludePatternsBox" Watermark="*.jpg;*.jpeg;*.png" />
<TextBlock Text="Dateien ausschliessen (optional)" />
<TextBox x:Name="ExcludePatternsBox" Watermark="*_preview*;*.tmp" />
<TextBlock Text="Antwort-Format (optional)" />
<ComboBox x:Name="ResponseFormatBox" SelectedIndex="0">
<ComboBoxItem Content="Auto" />
<ComboBoxItem Content="JSON" />
<ComboBoxItem Content="XML" />
</ComboBox>
<TextBlock Text="Manuelle Zugangsdaten (optional)" FontWeight="SemiBold" Margin="0,8,0,0" />
<TextBlock Text="Diese Felder ueberschreiben den Verbindungscode." Classes="subtitle" TextWrapping="Wrap" />
<TextBlock Text="Upload-URL" />
@@ -106,9 +119,12 @@
<TextBlock x:Name="EventNameText" Text="Event: —" TextWrapping="Wrap" />
<TextBlock x:Name="BaseUrlText" Text="Basis-URL: —" TextWrapping="Wrap" />
<TextBlock x:Name="VersionText" Text="App-Version: —" />
<TextBlock x:Name="ConnectExpiryText" Text="Verbindungscode: —" TextWrapping="Wrap" />
<TextBlock x:Name="FolderHealthText" Text="Ordner: —" TextWrapping="Wrap" />
<TextBlock x:Name="DiskFreeText" Text="Freier Speicher: —" TextWrapping="Wrap" />
<TextBlock x:Name="LastSeenText" Text="Letzte Datei: —" TextWrapping="Wrap" />
<TextBlock x:Name="LastErrorText" Text="Letzter Fehler: —" TextWrapping="Wrap" />
<Button x:Name="LogCopyButton" Content="Log kopieren" Click="LogCopyButton_Click" Classes="secondary" />
</StackPanel>
</Border>
@@ -128,7 +144,10 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Name="RetryFailedButton" Content="Fehlgeschlagene erneut senden" Click="RetryFailedButton_Click" IsEnabled="False" Classes="secondary" />
<Button x:Name="ClearFailedButton" Content="Fehlerliste leeren" Click="ClearFailedButton_Click" IsEnabled="False" Classes="secondary" />
</StackPanel>
</StackPanel>
</Border>
</StackPanel>

View File

@@ -35,6 +35,7 @@ public partial class MainWindow : Window
private DateTimeOffset? _lastSuccessAt;
private bool _advancedVisible;
private readonly DispatcherTimer _liveTimer = new();
private readonly List<string> _logBuffer = new();
public ObservableCollection<UploadItem> RecentUploads { get; } = new();
@@ -130,12 +131,15 @@ public partial class MainWindow : Window
_settings.Password = response.Data.Password;
_settings.ResponseFormat = response.Data.ResponseFormat;
_settings.EventName = response.Data.EventName;
_settings.ConnectExpiresAt = response.Data.ExpiresAt;
_settingsStore.Save(_settings);
StatusText.Text = "Verbunden. Upload bereit.";
AppendLog("Verbunden mit Event.");
PickFolderButton.IsEnabled = true;
TestUploadButton.IsEnabled = true;
ReconnectButton.IsEnabled = true;
UpdateAdvancedLockState();
UpdateDiagnostics();
StartUploadPipelineIfReady();
}
@@ -177,9 +181,11 @@ public partial class MainWindow : Window
UploadTempoBox.SelectedIndex = ResolveUploadTempoIndex(_settings.UploadDelayMs);
IncludePatternsBox.Text = _settings.IncludePatterns ?? string.Empty;
ExcludePatternsBox.Text = _settings.ExcludePatterns ?? string.Empty;
ResponseFormatBox.SelectedIndex = ResolveResponseFormatIndex(_settings.ResponseFormat);
ManualUploadUrlBox.Text = _settings.UploadUrl ?? string.Empty;
ManualUsernameBox.Text = _settings.Username ?? string.Empty;
ManualPasswordBox.Text = string.Empty;
SettingsUnlockToggle.IsChecked = false;
if (!string.IsNullOrWhiteSpace(_settings.UploadUrl))
{
@@ -194,6 +200,8 @@ public partial class MainWindow : Window
UpdateFolderHealth();
UpdateDiagnostics();
UpdateSteps();
RefreshProfiles();
UpdateAdvancedLockState();
}
private void OnWindowOpened(object? sender, EventArgs e)
@@ -263,6 +271,7 @@ public partial class MainWindow : Window
return;
}
RecordLastSeen(e.FullPath);
_uploadService.Enqueue(e.FullPath, OnQueued);
}
@@ -278,6 +287,7 @@ public partial class MainWindow : Window
return;
}
RecordLastSeen(e.FullPath);
_uploadService.Enqueue(e.FullPath, OnQueued);
}
@@ -299,6 +309,7 @@ public partial class MainWindow : Window
UpdateUpload(path, UploadStatus.Queued);
AddPendingUpload(path);
UpdateStatusIfAllowed($"Wartet: {Path.GetFileName(path)}", false);
AppendLog($"Wartet: {Path.GetFileName(path)}");
UpdateCountersText();
}
@@ -320,6 +331,7 @@ public partial class MainWindow : Window
RemovePendingUpload(path);
MarkUploaded(path);
UpdateStatusIfAllowed($"Hochgeladen: {Path.GetFileName(path)}", false);
AppendLog($"Hochgeladen: {Path.GetFileName(path)}");
UpdateCountersText();
UpdateLiveStatus();
}
@@ -333,6 +345,7 @@ public partial class MainWindow : Window
RemovePendingUpload(path);
UpdateStatusIfAllowed($"Upload fehlgeschlagen: {Path.GetFileName(path)}", true);
SetLastError($"{Path.GetFileName(path)} {message}");
AppendLog($"Fehlgeschlagen: {Path.GetFileName(path)} {message}");
UpdateRetryButton();
UpdateCountersText();
}
@@ -379,6 +392,7 @@ public partial class MainWindow : Window
private void UpdateRetryButton()
{
RetryFailedButton.IsEnabled = _failedPaths.Count > 0;
ClearFailedButton.IsEnabled = _failedPaths.Count > 0;
}
private void RetryFailedButton_Click(object? sender, RoutedEventArgs e)
@@ -401,6 +415,21 @@ public partial class MainWindow : Window
UpdateCountersText();
}
private void ClearFailedButton_Click(object? sender, RoutedEventArgs e)
{
if (_failedPaths.Count == 0)
{
return;
}
var cleared = _failedPaths.Count;
_failedPaths.Clear();
Interlocked.Add(ref _failedCount, -cleared);
UpdateRetryButton();
UpdateCountersText();
UpdateStatusIfAllowed("Fehlerliste geleert.", false);
}
private void UpdateSteps()
{
var hasCode = !string.IsNullOrWhiteSpace(_settings.UploadUrl);
@@ -419,6 +448,11 @@ public partial class MainWindow : Window
ReconnectButton.IsEnabled = enabled;
}
private void SettingsUnlockToggle_Changed(object? sender, RoutedEventArgs e)
{
UpdateAdvancedLockState();
}
private void RestorePendingUploads()
{
EnsureSettingsCollections();
@@ -553,6 +587,7 @@ public partial class MainWindow : Window
_settings.UploadDelayMs = ResolveUploadDelay(UploadTempoBox.SelectedIndex);
_settings.IncludePatterns = NormalizePatternInput(IncludePatternsBox.Text);
_settings.ExcludePatterns = NormalizePatternInput(ExcludePatternsBox.Text);
_settings.ResponseFormat = ResolveResponseFormat(ResponseFormatBox.SelectedIndex);
if (!string.IsNullOrWhiteSpace(manualUploadUrl))
{
@@ -577,6 +612,7 @@ public partial class MainWindow : Window
UpdateFolderHealth();
RestartUploadPipeline();
UpdateSteps();
UpdateAdvancedLockState();
if (!string.IsNullOrWhiteSpace(_settings.UploadUrl))
{
@@ -678,6 +714,7 @@ public partial class MainWindow : Window
{
FolderHealthText.Text = "Ordner: —";
DiskFreeText.Text = "Freier Speicher: —";
LastSeenText.Text = "Letzte Datei: —";
return;
}
@@ -685,13 +722,14 @@ public partial class MainWindow : Window
{
FolderHealthText.Text = "Ordner: fehlt";
DiskFreeText.Text = "Freier Speicher: —";
LastSeenText.Text = "Letzte Datei: —";
return;
}
try
{
_ = Directory.EnumerateFileSystemEntries(folder).FirstOrDefault();
FolderHealthText.Text = "Ordner: OK";
FolderHealthText.Text = CanWriteToFolder(folder) ? "Ordner: OK (schreibbar)" : "Ordner: OK (nur lesen)";
}
catch (UnauthorizedAccessException)
{
@@ -702,7 +740,8 @@ public partial class MainWindow : Window
FolderHealthText.Text = "Ordner: Fehler";
}
DiskFreeText.Text = $"Freier Speicher: {FormatDiskFree(folder)}";
DiskFreeText.Text = FormatDiskFree(folder);
LastSeenText.Text = FormatLastSeen();
}
private void UpdateLiveStatus()
@@ -712,6 +751,7 @@ public partial class MainWindow : Window
if (_lastSuccessAt is null)
{
LiveStatusText.Text = "Live: —";
UpdateDiagnostics();
return;
}
@@ -719,6 +759,7 @@ public partial class MainWindow : Window
var isLive = age <= TimeSpan.FromMinutes(5);
var label = isLive ? "Live: Ja" : "Live: Nein";
LiveStatusText.Text = $"{label} (letzter Upload {FormatRelativeAge(age)})";
UpdateDiagnostics();
});
}
@@ -747,17 +788,18 @@ public partial class MainWindow : Window
var root = Path.GetPathRoot(folder);
if (string.IsNullOrWhiteSpace(root))
{
return "—";
return "Freier Speicher: —";
}
var drive = new DriveInfo(root);
if (!drive.IsReady)
{
return "—";
return "Freier Speicher: —";
}
var freeGb = drive.AvailableFreeSpace / (1024d * 1024d * 1024d);
return $"{freeGb:0.0} GB";
var label = freeGb < 5 ? "niedrig" : "ok";
return $"Freier Speicher: {freeGb:0.0} GB ({label})";
}
private string? ResolveUploadUrl(string? uploadUrl)
@@ -816,6 +858,7 @@ public partial class MainWindow : Window
EventNameText.Text = $"Event: {_settings.EventName ?? ""}";
BaseUrlText.Text = $"Basis-URL: {_settings.BaseUrl ?? ""}";
VersionText.Text = $"App-Version: {GetAppVersion()}";
ConnectExpiryText.Text = FormatConnectExpiry();
LastErrorText.Text = $"Letzter Fehler: {FormatLastError()}";
});
}
@@ -866,6 +909,71 @@ public partial class MainWindow : Window
{
_settings.PendingUploads ??= new List<string>();
_settings.UploadedFiles ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
_settings.Profiles ??= new List<PhotoboothProfile>();
}
private void RecordLastSeen(string path)
{
_settings.LastSeenFile = Path.GetFileName(path);
_settings.LastSeenAt = DateTimeOffset.Now.ToString("O");
_settingsStore.Save(_settings);
UpdateFolderHealth();
}
private bool CanWriteToFolder(string folder)
{
try
{
var testFile = Path.Combine(folder, $".fotospiel-write-test-{Guid.NewGuid():N}.tmp");
File.WriteAllText(testFile, "ok");
File.Delete(testFile);
return true;
}
catch
{
return false;
}
}
private string FormatLastSeen()
{
if (string.IsNullOrWhiteSpace(_settings.LastSeenAt) || string.IsNullOrWhiteSpace(_settings.LastSeenFile))
{
return "Letzte Datei: —";
}
if (DateTimeOffset.TryParse(_settings.LastSeenAt, out var timestamp))
{
return $"Letzte Datei: {_settings.LastSeenFile} ({timestamp:HH:mm})";
}
return $"Letzte Datei: {_settings.LastSeenFile}";
}
private string FormatConnectExpiry()
{
if (string.IsNullOrWhiteSpace(_settings.ConnectExpiresAt))
{
return "Verbindungscode: —";
}
if (!DateTimeOffset.TryParse(_settings.ConnectExpiresAt, out var expiry))
{
return "Verbindungscode: —";
}
var remaining = expiry - DateTimeOffset.Now;
if (remaining <= TimeSpan.Zero)
{
return "Verbindungscode: abgelaufen";
}
if (remaining.TotalHours >= 1)
{
return $"Verbindungscode: {Math.Ceiling(remaining.TotalHours)} h gueltig";
}
return $"Verbindungscode: {Math.Ceiling(remaining.TotalMinutes)} min gueltig";
}
private string? ResolveTestUrl()
@@ -926,6 +1034,31 @@ public partial class MainWindow : Window
return 1;
}
private static string? ResolveResponseFormat(int selectedIndex)
{
return selectedIndex switch
{
1 => "json",
2 => "xml",
_ => null,
};
}
private static int ResolveResponseFormatIndex(string? format)
{
if (string.Equals(format, "json", StringComparison.OrdinalIgnoreCase))
{
return 1;
}
if (string.Equals(format, "xml", StringComparison.OrdinalIgnoreCase))
{
return 2;
}
return 0;
}
private static string NormalizePatternInput(string? input)
{
if (string.IsNullOrWhiteSpace(input))
@@ -1022,4 +1155,138 @@ public partial class MainWindow : Window
return patternIndex == pattern.Length;
}
private void UpdateAdvancedLockState()
{
var connected = !string.IsNullOrWhiteSpace(_settings.UploadUrl);
var unlocked = SettingsUnlockToggle?.IsChecked ?? false;
var enabled = !connected || unlocked;
BaseUrlBox.IsEnabled = enabled;
MaxUploadsBox.IsEnabled = enabled;
UploadTempoBox.IsEnabled = enabled;
IncludePatternsBox.IsEnabled = enabled;
ExcludePatternsBox.IsEnabled = enabled;
ResponseFormatBox.IsEnabled = enabled;
ManualUploadUrlBox.IsEnabled = enabled;
ManualUsernameBox.IsEnabled = enabled;
ManualPasswordBox.IsEnabled = enabled;
TestConnectionButton.IsEnabled = enabled;
SaveAdvancedButton.IsEnabled = enabled;
}
private void RefreshProfiles()
{
ProfilesBox.ItemsSource = _settings.Profiles.Select(profile => profile.DisplayName).ToList();
}
private void LoadProfileButton_Click(object? sender, RoutedEventArgs e)
{
var index = ProfilesBox.SelectedIndex;
if (index < 0 || index >= _settings.Profiles.Count)
{
UpdateStatus("Bitte zuerst ein Profil auswaehlen.");
return;
}
var profile = _settings.Profiles[index];
_settings.BaseUrl = NormalizeBaseUrl(profile.BaseUrl) ?? _settings.BaseUrl;
_settings.UploadUrl = profile.UploadUrl;
_settings.Username = profile.Username;
_settings.Password = profile.Password;
_settings.ResponseFormat = profile.ResponseFormat;
_settings.WatchFolder = profile.WatchFolder;
_settings.IncludePatterns = profile.IncludePatterns;
_settings.ExcludePatterns = profile.ExcludePatterns;
_settings.MaxConcurrentUploads = profile.MaxConcurrentUploads > 0 ? profile.MaxConcurrentUploads : _settings.MaxConcurrentUploads;
_settings.UploadDelayMs = profile.UploadDelayMs;
_settingsStore.Save(_settings);
ApplySettings();
UpdateStatus("Profil geladen.");
}
private void SaveProfileButton_Click(object? sender, RoutedEventArgs e)
{
var label = _settings.EventName ?? "Neues Profil";
var profile = new PhotoboothProfile
{
Label = label,
EventName = _settings.EventName,
BaseUrl = _settings.BaseUrl,
UploadUrl = _settings.UploadUrl,
Username = _settings.Username,
Password = _settings.Password,
ResponseFormat = _settings.ResponseFormat,
WatchFolder = _settings.WatchFolder,
IncludePatterns = _settings.IncludePatterns,
ExcludePatterns = _settings.ExcludePatterns,
MaxConcurrentUploads = _settings.MaxConcurrentUploads,
UploadDelayMs = _settings.UploadDelayMs,
};
_settings.Profiles.RemoveAll(existing => string.Equals(existing.DisplayName, profile.DisplayName, StringComparison.OrdinalIgnoreCase));
_settings.Profiles.Insert(0, profile);
_settingsStore.Save(_settings);
RefreshProfiles();
ProfilesBox.SelectedIndex = 0;
UpdateStatus("Profil gespeichert.");
}
private async void LogCopyButton_Click(object? sender, RoutedEventArgs e)
{
var content = ReadLogForCopy();
if (string.IsNullOrWhiteSpace(content))
{
UpdateStatus("Log ist leer.");
return;
}
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
if (clipboard is null)
{
UpdateStatus("Zwischenablage nicht verfuegbar.");
return;
}
await clipboard.SetTextAsync(content);
UpdateStatus("Log kopiert.");
}
private void AppendLog(string message)
{
var line = $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss} {message}";
_logBuffer.Add(line);
while (_logBuffer.Count > 200)
{
_logBuffer.RemoveAt(0);
}
try
{
File.AppendAllLines(_settingsStore.LogPath, new[] { line });
}
catch
{
// ignore file errors
}
}
private string ReadLogForCopy()
{
try
{
if (File.Exists(_settingsStore.LogPath))
{
var lines = File.ReadAllLines(_settingsStore.LogPath);
return string.Join(Environment.NewLine, lines.TakeLast(200));
}
}
catch
{
// ignore
}
return _logBuffer.Count > 0 ? string.Join(Environment.NewLine, _logBuffer) : string.Empty;
}
}

View File

@@ -0,0 +1,26 @@
using System;
namespace PhotoboothUploader.Models;
public sealed class PhotoboothProfile
{
public string? Label { get; set; }
public string? EventName { get; set; }
public string? BaseUrl { get; set; }
public string? UploadUrl { get; set; }
public string? Username { get; set; }
public string? Password { get; set; }
public string? ResponseFormat { get; set; }
public string? WatchFolder { get; set; }
public string? IncludePatterns { get; set; }
public string? ExcludePatterns { get; set; }
public int MaxConcurrentUploads { get; set; } = 2;
public int UploadDelayMs { get; set; } = 500;
public string DisplayName
=> !string.IsNullOrWhiteSpace(Label)
? Label
: !string.IsNullOrWhiteSpace(EventName)
? EventName
: UploadUrl ?? BaseUrl ?? "Profil";
}

View File

@@ -16,6 +16,10 @@ public sealed class PhotoboothSettings
public string? ExcludePatterns { get; set; }
public List<string> PendingUploads { get; set; } = new();
public Dictionary<string, string> UploadedFiles { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public List<PhotoboothProfile> Profiles { get; set; } = new();
public string? ConnectExpiresAt { get; set; }
public string? LastSeenFile { get; set; }
public string? LastSeenAt { get; set; }
public string? LastError { get; set; }
public string? LastErrorAt { get; set; }
public int MaxConcurrentUploads { get; set; } = 2;

View File

@@ -14,6 +14,7 @@ public sealed class SettingsStore
};
public string SettingsPath { get; }
public string LogPath { get; }
public SettingsStore()
{
@@ -24,6 +25,7 @@ public sealed class SettingsStore
Directory.CreateDirectory(basePath);
SettingsPath = Path.Combine(basePath, "settings.json");
LogPath = Path.Combine(basePath, "uploader.log");
}
public PhotoboothSettings Load()