authentication and calendar-sync passwords keyed (214 -> 163)

Authentication reaches zero -- sign-in, bootstrap, forgot and reset password. These
are the first words a new account reads, which is why the brief ranked them above
the larger screens.

DavAppPasswords had the third toLocaleDateString(undefined, ...) in the shell; it now
uses useFormat().date like the other two.

DebugPanel's three labels are keyed too. Worth knowing rather than acting on: that
panel is rendered behind showDebug, which Background.tsx hardcodes to false, so it
never appears. Keyed rather than deleted because removing it is not my call.
This commit is contained in:
2026-08-23 05:15:22 +00:00
parent 7f8455f1af
commit b978e09327
9 changed files with 189 additions and 63 deletions
@@ -7,6 +7,8 @@ import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { useClient } from 'hooks/useClient';
import { copyToClipboard } from 'helpers/clipboard';
import { useFormat } from 'i18n';
import { useTranslation } from '@/lib/i18n';
// Per-device credentials for calendar and contacts sync (DAVx5, iOS, macOS, Thunderbird).
//
@@ -26,19 +28,21 @@ type DavPassword = {
type Minted = { password: string; username: string; label: string };
const formatDate = (value: string | null) =>
value ? new Date(value).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) : null;
const copy = async (text: string, what: string) => {
try {
await copyToClipboard(text);
toast.success(`${what} copied`);
} catch {
toast.error('Could not copy — select and copy manually');
}
};
// Dates come from useFormat() below, bound to the app's locale rather than the browser's (D7).
export const DavAppPasswords = () => {
const { t } = useTranslation();
const { date: formatDate } = useFormat();
const copy = async (text: string, what: string) => {
try {
await copyToClipboard(text);
toast.success(t('settings.dav.copied', { what }));
} catch {
toast.error(t('settings.dav.copyFailed'));
}
};
const client = useClient();
const [passwords, setPasswords] = useState<DavPassword[]>([]);
const [isLoading, setIsLoading] = useState(true);
@@ -71,7 +75,7 @@ export const DavAppPasswords = () => {
setLabel('');
load();
} catch {
toast.error('Could not create the app password');
toast.error(t('settings.dav.createFailed'));
} finally {
setIsCreating(false);
}
@@ -80,10 +84,10 @@ export const DavAppPasswords = () => {
const revoke = async (id: number) => {
try {
await client.post(`/dav/passwords/${id}/revoke`, {});
toast.success('Revoked — that device will stop syncing');
toast.success(t('settings.dav.revoked'));
load();
} catch {
toast.error('Could not revoke');
toast.error(t('settings.dav.revokeFailed'));
}
};
@@ -92,7 +96,7 @@ export const DavAppPasswords = () => {
await client.delete(`/dav/passwords/${id}`);
load();
} catch {
toast.error('Could not delete');
toast.error(t('settings.dav.deleteFailed'));
}
};
@@ -126,7 +130,12 @@ export const DavAppPasswords = () => {
<code className="min-w-0 flex-1 truncate rounded bg-background/70 px-2 py-1.5 font-mono text-xs">
{value}
</code>
<Button variant="ghost" size="sm" onClick={() => copy(value, field)} title={`Copy ${field}`}>
<Button
variant="ghost"
size="sm"
onClick={() => copy(value, field)}
title={t('settings.dav.copyField', { field })}
>
<Copy className="h-3.5 w-3.5" />
</Button>
</div>
@@ -146,29 +155,31 @@ export const DavAppPasswords = () => {
<form onSubmit={create} className="flex items-end gap-2">
<Label className="grid flex-1 gap-2">
<span className="text-duck-dark/70 dark:text-foreground/70">New device</span>
<span className="text-duck-dark/70 dark:text-foreground/70">{t('settings.dav.newDevice')}</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20"
value={label}
onChange={(ev) => setLabel(ev.target.value)}
placeholder="Pixel 9, iPad, Thunderbird…"
placeholder={t('settings.dav.placeholder')}
/>
</Label>
<Button type="submit" disabled={!label.trim() || isCreating} className="h-11">
<Plus className="mr-1.5 h-4 w-4" />
Create
{t('settings.dav.create')}
</Button>
</form>
{passwords.length === 0 ? (
<div className="rounded-lg border border-dashed border-duck-dark/15 dark:border-foreground/15 p-6 text-center text-sm text-duck-dark/40 dark:text-foreground/40">
No devices yet.
{t('settings.dav.none')}
</div>
) : (
<div className="grid gap-2">
{passwords.map((entry) => {
const revoked = !!entry.revokedAt;
const lastUsed = formatDate(entry.lastUsedAt);
const lastUsed = entry.lastUsedAt
? formatDate(entry.lastUsedAt, { month: 'short', day: 'numeric', year: 'numeric' })
: null;
return (
<div
key={entry.id}
@@ -192,7 +203,7 @@ export const DavAppPasswords = () => {
</div>
</div>
{!revoked && (
<Button variant="ghost" size="sm" onClick={() => revoke(entry.id)} title="Revoke">
<Button variant="ghost" size="sm" onClick={() => revoke(entry.id)} title={t('settings.dav.revoke')}>
<Ban className="h-3.5 w-3.5" />
</Button>
)}
@@ -200,7 +211,7 @@ export const DavAppPasswords = () => {
variant="ghost"
size="sm"
onClick={() => remove(entry.id)}
title="Delete"
title={t('settings.dav.delete')}
className="hover:text-red-500"
>
<Trash2 className="h-3.5 w-3.5" />