Drei neue app_settings-Spalten (Migration AddPowerUseWindow): power_use_window_start/end (HH:mm, lokal, nullable) und power_use_handoff_pct (NOT NULL DEFAULT 95, geclampt 50-99). Die Fensterlogik liegt als reine Funktion in PowerUseWindow (Contains/Normalize/IsOpen): beide Grenzen leer oder nur eine gesetzt = rund um die Uhr, Ende vor Start wrappt ueber Mitternacht, Grenzen halboffen. Geprueft wird in ComposeHandlerPromptAsync, also pro Phase zum Zeitpunkt des Prompt-Baus - die Uhr ist per Func<DateTime> injizierbar. Bedienung im Usage-Monitor neben dem PowerUse-Toggle (Commit auf Blur/Enter, ein Write fuer alle vier Felder); in Settings->General unsichtbar, aber load->save durchgereicht. Nebenbefund, gleicher Pfad: AppSettingsRepository.UpdateAsync kopiert Feld fuer Feld und hat ModelPresets, PowerUseEnabled und ListHandlerMaxHandoffs nie geschrieben - jeder Save dieser drei war still wirkungslos. Mit ergaenzt und per Round-trip-Test gepinnt.
33 lines
1.6 KiB
C#
33 lines
1.6 KiB
C#
namespace ClaudeDo.Data.Models;
|
|
|
|
/// <summary>
|
|
/// The optional local-time window restricting when PowerUse applies
|
|
/// (<see cref="AppSettingsEntity.PowerUseWindowStart"/> / <c>End</c>, both <c>HH:mm</c>).
|
|
/// Pure on purpose — the caller supplies "now", so the window logic is testable without a clock.
|
|
/// </summary>
|
|
public static class PowerUseWindow
|
|
{
|
|
/// <summary>Parses an <c>HH:mm</c> string; anything else (including null/blank) is null.</summary>
|
|
public static TimeOnly? Parse(string? value)
|
|
=> TimeOnly.TryParseExact(value?.Trim(), "HH:mm", out var t) ? t : null;
|
|
|
|
/// <summary>Round-trips a stored/typed value through <see cref="Parse"/>, so an unparseable
|
|
/// string is stored as null instead of sitting in the column pretending to be a window.</summary>
|
|
public static string? Normalize(string? value) => Parse(value)?.ToString("HH:mm");
|
|
|
|
/// <summary>
|
|
/// True when <paramref name="now"/> falls inside the window. Half-open: start is included,
|
|
/// end is not. Either bound missing = no window at all = always true (the pre-window
|
|
/// behaviour). A window whose end is before its start wraps over midnight.
|
|
/// </summary>
|
|
public static bool Contains(TimeOnly now, TimeOnly? start, TimeOnly? end)
|
|
{
|
|
if (start is not { } from || end is not { } to) return true;
|
|
return from < to ? now >= from && now < to : now >= from || now < to;
|
|
}
|
|
|
|
/// <summary>Convenience overload for the two stored strings and a wall clock.</summary>
|
|
public static bool IsOpen(DateTime now, string? start, string? end)
|
|
=> Contains(TimeOnly.FromDateTime(now), Parse(start), Parse(end));
|
|
}
|