Merge pull request #87 from kristianwind/et-udfald-siger-det
feat(net): et udfald siger "ingen forbindelse" frem for at bede om kodeordet
This commit is contained in:
@@ -4748,6 +4748,40 @@ test('en ny note hentes ikke igen, når den lige er lavet', async ({ page }) =>
|
|||||||
expect(trouble).toEqual([]);
|
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.
|
* 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>
|
||||||
@@ -237,6 +237,9 @@ export const da = {
|
|||||||
'nav.offlineHint': 'Ændringer fra andre vises ikke lige nu',
|
'nav.offlineHint': 'Ændringer fra andre vises ikke lige nu',
|
||||||
'net.reconnecting': 'Genopretter forbindelse…',
|
'net.reconnecting': 'Genopretter forbindelse…',
|
||||||
'net.offline': 'Ingen 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',
|
'nav.doItToday': 'Gør det i dag',
|
||||||
|
|
||||||
// --- the sidebar's own sections ------------------------------------------------
|
// --- the sidebar's own sections ------------------------------------------------
|
||||||
|
|||||||
@@ -234,6 +234,9 @@ export const en = {
|
|||||||
'nav.offlineHint': 'Changes from other people are not showing right now',
|
'nav.offlineHint': 'Changes from other people are not showing right now',
|
||||||
'net.reconnecting': 'Reconnecting…',
|
'net.reconnecting': 'Reconnecting…',
|
||||||
'net.offline': 'No connection',
|
'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',
|
'nav.doItToday': 'Do it today',
|
||||||
|
|
||||||
// --- the sidebar's own sections ------------------------------------------------
|
// --- the sidebar's own sections ------------------------------------------------
|
||||||
|
|||||||
@@ -29,6 +29,20 @@ class AppState {
|
|||||||
people = $state([]);
|
people = $state([]);
|
||||||
tasks = $state([]);
|
tasks = $state([]);
|
||||||
loading = $state(true);
|
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. */
|
/** Transient messages: a failed save, a rolled-back change. */
|
||||||
toasts = $state([]);
|
toasts = $state([]);
|
||||||
connected = $state(false);
|
connected = $state(false);
|
||||||
@@ -96,6 +110,7 @@ class AppState {
|
|||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
this.user = await api.me();
|
this.user = await api.me();
|
||||||
|
this.unreachable = false;
|
||||||
// The interface's language follows the account, so it is set the moment
|
// The interface's language follows the account, so it is set the moment
|
||||||
// the session is known — before anything has drawn a string.
|
// the session is known — before anything has drawn a string.
|
||||||
i18n.set(this.user.locale);
|
i18n.set(this.user.locale);
|
||||||
@@ -111,6 +126,10 @@ class AppState {
|
|||||||
this.loadNotifications();
|
this.loadNotifications();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.user = null;
|
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 {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import Sidebar from '$lib/components/Sidebar.svelte';
|
import Sidebar from '$lib/components/Sidebar.svelte';
|
||||||
import CommandPalette from '$lib/components/CommandPalette.svelte';
|
import CommandPalette from '$lib/components/CommandPalette.svelte';
|
||||||
import SignIn from '$lib/components/SignIn.svelte';
|
import SignIn from '$lib/components/SignIn.svelte';
|
||||||
|
import NoConnection from '$lib/components/NoConnection.svelte';
|
||||||
import TaskDetail from '$lib/components/TaskDetail.svelte';
|
import TaskDetail from '$lib/components/TaskDetail.svelte';
|
||||||
import { t } from '$lib/i18n.svelte.js';
|
import { t } from '$lib/i18n.svelte.js';
|
||||||
import { setBadge } from '$lib/badge.js';
|
import { setBadge } from '$lib/badge.js';
|
||||||
@@ -166,6 +167,10 @@
|
|||||||
<div class="booting"></div>
|
<div class="booting"></div>
|
||||||
{:else if standalone}
|
{:else if standalone}
|
||||||
{@render children()}
|
{@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}
|
{:else if !app.user}
|
||||||
<SignIn />
|
<SignIn />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
Reference in New Issue
Block a user