radio: quiet hours for reminders (default 23:00-07:30 Amsterdam, RADIO_QUIET_HOURS overrides)
All checks were successful
Deploy production / deploy (push) Successful in 1m21s

This commit is contained in:
2026-10-26 17:18:59 +00:00
parent 95a8b76562
commit f954881bcc
6 changed files with 74 additions and 5 deletions

View File

@@ -4,10 +4,13 @@
import {
ISS_DOWNLINK_HZ,
ISS_MIN_EL,
QUIET_HOURS,
REMIND_MINUTES,
compass,
dayKey,
fmtTime,
formatFreq,
parseQuiet,
relDay,
until
} from './index.js';
@@ -100,7 +103,8 @@
<p class="meta foot">
{tle.name} TLE from Celestrak, epoch {tle.epoch ? fmtAgo(tle.epoch) : '?'}, fetched {tle.fetchedAt
? fmtAgo(tle.fetchedAt)
: '?'} (refreshed daily).
: '?'} (refreshed daily). Passes of {ISS_MIN_EL}° and up get a reminder {REMIND_MINUTES} min before,
except during quiet hours ({parseQuiet(QUIET_HOURS)?.label}).
</p>
{/if}

View File

@@ -12,7 +12,9 @@
BANDS,
BAND_LABEL,
DAY_SHORT,
QUIET_HOURS,
REMIND_MINUTES,
parseQuiet,
daysLabel,
fmtTime,
formatFreq,
@@ -118,7 +120,8 @@
<section class="card">
<div class="bar">
<p class="meta">
Times are Amsterdam time. A bell sends an ntfy reminder {REMIND_MINUTES} minutes before each start.
Times are Amsterdam time. A bell sends an ntfy reminder {REMIND_MINUTES} minutes before each start,
except during quiet hours ({parseQuiet(QUIET_HOURS)?.label}).
</p>
<button
type="button"

View File

@@ -1,5 +1,15 @@
import { describe, expect, it } from 'vitest';
import { alphaTag, bankOf, nextOccurrence, occurrences, parseFreq, scannerCsv } from './index';
import {
alphaTag,
bankOf,
fromZoned,
inQuiet,
parseQuiet,
nextOccurrence,
occurrences,
parseFreq,
scannerCsv
} from './index';
describe('parseFreq', () => {
it('reads MHz, kHz and Hz', () => {
@@ -77,3 +87,17 @@ describe('scanner', () => {
).toBeLessThanOrEqual(16);
});
});
describe('quiet hours', () => {
const q = parseQuiet('23:00-07:30');
it('parses and wraps midnight (Amsterdam time)', () => {
expect(q?.label).toBe('23:00–07:30');
expect(parseQuiet('off')).toBeNull();
expect(inQuiet(fromZoned(2026, 10, 27, 2, 11), q)).toBe(true);
expect(inQuiet(fromZoned(2026, 10, 27, 7, 1), q)).toBe(true);
expect(inQuiet(fromZoned(2026, 10, 27, 7, 30), q)).toBe(false);
expect(inQuiet(fromZoned(2026, 10, 26, 18, 55), q)).toBe(false);
expect(inQuiet(fromZoned(2026, 10, 26, 23, 0), q)).toBe(true);
expect(inQuiet(fromZoned(2026, 10, 26, 18, 55), null)).toBe(false);
});
});

View File

@@ -40,6 +40,20 @@ export const ALPHA_TAG_MAX = 16;
export const ISS_MIN_EL = 10;
export const REMIND_MINUTES = 10;
export const ISS_DOWNLINK_HZ = 145_800_000;
/** No reminders are sent when the send time falls in this Amsterdam window (RADIO_QUIET_HOURS overrides). */
export const QUIET_HOURS = '23:00-07:30';
export type Quiet = { from: number; to: number; label: string } | null;
/** "23:00-07:30" -> minutes of day; "off" / "" -> null (no quiet hours). */
export function parseQuiet(spec: string | null | undefined): Quiet {
const m = /^\s*(\d{1,2}):(\d{2})\s*[-–]\s*(\d{1,2}):(\d{2})\s*$/.exec(spec ?? '');
if (!m) return null;
const from = Number(m[1]) * 60 + Number(m[2]);
const to = Number(m[3]) * 60 + Number(m[4]);
if (from > 1440 || to > 1440 || from === to) return null;
const pad = (n: number) =>
`${String(Math.floor(n / 60)).padStart(2, '0')}:${String(n % 60).padStart(2, '0')}`;
return { from, to, label: `${pad(from)}–${pad(to)}` };
}
export const DAY_SHORT = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const;
@@ -312,3 +326,11 @@ export function alphaTag(name: string, marineCh?: string | null): string {
if (marineCh) s = `${marineCh} ${s}`;
return s.slice(0, ALPHA_TAG_MAX).trim();
}
/** Whether an instant falls in the quiet window (Amsterdam wall clock; the window may wrap midnight). */
export function inQuiet(at: Date, quiet: Quiet): boolean {
if (!quiet) return false;
const z = zoned(at);
const t = z.h * 60 + z.min;
return quiet.from < quiet.to ? t >= quiet.from && t < quiet.to : t >= quiet.from || t < quiet.to;
}