diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index 63ca5de..4c9fa6a 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -4748,6 +4748,40 @@ test('en ny note hentes ikke igen, når den lige er lavet', async ({ page }) => expect(trouble).toEqual([]); }); +/** + * Et totalt udfald siger "ingen forbindelse" — ikke "log ind". + * + * Opstarten spørger serveren, hvem man er. Når svaret aldrig kommer, står `user` + * som null, og null betyder "logget ud" alle andre steder i programmet — så et + * udfald blev vist som login-skærmen. Appen gav altså sessionen skylden for, at + * nettet var nede, og tilbød et kodeordsfelt, der umuligt kunne virke. + * + * Det er værre end en fejlbesked, fordi det ligner noget, der kunne virke: man + * taster kodeordet, der sker ingenting, og man konkluderer, at kontoen er i + * stykker. Det skete i virkeligheden 17. september 2026, da instansens DNS-post + * blev slettet. + * + * Prøven afbryder API-kaldene frem for at stoppe serveren: en afbrudt forespørgsel + * er præcis det, en manglende DNS-post giver browseren — fetch afviser, uden at der + * nogensinde kom en status. En 500 ville være serveren, der svarer, og det er den + * anden sag. + */ +test('et udfald siger ingen forbindelse frem for at bede om kodeordet', async ({ page }) => { + await page.route('**/api/v1/**', (route) => route.abort()); + + await page.goto('/'); + + // Prøvetiden skal kunne rumme hele forsøgsvinduet: forespørgselslaget prøver + // igen i femten sekunder, før det kalder det et udfald. Det er med vilje — + // et blink på nettet må ikke smide folk ud — så prøven må vente det ud. + await expect(page.getByText('Ingen forbindelse', { exact: true })).toBeVisible({ + timeout: 25000 + }); + + // Og ikke kodeordsfeltet. Det er hele pointen. + await expect(page.getByLabel('Adgangskode')).toHaveCount(0); +}); + /** * Et indsat billede kan ses i fuld størrelse og kopieres videre. * diff --git a/web/src/lib/components/NoConnection.svelte b/web/src/lib/components/NoConnection.svelte new file mode 100644 index 0000000..4d32576 --- /dev/null +++ b/web/src/lib/components/NoConnection.svelte @@ -0,0 +1,95 @@ + + +
+
+
+ +

verdande

+
+ +

{t('net.downTitle')}

+

{t('net.downBody')}

+ + +
+
+ + diff --git a/web/src/lib/locales/da.js b/web/src/lib/locales/da.js index a04d027..e03a5fd 100644 --- a/web/src/lib/locales/da.js +++ b/web/src/lib/locales/da.js @@ -237,6 +237,9 @@ export const da = { 'nav.offlineHint': 'Ændringer fra andre vises ikke lige nu', 'net.reconnecting': 'Genopretter forbindelse…', 'net.offline': 'Ingen forbindelse', + 'net.downTitle': 'Ingen forbindelse', + 'net.downBody': 'Verdande kan ikke nås herfra lige nu. Din tekst er ikke væk — den ligger på serveren, og den kommer igen, når forbindelsen gør.', + 'net.downRetry': 'Prøv igen', 'nav.doItToday': 'Gør det i dag', // --- the sidebar's own sections ------------------------------------------------ diff --git a/web/src/lib/locales/en.js b/web/src/lib/locales/en.js index 6f6e55b..37b9422 100644 --- a/web/src/lib/locales/en.js +++ b/web/src/lib/locales/en.js @@ -234,6 +234,9 @@ export const en = { 'nav.offlineHint': 'Changes from other people are not showing right now', 'net.reconnecting': 'Reconnecting…', 'net.offline': 'No connection', + 'net.downTitle': 'No connection', + 'net.downBody': 'Verdande cannot be reached from here right now. Nothing you wrote is lost — it is on the server, and it comes back when the connection does.', + 'net.downRetry': 'Try again', 'nav.doItToday': 'Do it today', // --- the sidebar's own sections ------------------------------------------------ diff --git a/web/src/lib/stores.svelte.js b/web/src/lib/stores.svelte.js index 4c1712b..a5f7430 100644 --- a/web/src/lib/stores.svelte.js +++ b/web/src/lib/stores.svelte.js @@ -29,6 +29,20 @@ class AppState { people = $state([]); tasks = $state([]); loading = $state(true); + + /** + * True when the boot never reached the server — as opposed to reaching it and + * being told the session is gone. + * + * The two are indistinguishable from `user === null`, and they are opposite + * things to the person looking at the screen: one means "log ind", the other + * means "der er intet at logge ind på herfra, og et kodeord hjælper ikke". A + * total outage used to show the login screen, so the app blamed the user's + * session for the network being down — and a login page that cannot possibly + * work is worse than an error, because it looks like it might. + */ + unreachable = $state(false); + /** Transient messages: a failed save, a rolled-back change. */ toasts = $state([]); connected = $state(false); @@ -96,6 +110,7 @@ class AppState { this.loading = true; try { this.user = await api.me(); + this.unreachable = false; // The interface's language follows the account, so it is set the moment // the session is known — before anything has drawn a string. i18n.set(this.user.locale); @@ -111,6 +126,10 @@ class AppState { this.loadNotifications(); } catch (e) { this.user = null; + // `offline` is the one code the request layer makes up itself: it is what + // is thrown when the retry window ran out without any status ever + // arriving. A 401 is the server answering, and that is a real sign-out. + this.unreachable = e?.code === 'offline'; } finally { this.loading = false; } diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index c8070b2..17f67df 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -8,6 +8,7 @@ import Sidebar from '$lib/components/Sidebar.svelte'; import CommandPalette from '$lib/components/CommandPalette.svelte'; import SignIn from '$lib/components/SignIn.svelte'; + import NoConnection from '$lib/components/NoConnection.svelte'; import TaskDetail from '$lib/components/TaskDetail.svelte'; import { t } from '$lib/i18n.svelte.js'; import { setBadge } from '$lib/badge.js'; @@ -166,6 +167,10 @@
{:else if standalone} {@render children()} +{:else if app.unreachable} + + {:else if !app.user} {:else}