Compare commits

...
Author SHA1 Message Date
kristianwindandClaude Opus 5 84af63316d feat(net): et udfald siger "ingen forbindelse" frem for at bede om kodeordet
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 <noreply@anthropic.com>
2026-09-17 12:29:57 +02:00
Kristian Wind 930522ecf6 Merge pull request #86 from kristianwind/ny-note-taber-ikke-tekst
fix(noter): en ny note taber ikke det, man skriver i den
2026-09-17 12:29:49 +02:00
6 changed files with 159 additions and 0 deletions
+34
View File
@@ -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.
*
@@ -0,0 +1,95 @@
<script>
/**
* What the app shows when it could not reach the server at all.
*
* Before this, an outage rendered the sign-in screen: the boot asks who you
* are, the request never gets an answer, `user` stays null, and null means
* "logged out" everywhere else. So the app blamed the session for the network
* being down, and offered a password box that could not possibly work. That is
* worse than an error message, because it looks like it might work — people
* type the password, get nothing, and conclude their account is broken.
*
* It happened for real on 17 September 2026: the instance's DNS record was
* deleted, and what the app showed was a slow-loading login page.
*
* The text says the data is safe, because that is the question somebody
* actually has when their notes vanish behind a login box.
*/
import { t } from '$lib/i18n.svelte.js';
import { app } from '$lib/stores.svelte.js';
let trying = $state(false);
async function again() {
trying = true;
try {
await app.load();
} finally {
trying = false;
}
}
</script>
<div class="screen">
<div class="card">
<div class="brand">
<span class="rune" aria-hidden="true">ᚹ</span>
<h1>verdande</h1>
</div>
<h2>{t('net.downTitle')}</h2>
<p class="lede">{t('net.downBody')}</p>
<button onclick={again} disabled={trying}>
{trying ? t('net.reconnecting') : t('net.downRetry')}
</button>
</div>
</div>
<style>
.screen {
height: 100dvh;
display: grid;
place-items: center;
padding: var(--s4);
background: var(--ground);
}
.card {
width: 100%;
max-width: 340px;
display: flex;
flex-direction: column;
gap: var(--s4);
text-align: center;
}
.brand {
display: flex;
align-items: center;
gap: var(--s3);
justify-content: center;
}
.rune {
font-size: var(--text-2xl);
color: var(--accent);
line-height: 1;
}
h1 {
font-size: var(--text-xl);
margin: 0;
}
h2 {
font-size: var(--text-lg);
margin: 0;
}
.lede {
margin: 0;
color: var(--ink-soft);
line-height: 1.5;
}
</style>
+3
View File
@@ -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 ------------------------------------------------
+3
View File
@@ -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 ------------------------------------------------
+19
View File
@@ -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;
}
+5
View File
@@ -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 @@
<div class="booting"></div>
{:else if standalone}
{@render children()}
{:else if app.unreachable}
<!-- Ikke login-skærmen: en total udfald er ikke en udløbet session, og et
kodeord hjælper ikke mod en manglende forbindelse. -->
<NoConnection />
{:else if !app.user}
<SignIn />
{:else}