From 84af63316d05e2f0e1d5f671f7c24eee126a6bdd Mon Sep 17 00:00:00 2001 From: kristianwind Date: Thu, 17 Sep 2026 12:23:39 +0200 Subject: [PATCH] feat(net): et udfald siger "ingen forbindelse" frem for at bede om kodeordet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 totalt udfald blev vist som login-skærmen: appen gav 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: det, appen viste, var en langsom login-side. Forskellen var der allerede i forespørgselslaget. Et netværkssvigt, der har brugt sit forsøgsvindue op, kastes som `ApiError(0, 'offline')` — koden laves af klienten selv, netop fordi der aldrig kom en status. En 401 er serveren, der svarer, og det er en rigtig udlogning. Butikken skelner nu på den kode, og fladen viser NoConnection i stedet for SignIn. Teksten siger, at data er i behold, for det er det spørgsmål, man faktisk har, når ens noter forsvinder bag en login-boks. Prøven afbryder API-kaldene frem for at stoppe serveren: en afbrudt forespørgsel er præcis det, en manglende DNS-post giver browseren. Efterprøvet mod den gamle kode — den fejler på "Ingen forbindelse". Co-Authored-By: Claude Opus 5 --- web/e2e/smoke.spec.js | 34 ++++++++ web/src/lib/components/NoConnection.svelte | 95 ++++++++++++++++++++++ web/src/lib/locales/da.js | 3 + web/src/lib/locales/en.js | 3 + web/src/lib/stores.svelte.js | 19 +++++ web/src/routes/+layout.svelte | 5 ++ 6 files changed, 159 insertions(+) create mode 100644 web/src/lib/components/NoConnection.svelte 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}