using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading; using System.Threading.Tasks; using Avalonia.Threading; using CommunityToolkit.Mvvm.Input; using Junction.Core.Monitoring; using Junction.Domain.Models; using Junction.Domain.Persistence; using Microsoft.Extensions.Logging; namespace Junction.App.ViewModels { /// /// Live machine dashboard. Loads configured machines from the repository, seeds each row from /// the monitor's latest-snapshot cache, and refreshes rows as /// fires. Snapshot events arrive on poll-loop threads and are marshalled onto the UI thread. /// public sealed partial class DashboardViewModel : ViewModelBase, IDisposable { private readonly IMachineRepository _repository; private readonly IMachineMonitor _monitor; private readonly ILogger _logger; private readonly Dictionary _rowsById = new Dictionary(); private bool _subscribed; private bool _disposed; public string Title => "Junction — Machines"; /// /// Shell reference, set by after construction (breaks the /// otherwise-circular DI graph). Used to open a machine's detail screen on row click. /// public MainWindowViewModel? Navigator { get; set; } public ObservableCollection Machines { get; } = new ObservableCollection(); public DashboardViewModel(IMachineRepository repository, IMachineMonitor monitor, ILogger logger) { _repository = repository; _monitor = monitor; _logger = logger; } /// Loads machines, seeds latest snapshots, and subscribes to live updates. public async Task LoadAsync() { // Subscribe first so no update slips through between load and subscribe; // rows are looked up by id, unknown ids are ignored. if (!_subscribed) { _monitor.SnapshotUpdated += OnSnapshotUpdated; _subscribed = true; } var result = await _repository.GetAllAsync(CancellationToken.None).ConfigureAwait(true); if (!result.IsSuccess) { var detail = result.Errors.Count > 0 ? result.Errors[0].Message : "unknown error"; _logger.LogError("Dashboard load failed: {Detail}", detail); return; } Machines.Clear(); _rowsById.Clear(); var latest = _monitor.LatestSnapshots; foreach (var machine in result.Value) { var row = new MachineRowViewModel(machine, OpenDetail); if (latest.TryGetValue(machine.Id, out var snapshot)) { row.Apply(snapshot); } _rowsById[machine.Id] = row; Machines.Add(row); } _logger.LogInformation("Dashboard loaded {Count} machine(s).", Machines.Count); } /// Header action: opens the add-machine config screen via the shell. [RelayCommand] private void AddMachine() => Navigator?.ShowConfig(null); /// Row-click handler: routes to the machine-detail screen via the shell. private void OpenDetail(MachineRowViewModel row) { if (row == null) { return; } Navigator?.ShowDetail(row.MachineId, row.Name); } private void OnSnapshotUpdated(object? sender, MachineSnapshot snapshot) { if (snapshot == null) { return; } // Event fires on a poll-loop thread → marshal all observable mutations to the UI thread. Dispatcher.UIThread.Post(() => { if (_disposed) { return; } if (_rowsById.TryGetValue(snapshot.MachineId, out var row)) { row.Apply(snapshot); } }); } public void Dispose() { if (_disposed) { return; } _disposed = true; if (_subscribed) { _monitor.SnapshotUpdated -= OnSnapshotUpdated; _subscribed = false; } } } }