Validate daily schedule start times

Disable schedule activation until the local time input matches a strict HH:MM value.

Keep a defensive activation guard that surfaces a localized validation error and skips persistence when invoked with invalid state.

Add red-green coverage for empty, malformed, out-of-range hour, and out-of-range minute values.
This commit is contained in:
Sucukdeluxe
2026-08-22 10:12:25 +02:00
parent 30bedd17dd
commit 2e94484274
5 changed files with 54 additions and 7 deletions
+23 -4
View File
@@ -1534,6 +1534,21 @@ export function buildDailyScheduleSettingsUpdate(
};
}
export async function activateDailyScheduleSettings(
time: string,
startDay: DailyScheduleStartDay,
persist: (update: RendererSettingsUpdate) => Promise<boolean>,
showError: (message: string) => void,
now = new Date()
): Promise<boolean> {
const update = buildDailyScheduleSettingsUpdate(time, startDay, now);
if (!update) {
showError("Bitte eine gültige Startzeit auswählen.");
return false;
}
return persist(update);
}
export async function persistDailyScheduleSettingsUpdate(
update: RendererSettingsUpdate,
operation: "activate" | "cancel",
@@ -4771,14 +4786,17 @@ export function App(): ReactElement {
), [applyAuthoritativeDailyScheduleSnapshot, applyDailyScheduleSettings, showToast]);
const activateDownloadSchedule = useCallback((): void => {
const update = buildDailyScheduleSettingsUpdate(scheduleTimeInput, scheduleStartDay);
if (!update) return;
void persistDownloadSchedule(update, "activate").then((persisted) => {
void activateDailyScheduleSettings(
scheduleTimeInput,
scheduleStartDay,
(update) => persistDownloadSchedule(update, "activate"),
(message) => showToast(message, 2800)
).then((persisted) => {
if (persisted) {
setSchedulePickerOpen(false);
}
});
}, [persistDownloadSchedule, scheduleStartDay, scheduleTimeInput]);
}, [persistDownloadSchedule, scheduleStartDay, scheduleTimeInput, showToast]);
const removeActionableDownloads = useCallback((): void => {
const ids = new Set(downloadsViewCore.actionableSelectedIds);
@@ -4812,6 +4830,7 @@ export function App(): ReactElement {
scheduleActive: snapshot.settings.dailyStartEnabled || snapshot.settings.scheduledStartEpochMs > 0,
scheduleOpen: schedulePickerOpen,
scheduleTime: scheduleTimeInput,
scheduleTimeValid: buildDailyScheduleSettingsUpdate(scheduleTimeInput, scheduleStartDay) !== null,
scheduleStartDay,
scheduleLabel: scheduleCountdown || (snapshot.settings.dailyStartEnabled
? formatDailyScheduleTime(snapshot.settings.dailyStartMinuteOfDay)
+1 -1
View File
@@ -63,7 +63,7 @@ const pairs = [
["Abgeschlossene und gelöschte Pakete erscheinen hier.", "Completed and deleted packages appear here."], ["Passe Filter oder Suche an.", "Adjust the filter or search."], ["Öffne die Ansicht erneut, um es noch einmal zu versuchen.", "Open the view again to retry."],
["Alle sichtbaren Einträge auswählen", "Select all visible entries"], ["Details anzeigen", "Show details"], ["Details ausblenden", "Hide details"],
["Sichtbar:", "Visible:"], ["pro Seite", "per page"],
["Verfügbarkeit", "Availability"], ["Hinzugefügt am", "Added on"], ["Ungeprüft", "Unchecked"], ["Paket gestoppt", "Package stopped"], ["Alle anzeigen", "Show all"], ["Planen", "Schedule"], ["Startzeit", "Start time"], ["Starttag", "Start day"], ["Ab heute", "Starting today"], ["Ab morgen", "Starting tomorrow"],
["Verfügbarkeit", "Availability"], ["Hinzugefügt am", "Added on"], ["Ungeprüft", "Unchecked"], ["Paket gestoppt", "Package stopped"], ["Alle anzeigen", "Show all"], ["Planen", "Schedule"], ["Startzeit", "Start time"], ["Starttag", "Start day"], ["Ab heute", "Starting today"], ["Ab morgen", "Starting tomorrow"], ["Bitte eine gültige Startzeit auswählen.", "Select a valid start time."],
["Keine Downloads", "No downloads"], ["Keine passenden Downloads", "No matching downloads"], ["Füge Links hinzu, um Downloads vorzubereiten.", "Add links to prepare downloads."], ["Passe Filter oder Suche an.", "Adjust the filter or search."],
["Keine Links gesammelt", "No links collected"], ["Keine passenden Links", "No matching links"], ["Füge Links oder Text ein, um sie zu sammeln.", "Paste links or text to collect them."], ["Links durchsuchen", "Search links"],
["Datenmenge", "Data volume"], ["Sitzungszähler", "Session counter"], ["Sieben Tage", "Seven days"], ["30 Tage", "30 days"], ["Zeitraum", "Period"], ["Erfolgreich", "Successful"],
@@ -39,9 +39,10 @@ export interface DownloadsViewModel extends DownloadsViewModelCore {
reconnectSeconds: number;
reconnectReason: string;
clipboardWatcher: boolean;
scheduleActive: boolean;
scheduleActive: boolean;
scheduleOpen: boolean;
scheduleTime: string;
scheduleTimeValid: boolean;
scheduleStartDay: DailyScheduleStartDay;
scheduleLabel: string;
packageSpeedBps: Record<string, number>;
@@ -141,7 +142,7 @@ export function DownloadsToolbar({ actions, model }: { actions: DownloadsViewAct
<span {...(!scheduleSlotOpen ? { inert: "true" } : {})} aria-hidden={!scheduleSlotOpen} className="downloads-schedule-controls">
{model.scheduleActive
? <><strong>Geplant: {model.scheduleLabel}</strong><button disabled={false} onClick={actions.onCancelSchedule} type="button">Abbrechen</button></>
: <><input aria-label="Startzeit" disabled={!scheduleSlotOpen} onChange={(event) => actions.onScheduleTimeChange(event.target.value)} type="time" value={model.scheduleTime} /><select aria-label="Starttag" disabled={!scheduleSlotOpen} onChange={(event) => actions.onScheduleStartDayChange(event.target.value as DailyScheduleStartDay)} value={model.scheduleStartDay}><option value="today">Ab heute</option><option value="tomorrow">Ab morgen</option></select><button disabled={!scheduleSlotOpen} onClick={actions.onActivateSchedule} type="button">Planen</button></>}
: <><input aria-label="Startzeit" disabled={!scheduleSlotOpen} onChange={(event) => actions.onScheduleTimeChange(event.target.value)} type="time" value={model.scheduleTime} /><select aria-label="Starttag" disabled={!scheduleSlotOpen} onChange={(event) => actions.onScheduleStartDayChange(event.target.value as DailyScheduleStartDay)} value={model.scheduleStartDay}><option value="today">Ab heute</option><option value="tomorrow">Ab morgen</option></select><button disabled={!scheduleSlotOpen || !model.scheduleTimeValid} onClick={actions.onActivateSchedule} type="button">Planen</button></>}
</span>
</span>
<span className="downloads-toolbar-divider" />