feat(web): send timezone on login + bootstrap + weekly
Web client posts Intl.DateTimeFormat().resolvedOptions().timeZone to PUT /api/me/timezone after every successful login, register, and bootstrap when the locally-stored tz_last_sent_at is >7 days old. Cadence tracked in localStorage keeps the server stateless on the "is this stale?" check. Failures swallowed: the server's UTC default + last-known value keep the scheduler functioning until the next successful attempt. SSR- safe via the typeof window guard. For #392 Half B. Companion Flutter change in next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,3 +53,12 @@ export async function getAPIToken(): Promise<APITokenResponse> {
|
||||
export async function regenerateAPIToken(): Promise<APITokenResponse> {
|
||||
return api.post<APITokenResponse>('/api/me/api-token', {});
|
||||
}
|
||||
|
||||
// Submits the browser's current IANA timezone for the authenticated
|
||||
// user. Called from the auth store on login + bootstrap + once weekly
|
||||
// (cadence tracked client-side in localStorage). Failures are
|
||||
// non-fatal — the server's UTC fallback keeps the scheduler working
|
||||
// until the next attempt.
|
||||
export async function putMyTimezone(timezone: string): Promise<void> {
|
||||
await api.put<void>('/api/me/timezone', { timezone });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { api, type User, type LoginResponse } from '$lib/api/client';
|
||||
import { putMyTimezone } from '$lib/api/me';
|
||||
import { queryClient } from '$lib/query/client';
|
||||
import { signalSessionEnd } from './sessionEnd.svelte';
|
||||
import { user, setUser } from './user.svelte';
|
||||
@@ -7,9 +8,34 @@ import { user, setUser } from './user.svelte';
|
||||
// callers keep working. New code can import directly from auth/user.svelte.
|
||||
export { user };
|
||||
|
||||
// Weekly client-driven cadence for sending the browser's current IANA
|
||||
// timezone to PUT /api/me/timezone (#392 Half B). Tracked in
|
||||
// localStorage so the cadence survives tab restarts; bumped after each
|
||||
// successful PUT. Failures are swallowed — server keeps its previous
|
||||
// value (or 'UTC' default).
|
||||
const TZ_LAST_SENT_KEY = 'minstrel.tz_last_sent_at';
|
||||
const WEEKLY_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
async function sendTimezoneIfStale(): Promise<void> {
|
||||
if (typeof window === 'undefined') return; // SSR-safe no-op
|
||||
try {
|
||||
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (!tz) return;
|
||||
const lastStr = window.localStorage.getItem(TZ_LAST_SENT_KEY);
|
||||
const last = lastStr ? Number(lastStr) : 0;
|
||||
if (Number.isFinite(last) && Date.now() - last < WEEKLY_MS) return;
|
||||
await putMyTimezone(tz);
|
||||
window.localStorage.setItem(TZ_LAST_SENT_KEY, String(Date.now()));
|
||||
} catch (err) {
|
||||
console.warn('tz send failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function bootstrap(): Promise<void> {
|
||||
try {
|
||||
setUser(await api.get<User>('/api/me'));
|
||||
// ignore: best-effort, runs in background
|
||||
void sendTimezoneIfStale();
|
||||
} catch {
|
||||
setUser(null);
|
||||
}
|
||||
@@ -18,6 +44,7 @@ export async function bootstrap(): Promise<void> {
|
||||
export async function login(username: string, password: string): Promise<void> {
|
||||
const res = await api.post<LoginResponse>('/api/auth/login', { username, password });
|
||||
setUser(res.user);
|
||||
void sendTimezoneIfStale();
|
||||
}
|
||||
|
||||
export async function register(opts: {
|
||||
@@ -35,6 +62,7 @@ export async function register(opts: {
|
||||
const res = await api.post<LoginResponse>('/api/auth/register', body);
|
||||
setUser(res.user);
|
||||
await bootstrap();
|
||||
void sendTimezoneIfStale();
|
||||
}
|
||||
|
||||
export async function forgotPassword(email: string): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user