Files
minstrel/web/src/lib/components/NotificationEmailCard.svelte
T
bvandeusenandClaude Opus 5.5 10d50dccbd docs: web comments say what the app does, not how it came to be (#6277)
Rewrites the web app's comments to rule 32. Task, milestone, rule and
spec ids are gone; regression stories are restated as the trap they
guard (public auth routes, the menu's stopPropagation, universal loads
under adapter-static, metric deltas with their margin). Flutter and
Tauri mentions now name the Android app or a native shell, operator
quotes and dates are dropped, and one library's counts no longer
appear outside test fixtures. Verified with a TypeScript-scanner token
comparison (template literals and regex literals re-scanned): only
comment text changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:42:12 -04:00

94 lines
3.3 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import {
getNotificationEmailSettings,
updateNotificationEmailSettings,
type NotificationEmailSettings
} from '#lib/api/admin.js';
import { errMessage } from '#lib/api/errors.js';
import { pushToast } from '#lib/stores/toast.svelte.js';
import SettingsGroup from '#lib/components/settings/SettingsGroup.svelte';
import SettingRow from '#lib/components/settings/SettingRow.svelte';
// When notification emails go out. Nothing is emailed per
// event: new music is a daily summary, everything else a batch.
const button =
'rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary disabled:opacity-50';
const field = 'w-24 rounded-md border border-border bg-background px-2 py-1.5 text-sm';
let saved = $state<NotificationEmailSettings | null>(null);
let form = $state<NotificationEmailSettings | null>(null);
let saving = $state(false);
let loadError = $state(false);
const dirty = $derived(
!!saved &&
!!form &&
(saved.summary_hour !== form.summary_hour ||
saved.batch_window_minutes !== form.batch_window_minutes)
);
const hours = Array.from({ length: 24 }, (_, h) => h);
function hourLabel(h: number): string {
return `${String(h).padStart(2, '0')}:00`;
}
async function load() {
try {
saved = await getNotificationEmailSettings();
form = { ...saved };
loadError = false;
} catch {
loadError = true;
}
}
onMount(load);
async function save() {
if (!form) return;
saving = true;
try {
saved = await updateNotificationEmailSettings(form);
form = { ...saved };
pushToast('Notification email schedule saved.');
} catch (e) {
// The server names the field it refused (errMessage, not e.message:
// the client throws a plain object).
pushToast(errMessage(e), 'error');
} finally {
saving = false;
}
}
</script>
<SettingsGroup title="Notification emails" id="notification-emails" hint="Never one per event. Each person picks theirs in their own settings.">
{#if loadError}
<p class="px-4 py-3 text-sm text-error-fg">
Couldn't load the email schedule.
<button type="button" class="underline hover:no-underline" onclick={load}>Try again</button>
</p>
{:else if form === null}
<p class="px-4 py-3 text-sm text-text-secondary">Loading the email schedule…</p>
{:else}
<SettingRow label="New music summary" for="summary-hour" hint="Daily, at this hour in each person's timezone.">
<select id="summary-hour" bind:value={form.summary_hour} class={field}>
{#each hours as h (h)}
<option value={h}>{hourLabel(h)}</option>
{/each}
</select>
</SettingRow>
<SettingRow label="Batch everything else" for="batch-window" hint="One email this long after the first item.">
<input id="batch-window" type="number" min="15" max="1440" bind:value={form.batch_window_minutes} class={field} />
<span class="text-sm text-text-secondary">minutes</span>
</SettingRow>
<div class="flex justify-end px-4 py-3">
<button type="button" class={button} disabled={!dirty || saving} onclick={save}>
{saving ? 'Saving…' : 'Save'}
</button>
</div>
{/if}
</SettingsGroup>