/* Copyright NetFoundry Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 */ using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using ZitiDesktopEdge.DataStructures; using ZitiDesktopEdge.ServiceClient; namespace ZitiDesktopEdge { /// /// Outcome of MaintenanceWindowControl.PersistAsync. On failure, FailedOperation names the /// IPC step that bounced (for log context) and ErrorMessage holds the user-facing string. /// On success, the captured values let the host update its in-memory settings cache /// without re-reading them from the combos (which the PropertyChanged-driven re-render /// may have already mutated by the time the host gets control back). /// public class MaintenanceWindowPersistResult { public bool Success; public string FailedOperation; // null on success public string ErrorMessage; // null on success public int StartHour; public int EndHour; public MaintenanceWindowFrequency Frequency; public MaintenanceWindowMonthlyMode MonthlyMode; public int? DayOfWeek; public int? DayOfMonth; public MaintenanceWindowMonthlyOrdinal? MonthlyOrdinal; } /// /// Self-contained Installation maintenance-window editor. Hosts: /// - Frequency combo (Daily / Weekly / Monthly) /// - Monthly Mode combo (By date / By weekday), visible only for Monthly /// - Day picker(s): DayOfWeek (Weekly), DayOfMonth (Monthly ByDate), or /// Ordinal + DayOfWeek (Monthly ByWeekday) /// - From / To hour combos plus "Any time" checkbox /// /// API for the host (typically MainMenu): /// - ApplyFromState(...) -- pushes saved values into the combos. /// - Read-only getters -- read by the host's save handler to push to IPC. /// - IsEditable -- enables / disables all combos and dims the heading. /// public partial class MaintenanceWindowControl : UserControl { // Order of frequency items must match the MaintenanceWindowFrequency enum // (Daily=0, Weekly=1, Monthly=2). private static readonly string[] FrequencyLabels = { "Daily", "Weekly", "Monthly" }; // Order matches System.DayOfWeek (Sunday=0 .. Saturday=6) which is what we // persist in settings.json / the policy registry. private static readonly string[] DayOfWeekLabels = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", }; // Order matches MaintenanceWindowMonthlyMode (ByDate=0, ByWeekday=1). private static readonly string[] MonthlyModeLabels = { "By date", "By weekday" }; // Order matches MaintenanceWindowMonthlyOrdinal (First=1 .. Last=5); we render in // that order so SelectedIndex+1 maps directly to the enum value. private static readonly string[] OrdinalLabels = { "First", "Second", "Third", "Fourth", "Last" }; private bool _isEditable = true; public MaintenanceWindowControl() { InitializeComponent(); PopulateHourCombos(); PopulateFrequencyAndDayCombos(); UpdateFrequencyDayVisibility(); } // --------------------------------------------------------------------------------- // Host API: read these in the parent's Save handler. // --------------------------------------------------------------------------------- /// Hour of day (0-23) when the maintenance window opens. public int WindowStart => Math.Max(0, MaintenanceWindowStartCombo.SelectedIndex); /// Hour of day (0-23) when the maintenance window closes. public int WindowEnd => Math.Max(0, MaintenanceWindowEndCombo.SelectedIndex); /// Configured cadence. public MaintenanceWindowFrequency Frequency => (MaintenanceWindowFrequency)Math.Max(0, MaintenanceWindowFrequencyCombo.SelectedIndex); /// Monthly sub-mode (only relevant when Frequency == Monthly). public MaintenanceWindowMonthlyMode MonthlyMode => (MaintenanceWindowMonthlyMode)Math.Max(0, MaintenanceWindowMonthlyModeCombo.SelectedIndex); /// /// Day-of-week to persist for the current cadence shape. Returns null when the /// configured cadence doesn't use a day-of-week (Daily, or Monthly+ByDate). /// public int? DayOfWeekToPersist { get { bool needsDow = Frequency == MaintenanceWindowFrequency.Weekly || (Frequency == MaintenanceWindowFrequency.Monthly && MonthlyMode == MaintenanceWindowMonthlyMode.ByWeekday); if (!needsDow) return null; return Math.Max(0, MaintenanceWindowDayOfWeekCombo.SelectedIndex); } } /// /// Day-of-month to persist for the current cadence shape. Returns null unless /// Frequency=Monthly and MonthlyMode=ByDate. The "Last day" entry maps to the /// MaintenanceWindowDayOfMonthSentinel.LastDay value. /// public int? DayOfMonthToPersist { get { bool needsDom = Frequency == MaintenanceWindowFrequency.Monthly && MonthlyMode == MaintenanceWindowMonthlyMode.ByDate; if (!needsDom) return null; return DayOfMonthSelectedValue(); } } /// /// Ordinal to persist for the current cadence shape. Returns null unless /// Frequency=Monthly and MonthlyMode=ByWeekday. /// public MaintenanceWindowMonthlyOrdinal? OrdinalToPersist { get { bool needsOrd = Frequency == MaintenanceWindowFrequency.Monthly && MonthlyMode == MaintenanceWindowMonthlyMode.ByWeekday; if (!needsOrd) return null; return (MaintenanceWindowMonthlyOrdinal)(Math.Max(0, MaintenanceWindowOrdinalCombo.SelectedIndex) + 1); } } /// /// True when controls should accept user input. When false, every combo + the /// "Any time" checkbox are disabled and dimmed, and the heading dims to match. /// public bool IsEditable { get => _isEditable; set { _isEditable = value; ApplyEditableState(); } } /// /// Push saved-state values into the combos and recompute visibility once at the end. /// public void ApplyFromState( int? windowStart, int? windowEnd, MaintenanceWindowFrequency frequency, MaintenanceWindowMonthlyMode monthlyMode, int? dayOfWeek, int? dayOfMonth, MaintenanceWindowMonthlyOrdinal? monthlyOrdinal) { int start = windowStart ?? 0; int end = windowEnd ?? 0; MaintenanceWindowStartCombo.SelectedIndex = start; MaintenanceWindowEndCombo.SelectedIndex = end; bool anyTime = start == 0 && end == 0; MaintenanceWindowAnyTime.IsChecked = anyTime; MaintenanceWindowFrequencyCombo.SelectedIndex = (int)frequency; MaintenanceWindowMonthlyModeCombo.SelectedIndex = (int)monthlyMode; int dow = dayOfWeek ?? 0; if (dow >= 0 && dow <= 6) { MaintenanceWindowDayOfWeekCombo.SelectedIndex = dow; } if (dayOfMonth.HasValue) { if (dayOfMonth.Value == MaintenanceWindowDayOfMonthSentinel.LastDay) { MaintenanceWindowDayOfMonthCombo.SelectedIndex = 28; // "Last day" } else if (dayOfMonth.Value >= 1 && dayOfMonth.Value <= 28) { MaintenanceWindowDayOfMonthCombo.SelectedIndex = dayOfMonth.Value - 1; } } var ord = monthlyOrdinal ?? MaintenanceWindowMonthlyOrdinal.Third; MaintenanceWindowOrdinalCombo.SelectedIndex = Math.Max(0, Math.Min(4, (int)ord - 1)); UpdateFrequencyDayVisibility(); } /// /// Snapshot every cadence value and push it through to the monitor service in a single /// atomic IPC call. On success the result carries the captured values so the host can /// update its in-memory state without re-reading the combos. /// public async Task PersistAsync(MonitorClient monitorClient) { var req = new MaintenanceWindowConfigRequest { Start = WindowStart, End = WindowEnd, Frequency = Frequency, MonthlyMode = MonthlyMode, DayOfWeek = DayOfWeekToPersist, DayOfMonth = DayOfMonthToPersist, MonthlyOrdinal = OrdinalToPersist, }; var result = new MaintenanceWindowPersistResult { StartHour = req.Start.Value, EndHour = req.End.Value, Frequency = req.Frequency, MonthlyMode = req.MonthlyMode, DayOfWeek = req.DayOfWeek, DayOfMonth = req.DayOfMonth, MonthlyOrdinal = req.MonthlyOrdinal, }; SvcResponse r = await monitorClient.SetMaintenanceWindowAsync(req); if (r == null || r.Code != 0) { result.Success = false; result.FailedOperation = "SetMaintenanceWindowAsync"; result.ErrorMessage = !string.IsNullOrEmpty(r?.Error) ? r.Error : "Could not save maintenance window settings."; return result; } result.Success = true; return result; } // --------------------------------------------------------------------------------- // Internal helpers + handlers // --------------------------------------------------------------------------------- private void PopulateHourCombos() { var hours = new List(); for (int h = 0; h < 24; h++) { hours.Add($"{h:D2}:00"); } MaintenanceWindowStartCombo.ItemsSource = hours; MaintenanceWindowEndCombo.ItemsSource = hours; } private void PopulateFrequencyAndDayCombos() { MaintenanceWindowFrequencyCombo.ItemsSource = FrequencyLabels; MaintenanceWindowFrequencyCombo.SelectedIndex = 0; MaintenanceWindowDayOfWeekCombo.ItemsSource = DayOfWeekLabels; MaintenanceWindowDayOfWeekCombo.SelectedIndex = 0; var domLabels = new List(); for (int d = 1; d <= 28; d++) { domLabels.Add(d.ToString()); } domLabels.Add("Last day"); // sentinel; resolves to LastDay = 32 in DayOfMonthSelectedValue() MaintenanceWindowDayOfMonthCombo.ItemsSource = domLabels; MaintenanceWindowDayOfMonthCombo.SelectedIndex = 0; MaintenanceWindowMonthlyModeCombo.ItemsSource = MonthlyModeLabels; MaintenanceWindowMonthlyModeCombo.SelectedIndex = 0; MaintenanceWindowOrdinalCombo.ItemsSource = OrdinalLabels; MaintenanceWindowOrdinalCombo.SelectedIndex = 2; // default Third (Patch Tuesday) } private void MaintenanceWindowFrequency_Changed(object sender, SelectionChangedEventArgs e) { UpdateFrequencyDayVisibility(); } private void MaintenanceWindowMonthlyMode_Changed(object sender, SelectionChangedEventArgs e) { UpdateFrequencyDayVisibility(); } private void UpdateFrequencyDayVisibility() { // Guard: WPF can fire SelectionChanged during InitializeComponent before fields are set. if (MaintenanceWindowFrequencyCombo == null) return; var freq = (MaintenanceWindowFrequency)Math.Max(0, MaintenanceWindowFrequencyCombo.SelectedIndex); bool weekly = freq == MaintenanceWindowFrequency.Weekly; bool monthly = freq == MaintenanceWindowFrequency.Monthly; var monthlyMode = (MaintenanceWindowMonthlyMode)Math.Max(0, MaintenanceWindowMonthlyModeCombo.SelectedIndex); bool monthlyByDate = monthly && monthlyMode == MaintenanceWindowMonthlyMode.ByDate; bool monthlyByWeekday = monthly && monthlyMode == MaintenanceWindowMonthlyMode.ByWeekday; MaintenanceWindowMonthlyModeLabel.Visibility = monthly ? Visibility.Visible : Visibility.Collapsed; MaintenanceWindowMonthlyModeCombo.Visibility = monthly ? Visibility.Visible : Visibility.Collapsed; // The DayOfWeek combo is shown in both Weekly and Monthly+ByWeekday, but its // leading "On" label is only used in Weekly -- the Monthly+ByWeekday row uses // the OrdinalLabel ("On the") as its leading text. bool showDow = weekly || monthlyByWeekday; MaintenanceWindowDayOfWeekLabel.Visibility = weekly ? Visibility.Visible : Visibility.Collapsed; MaintenanceWindowDayOfWeekCombo.Visibility = showDow ? Visibility.Visible : Visibility.Collapsed; MaintenanceWindowOrdinalLabel.Visibility = monthlyByWeekday ? Visibility.Visible : Visibility.Collapsed; MaintenanceWindowOrdinalCombo.Visibility = monthlyByWeekday ? Visibility.Visible : Visibility.Collapsed; MaintenanceWindowDayOfMonthLabel.Visibility = monthlyByDate ? Visibility.Visible : Visibility.Collapsed; MaintenanceWindowDayOfMonthCombo.Visibility = monthlyByDate ? Visibility.Visible : Visibility.Collapsed; } // Maps the "Last day" combo entry (index 28) to LastDay sentinel; otherwise returns 1-28. private int DayOfMonthSelectedValue() { int idx = MaintenanceWindowDayOfMonthCombo.SelectedIndex; if (idx == 28) return MaintenanceWindowDayOfMonthSentinel.LastDay; return Math.Max(1, idx + 1); } private void MaintenanceWindowAnyTime_Changed(object sender, RoutedEventArgs e) { bool anyTime = MaintenanceWindowAnyTime.IsChecked == true; if (anyTime) { MaintenanceWindowStartCombo.SelectedIndex = 0; MaintenanceWindowEndCombo.SelectedIndex = 0; } // Start/End enabling depends on both IsEditable and whether AnyTime is checked. MaintenanceWindowStartCombo.IsEnabled = _isEditable && !anyTime; MaintenanceWindowEndCombo.IsEnabled = _isEditable && !anyTime; } private void ApplyEditableState() { double opacity = _isEditable ? 1.0 : 0.3; bool anyTime = MaintenanceWindowAnyTime.IsChecked == true; MaintenanceWindowHeading.Opacity = opacity; MaintenanceWindowAnyTime.IsEnabled = _isEditable; MaintenanceWindowAnyTime.Opacity = opacity; MaintenanceWindowStartCombo.IsEnabled = _isEditable && !anyTime; MaintenanceWindowStartCombo.Opacity = opacity; MaintenanceWindowEndCombo.IsEnabled = _isEditable && !anyTime; MaintenanceWindowEndCombo.Opacity = opacity; MaintenanceWindowFrequencyLabel.Opacity = opacity; MaintenanceWindowFrequencyCombo.IsEnabled = _isEditable; MaintenanceWindowFrequencyCombo.Opacity = opacity; MaintenanceWindowMonthlyModeLabel.Opacity = opacity; MaintenanceWindowMonthlyModeCombo.IsEnabled = _isEditable; MaintenanceWindowMonthlyModeCombo.Opacity = opacity; MaintenanceWindowDayOfWeekLabel.Opacity = opacity; MaintenanceWindowDayOfWeekCombo.IsEnabled = _isEditable; MaintenanceWindowDayOfWeekCombo.Opacity = opacity; MaintenanceWindowDayOfMonthLabel.Opacity = opacity; MaintenanceWindowDayOfMonthCombo.IsEnabled = _isEditable; MaintenanceWindowDayOfMonthCombo.Opacity = opacity; MaintenanceWindowOrdinalLabel.Opacity = opacity; MaintenanceWindowOrdinalCombo.IsEnabled = _isEditable; MaintenanceWindowOrdinalCombo.Opacity = opacity; } } }