|
| 1 | +using System.IO; |
| 2 | +using System.Text.Json; |
| 3 | +using System.Windows; |
| 4 | +using System.Windows.Automation; |
| 5 | +using System.Windows.Controls; |
| 6 | +using System.Windows.Media; |
| 7 | +using LifeOS.Core.DeviceTransfer; |
| 8 | +using LifeOS.Core.Forms; |
| 9 | +using LifeOS.Shared.DeviceTransfer; |
| 10 | +using LifeOS.Shared.Storage; |
| 11 | + |
| 12 | +namespace LifeOS.Desktop; |
| 13 | + |
| 14 | +public sealed class DeviceTransferWorkspaceView : UserControl |
| 15 | +{ |
| 16 | + private readonly bool _demo; private List<DeviceTransferReview> _reviews = []; private IReadOnlyList<FormFieldIssue> _issues = []; private UserFacingProblem? _problem; private string? _notice; |
| 17 | + private string _source = "", _destination = "", _key = "", _local = "", _incoming = ""; |
| 18 | + public DeviceTransferWorkspaceView(bool demo) { _demo = demo; Background = Brush("#0C1220"); Foreground = Brushes.White; FontFamily = new FontFamily("Segoe UI"); try { _reviews = DeviceTransferStorage.Load(); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException) { _problem = UserFacingProblemFactory.FromException(ex, "load device transfer reviews"); } Render(); } |
| 19 | + |
| 20 | + private void Render() |
| 21 | + { |
| 22 | + int conflicts = _reviews.Count(review => review.State == DeviceTransferState.ConflictReview), duplicates = _reviews.Count(review => review.State == DeviceTransferState.Duplicate), resolved = _reviews.Count(review => review.State is DeviceTransferState.Resolved or DeviceTransferState.Rejected); |
| 23 | + StackPanel root = new() { Margin = new Thickness(24) }; |
| 24 | + root.Children.Add(Text(_demo ? "PORTFOLIO DEMO • ISOLATED" : "ORDINARY MODE • LOCAL REVIEW", 11, "#AFA4FF", FontWeights.SemiBold)); root.Children.Add(Text("Device Transfer & Conflict Review", 30, "#FFFFFF", FontWeights.Bold)); root.Children.Add(Text("Compare transfer fingerprints and record an explicit conflict decision. No payload is copied, merged, uploaded or promoted to authoritative state here.", 14, "#B8C5D8")); if (_notice is not null) root.Children.Add(Text(_notice, 12, "#83D4B3", FontWeights.SemiBold)); if (_problem is not null) root.Children.Add(Problem(_problem)); |
| 25 | + WrapPanel metrics = new() { Margin = new Thickness(0, 16, 0, 8) }; metrics.Children.Add(Metric("Reviews", _reviews.Count.ToString(), "Local manifests")); metrics.Children.Add(Metric("Conflicts", conflicts.ToString(), "Decision required")); metrics.Children.Add(Metric("Duplicates", duplicates.ToString(), "No mutation")); metrics.Children.Add(Metric("Resolved", resolved.ToString(), "Decision ledger only")); root.Children.Add(metrics); root.Children.Add(Capture()); root.Children.Add(Heading("Transfer review queue", 21, new Thickness(0, 20, 0, 6))); |
| 26 | + if (_reviews.Count == 0) root.Children.Add(Card("No transfer manifests yet", "Ordinary mode does not seed devices, record keys, payloads or fingerprints.", "#151F30")); else foreach (DeviceTransferReview review in _reviews) root.Children.Add(ReviewCard(review)); |
| 27 | + LocalStoreHealth health = DeviceTransferStorage.Inspect(); root.Children.Add(Heading("Transfer boundary", 21, new Thickness(0, 20, 0, 6))); root.Children.Add(Card("Versioned device-transfer-review store", $"State: {health.State}. Only manifest metadata and explicit decisions are stored. Payload transport, decryption and authoritative replacement are unavailable.", "#152437")); Content = new ScrollViewer { VerticalScrollBarVisibility = ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, Content = root }; |
| 28 | + } |
| 29 | + |
| 30 | + private UIElement Capture() |
| 31 | + { |
| 32 | + StackPanel body = new(); body.Children.Add(Heading("Stage a transfer manifest", 20)); body.Children.Add(Text("Use SHA-256 fingerprints computed outside this review surface. Required fields are validated before save.", 12, "#A9B6CA")); WrapPanel row = new(); row.Children.Add(Input("Source device *", "transfer-source", "DeviceTransfer.Source", _source, value => _source = value, 480)); row.Children.Add(Input("Destination device *", "transfer-destination", "DeviceTransfer.Destination", _destination, value => _destination = value, 480)); body.Children.Add(row); body.Children.Add(Input("Record key *", "transfer-record-key", "DeviceTransfer.RecordKey", _key, value => _key = value, 970)); body.Children.Add(Input("Local SHA-256 *", "transfer-local-fingerprint", "DeviceTransfer.LocalFingerprint", _local, value => _local = value, 970)); body.Children.Add(Input("Incoming SHA-256 *", "transfer-incoming-fingerprint", "DeviceTransfer.IncomingFingerprint", _incoming, value => _incoming = value, 970)); Button add = Button("Stage transfer review", "DeviceTransfer.Stage", false); add.Click += (_, _) => Stage(); body.Children.Add(add); return Panel(body, new Thickness(0, 14, 0, 0)); |
| 33 | + } |
| 34 | + |
| 35 | + private UIElement ReviewCard(DeviceTransferReview review) |
| 36 | + { |
| 37 | + StackPanel body = new(); DockPanel header = new(); TextBlock state = Text(review.State.ToString().ToUpperInvariant(), 11, "#AFA4FF", FontWeights.SemiBold); DockPanel.SetDock(state, Dock.Right); header.Children.Add(state); header.Children.Add(Heading(review.RecordKey, 18)); body.Children.Add(header); body.Children.Add(Text($"{review.SourceDevice} → {review.DestinationDevice}", 12, "#A9B6CA")); body.Children.Add(Text($"Local {Short(review.LocalFingerprint)} • Incoming {Short(review.IncomingFingerprint)}", 12, "#E7ECF4", FontWeights.SemiBold)); |
| 38 | + if (review.State == DeviceTransferState.ConflictReview) { WrapPanel actions = new(); foreach ((string label, DeviceTransferResolution resolution) in Decisions()) { Button button = Button(label, $"DeviceTransfer.Resolve.{resolution}.{review.Id:N}", true); button.Click += (_, _) => Resolve(review, resolution); actions.Children.Add(button); } body.Children.Add(actions); } else body.Children.Add(Text(review.Resolution == DeviceTransferResolution.None ? "Matching fingerprint; no transfer action was taken." : $"Decision: {review.Resolution}. Authoritative data was not changed.", 12, "#A9B6CA")); return Panel(body, new Thickness(0, 8, 0, 0)); |
| 39 | + } |
| 40 | + |
| 41 | + private void Stage() |
| 42 | + { |
| 43 | + DeviceTransferDraft draft = new(_source, _destination, _key, _local, _incoming); FormValidationResult validation = DeviceTransferService.Validate(draft); _issues = validation.Issues; if (!validation.IsValid) { _problem = new UserFacingProblem("device-transfer-validation-failed", "Review the transfer manifest", "No transfer review was staged because one or more fields are invalid.", "Correct the highlighted fields, then try again.", true); Render(); return; } |
| 44 | + try { DeviceTransferReview review = DeviceTransferService.Create(draft, DateTimeOffset.UtcNow); List<DeviceTransferReview> candidate = _reviews.Append(review).ToList(); DeviceTransferStorage.Save(candidate); _reviews = candidate; _source = _destination = _key = _local = _incoming = ""; _issues = []; _problem = null; _notice = review.State == DeviceTransferState.Duplicate ? "Recorded a duplicate fingerprint. No payload action occurred." : "Staged a conflict for explicit review."; Render(); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException or InvalidDataException) { _problem = UserFacingProblemFactory.FromException(ex, "save the transfer review"); Render(); } |
| 45 | + } |
| 46 | + |
| 47 | + private void Resolve(DeviceTransferReview review, DeviceTransferResolution resolution) { try { DeviceTransferReview changed = DeviceTransferService.Resolve(review, resolution, DateTimeOffset.UtcNow); List<DeviceTransferReview> candidate = _reviews.Select(value => value.Id == review.Id ? changed : value).ToList(); DeviceTransferStorage.Save(candidate); _reviews = candidate; _notice = $"Recorded {resolution}. No authoritative record was replaced."; _problem = null; Render(); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException or InvalidDataException or InvalidOperationException) { _problem = UserFacingProblemFactory.FromException(ex, "resolve the transfer conflict"); Render(); } } |
| 48 | + private static IReadOnlyList<(string, DeviceTransferResolution)> Decisions() => [("Keep local", DeviceTransferResolution.KeepLocal), ("Keep incoming candidate", DeviceTransferResolution.KeepIncomingCandidate), ("Keep both candidates", DeviceTransferResolution.KeepBothCandidates), ("Reject incoming", DeviceTransferResolution.RejectIncoming)]; |
| 49 | + private static string Short(string value) => value.Length < 12 ? value : value[..12] + "…"; |
| 50 | + private UIElement Input(string label, string fieldId, string id, string value, Action<string> changed, double width) { StackPanel field = new() { Width = width, Margin = new Thickness(0, 7, 10, 0) }; field.Children.Add(Text(label, 12, "#C7D2E3", FontWeights.SemiBold)); TextBox input = new() { Text = value, MinHeight = 38, Background = Brush("#101827"), Foreground = Brushes.White, BorderBrush = Brush("#3A4B66"), Padding = new Thickness(10, 7, 10, 7) }; AutomationProperties.SetAutomationId(input, id); input.TextChanged += (_, _) => changed(input.Text); field.Children.Add(input); string error = string.Join(" ", _issues.Where(issue => issue.FieldId == fieldId).Select(issue => issue.Message)); if (error.Length > 0) field.Children.Add(Text(error, 11, "#FF7788", FontWeights.SemiBold)); return field; } |
| 51 | + private static Border Problem(UserFacingProblem p) { StackPanel body = new(); body.Children.Add(Heading($"{p.Title} ({p.Code})", 16)); body.Children.Add(Text(p.Detail, 12, "#E1E7F0")); body.Children.Add(Text($"Next: {p.RecoveryAction}", 12, "#C5AECF")); return new Border { Background = Brush("#251925"), BorderBrush = Brush("#C95F75"), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(9), Padding = new Thickness(15), Margin = new Thickness(0, 12, 0, 0), Child = body }; } |
| 52 | + private static Button Button(string label, string id, bool secondary) { Button button = new() { Content = label, Background = Brush(secondary ? "#25334A" : "#315E91"), Foreground = Brushes.White, BorderBrush = Brush(secondary ? "#405472" : "#477DB4"), Padding = new Thickness(14, 7, 14, 7), Margin = new Thickness(0, 8, 8, 0), MinHeight = 36 }; AutomationProperties.SetAutomationId(button, id); return button; } |
| 53 | + private static Border Metric(string label, string value, string detail) { StackPanel body = new(); body.Children.Add(Text(label, 11, "#9EACC0")); body.Children.Add(Text(value, 22, "#FFFFFF", FontWeights.SemiBold)); body.Children.Add(Text(detail, 11, "#9EACC0")); return new Border { Width = 230, MinHeight = 100, Background = Brush("#151F30"), BorderBrush = Brush("#31445F"), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(9), Padding = new Thickness(14), Margin = new Thickness(0, 0, 10, 10), Child = body }; } |
| 54 | + private static Border Card(string title, string body, string background) { StackPanel content = new(); content.Children.Add(Heading(title, 16)); content.Children.Add(Text(body, 12, "#C2CDDC")); return new Border { Background = Brush(background), BorderBrush = Brush("#31445F"), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(9), Padding = new Thickness(16), Margin = new Thickness(0, 8, 0, 0), Child = content }; } |
| 55 | + private static Border Panel(UIElement child, Thickness margin) => new() { Background = Brush("#151F30"), BorderBrush = Brush("#31445F"), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(9), Padding = new Thickness(16), Margin = margin, Child = child }; private static TextBlock Heading(string text, double size, Thickness? margin = null) => new() { Text = text, FontSize = size, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White, TextWrapping = TextWrapping.Wrap, Margin = margin ?? new Thickness(0, 0, 0, 4) }; private static TextBlock Text(string text, double size, string color, FontWeight? weight = null) => new() { Text = text, FontSize = size, FontWeight = weight ?? FontWeights.Normal, Foreground = Brush(color), TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 3, 0, 0) }; private static SolidColorBrush Brush(string value) => new((Color)ColorConverter.ConvertFromString(value)); |
| 56 | +} |
0 commit comments