From a1122d7666e4f98eaab823473d8f7a99468d6b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Mon, 13 Apr 2026 15:29:53 +0200 Subject: [PATCH] kill AM/PM from the schedule picker, enforce iso 8601 24h everywhere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit native inherits the browser/OS locale; on chrome/macos in en-US that means AM/PM in the settings schedule editor. no attribute turns it off — lang="en-GB" is not honored. swap for a tiny headless TimeInput (two number fields, HH clamp 0..23, MM clamp 0..59) that always emits HH:MM. also drop toLocaleString() in ScanPage, which silently flips thousands separators in de-DE / fr-FR. add formatThousands() that always produces the 1,234,567 form regardless of locale. shared/components/ui/time-input.tsx + shared/lib/utils.ts#formatThousands are now the canonical helpers — every future time/number render must use them. captured as a forward-looking feedback memory so this doesn't regress. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- src/features/scan/ScanPage.tsx | 3 +- src/features/settings/SettingsPage.tsx | 17 ++---- src/shared/components/ui/time-input.tsx | 78 +++++++++++++++++++++++++ src/shared/lib/utils.ts | 7 +++ 5 files changed, 92 insertions(+), 15 deletions(-) create mode 100644 src/shared/components/ui/time-input.tsx diff --git a/package.json b/package.json index 260869d..c4e3f41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "netfelix-audio-fix", - "version": "2026.04.13.7", + "version": "2026.04.13.8", "scripts": { "dev:server": "NODE_ENV=development bun --hot server/index.tsx", "dev:client": "vite", diff --git a/src/features/scan/ScanPage.tsx b/src/features/scan/ScanPage.tsx index f8778d9..7d27f46 100644 --- a/src/features/scan/ScanPage.tsx +++ b/src/features/scan/ScanPage.tsx @@ -4,6 +4,7 @@ import { Alert } from "~/shared/components/ui/alert"; import { Badge } from "~/shared/components/ui/badge"; import { Button } from "~/shared/components/ui/button"; import { api } from "~/shared/lib/api"; +import { formatThousands } from "~/shared/lib/utils"; interface ScanStatus { running: boolean; @@ -32,7 +33,7 @@ function StatCard({ label, value, danger }: { label: string; value: number; dang return (
- {value.toLocaleString()} + {formatThousands(value)}
{label}
diff --git a/src/features/settings/SettingsPage.tsx b/src/features/settings/SettingsPage.tsx index 46cfbbd..10d8026 100644 --- a/src/features/settings/SettingsPage.tsx +++ b/src/features/settings/SettingsPage.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Button } from "~/shared/components/ui/button"; import { Input } from "~/shared/components/ui/input"; import { Select } from "~/shared/components/ui/select"; +import { TimeInput } from "~/shared/components/ui/time-input"; import { api } from "~/shared/lib/api"; import { LANG_NAMES } from "~/shared/lib/lang"; @@ -273,20 +274,10 @@ function WindowEditor({ {window.enabled && (
- onChange({ ...window, start: e.target.value })} - className="w-28" - /> + onChange({ ...window, start: next })} /> to - onChange({ ...window, end: e.target.value })} - className="w-28" - /> - (overnight ranges wrap midnight) + onChange({ ...window, end: next })} /> + (24h, overnight ranges wrap midnight)
)} diff --git a/src/shared/components/ui/time-input.tsx b/src/shared/components/ui/time-input.tsx new file mode 100644 index 0000000..42b09b1 --- /dev/null +++ b/src/shared/components/ui/time-input.tsx @@ -0,0 +1,78 @@ +import type { ChangeEvent } from "react"; +import { cn } from "~/shared/lib/utils"; + +/** + * ISO 8601 24-hour time input that never leaks AM/PM, never respects + * browser/OS locale, and always emits `HH:MM`. Two plain number fields + * with hard clamps — deliberately simpler than a real masked input so + * there's nothing to go wrong in foreign locales. + */ +export function TimeInput({ + value, + onChange, + disabled, + className, +}: { + /** "HH:MM" — always 24-hour. Empty string is allowed and rendered as blank. */ + value: string; + onChange: (next: string) => void; + disabled?: boolean; + className?: string; +}) { + const [rawH, rawM] = (value ?? "").split(":"); + const hh = rawH ?? ""; + const mm = rawM ?? ""; + + const emit = (nextH: string, nextM: string) => { + // Clamp + pad on emit; empty segments become "00" so downstream schedulers + // always see a parseable "HH:MM". Users can still type single digits while + // editing — clamping happens onBlur, not per-keystroke. + const h = clamp(nextH, 23); + const m = clamp(nextM, 59); + onChange(`${pad(h)}:${pad(m)}`); + }; + + const onHourChange = (e: ChangeEvent) => emit(e.target.value, mm); + const onMinuteChange = (e: ChangeEvent) => emit(hh, e.target.value); + + return ( +
+ + : + +
+ ); +} + +function clamp(raw: string, max: number): number { + const n = Number.parseInt(raw, 10); + if (!Number.isFinite(n) || n < 0) return 0; + if (n > max) return max; + return n; +} + +function pad(n: number): string { + return n.toString().padStart(2, "0"); +} diff --git a/src/shared/lib/utils.ts b/src/shared/lib/utils.ts index ac680b3..040decf 100644 --- a/src/shared/lib/utils.ts +++ b/src/shared/lib/utils.ts @@ -4,3 +4,10 @@ import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +/** Locale-free thousands separator. Always emits the comma form (1,234,567) — + * never defers to the browser's locale, which can silently flip to "1.234.567" + * in de-DE or "1 234 567" in fr-FR and make numbers unreadable across users. */ +export function formatThousands(n: number): string { + return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +}