Check, format and test / build (push) Successful in 11m10s
The "Edit address" field in Settings only accepted an IPv4 address (IPV4.test), rejecting a bare hostname (e.g. "timbratore") or FQDN (e.g. "timbratore.lan") even though the underlying connection (Rust's ToSocketAddrs) already resolves those fine. Onboarding had no such restriction. Replaced the IPv4-only check with isValidHost (IPv4 OR a standard DNS hostname), dropped the numeric-only keyboard hint on that field, and reworded the labels/placeholder error to not imply IP-only. Also makes "Edit address" test the connection before accepting it, instead of saving unconditionally: ActionButton gained a `disabled` prop so the confirm/cancel buttons can be locked (with a spinner on confirm) while the test is in flight, and the edit form now only closes on a successful test, staying open with the raw error shown otherwise. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
51 lines
2.1 KiB
TypeScript
51 lines
2.1 KiB
TypeScript
import { parseT } from '$lib/time';
|
|
import type { Shifts } from '$lib/shifts';
|
|
|
|
export type { Shifts };
|
|
|
|
const IPV4 = /^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
|
|
// A DNS label: alphanumeric, hyphens allowed in the middle, 1-63 chars.
|
|
const LABEL = '[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?';
|
|
const HOSTNAME = new RegExp(`^${LABEL}(?:\\.${LABEL})*$`);
|
|
|
|
/** Whether `v` is a usable time clock address: an IPv4 address, a bare
|
|
* hostname (e.g. "timbratore") or a FQDN (e.g. "timbratore.lan"). */
|
|
export const isValidHost = (v: string): boolean => IPV4.test(v) || HOSTNAME.test(v);
|
|
|
|
/** The 4 time fields of `Shifts` — everything but `round_step_min`, which isn't a time. */
|
|
export type ShiftTimeKey = 'in1' | 'out1' | 'in2' | 'out2';
|
|
|
|
export const SHIFT_ROWS: { titleKey: string; a: ShiftTimeKey; b: ShiftTimeKey }[] = [
|
|
{ titleKey: 'settings.shift_1', a: 'in1', b: 'out1' },
|
|
{ titleKey: 'settings.shift_2', a: 'in2', b: 'out2' }
|
|
];
|
|
|
|
export interface ShiftCheck {
|
|
minutes: Record<ShiftTimeKey, number> | null;
|
|
bad: Partial<Record<ShiftTimeKey, true>>;
|
|
errorKey: string | null;
|
|
}
|
|
|
|
export function checkShifts(shifts: Shifts): ShiftCheck {
|
|
const t: Record<ShiftTimeKey, number | null> = {
|
|
in1: parseT(shifts.in1),
|
|
out1: parseT(shifts.out1),
|
|
in2: parseT(shifts.in2),
|
|
out2: parseT(shifts.out2)
|
|
};
|
|
const bad: Partial<Record<ShiftTimeKey, true>> = {};
|
|
for (const key of Object.keys(t) as ShiftTimeKey[]) {
|
|
if (t[key] === null) bad[key] = true;
|
|
}
|
|
if (Object.keys(bad).length) return { minutes: null, bad, errorKey: 'settings.shift_error_format' };
|
|
|
|
const n = t as Record<ShiftTimeKey, number>;
|
|
if (n.out1 <= n.in1) return { minutes: null, bad: { out1: true }, errorKey: 'settings.shift_error_out1' };
|
|
if (n.in2 < n.out1) return { minutes: null, bad: { in2: true }, errorKey: 'settings.shift_error_in2' };
|
|
if (n.out2 <= n.in2) return { minutes: null, bad: { out2: true }, errorKey: 'settings.shift_error_out2' };
|
|
return { minutes: n, bad: {}, errorKey: null };
|
|
}
|
|
|
|
export const roundValid = (v: string): boolean => /^\d{1,2}$/.test(v) && Number(v) >= 1 && Number(v) <= 60;
|