feat(udgaver): fladen følger udgaven, og søgningen holder opgaverne inde
Trin 2. Serveren kunne allerede lade være med at montere opgaveruterne; fladen tilbød dem stadig, så en noter-instans havde links til sider, der ikke fandtes. --- hvordan fladen får det at vide --- `/auth/setup` bærer nu `edition`. Dér og ikke på `/auth/me`: udgaven er en egenskab ved instansen, ikke ved den, der er logget ind, og fladen skal kende den, før nogen er. Det er i forvejen kaldet, der spørger "hvad er det her for en instans", så den følger med uden en ekstra tur. Butikken henter formen ved siden af kontoen og tier, hvis den fejler: en 401 fra `me` må ikke tage udgaven med sig — login-skærmen er også en flade i en udgave. Afventet før `loading` slippes, så fladen aldrig tegnes, før den ved hvilket program den er; tegnet først og rettet bagefter ville vise opgaver i et sekund på en instans, der ikke har nogen. `full` som udgangspunkt, fordi det er det, programmet altid har været. --- hvad der forsvinder --- Menuens opgaveindgange er udeladt, ikke gråtonet. Forsiden er noterne. En gemt adresse til /upcoming, /kalender, /uddelegeret, /faerdige, /opgave, /filter eller /etiket fører til noterne frem for en blindgyde — der er ikke sket noget forkert, adressen hører bare til et andet program end det, der kører her. Listen står ét sted i layoutet frem for som en vagt pr. rute: listen ER beslutningen, og en vagt pr. fil er syv steder at glemme den ottende. --- et hul, prøven fandt --- /search er monteret i begge udgaver, fordi noter og notesbøger skal kunne findes — og den returnerede opgaver. Ikke teoretisk: en instans lavet om fra en fuld har opgaverne liggende i databasen endnu, så ⌘K ville være den ene dør, der stod åben ind til den halvdel, resten af programmet lige havde lukket. Lukket på serveren, hvor det hører hjemme, og ⌘K spørger også selv — en tom liste af den rigtige grund og en tom liste ved et tilfælde ser ens ud. Prøven har en kontrol: den fulde udgave SKAL finde opgaven, ellers beviser nullet i noter-udgaven ingenting. --- prøverne --- Et nyt Playwright-projekt med sin egen server, sin egen port og sin egen database. Ingen lånt session: den instans har aldrig set den konto, den fulde udgaves opsætning lavede. En skakt, der kun er prøvet ved at kalde en funktion, er prøvet på det, nogen mente — det her måler programmet, som en bruger møder det. Seks prøver, heriblandt forskellen på skjult og fraværende målt udefra: /api/v1/tasks, /labels, /upcoming og /filters skal svare 404, og /api/v1/notes skal svare 200 — ellers ville de fire bestå på en server, der bare var væk. `testIgnore` på chromium-projektet, fordi filen ellers også kørte dér, mod den fulde udgave, hvor den fejler med beskeder der læser som om skakten ikke virker. Den rigtige fejl stod i projektnavnet, ikke i påstanden. Go-suiten grøn. chromium + notes-edition: 110 bestået, 0 fejlet. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9b067f7057
commit
3cee9bfb6e
@@ -0,0 +1,120 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Noter-udgaven, kørt som sit eget program.
|
||||
*
|
||||
* Egen server på sin egen port med `VERDANDE_EDITION=notes`, egen database og
|
||||
* ingen lånt session: den her instans har aldrig set den konto, den fulde udgaves
|
||||
* opsætning lavede, så den laver sin egen.
|
||||
*
|
||||
* Og det er hele grunden til at prøven findes som en separat server frem for som
|
||||
* en indstilling, en prøve skruer på. En skakt, der kun er prøvet ved at kalde en
|
||||
* funktion, er prøvet på det, nogen mente — det her måler programmet, som en
|
||||
* bruger møder det.
|
||||
*/
|
||||
|
||||
const KONTO = {
|
||||
email: '[email protected]',
|
||||
name: 'Noteskriver',
|
||||
password: 'et langt kodeord til test'
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
// Første konto, hvis den ikke er lavet endnu. Prøverne i filen deler instans,
|
||||
// så den anden gang er det en almindelig login.
|
||||
const opret = page.getByRole('button', { name: 'Opret konto' });
|
||||
const login = page.getByRole('button', { name: 'Log ind' });
|
||||
// Ventet på, at EN af de to er der, før der spørges hvilken. Spurgt før
|
||||
// tegningen svarer begge nej, og så faldt prøven i den forkerte gren og ventede
|
||||
// tredive sekunder på en knap, der aldrig kom.
|
||||
await expect(opret.or(login)).toBeVisible();
|
||||
|
||||
if (await opret.isVisible()) {
|
||||
await page.getByLabel(/Navn/).fill(KONTO.name);
|
||||
await page.getByLabel(/E-mail/).fill(KONTO.email);
|
||||
await page.getByLabel(/Adgangskode/).fill(KONTO.password);
|
||||
await opret.click();
|
||||
// Beaconens besked står øverst første gang; den er ikke det her prøvens
|
||||
// ærinde, men den dækker for det, der er.
|
||||
const besked = page.getByRole('region', { name: /melder, at den findes/ });
|
||||
if (await besked.isVisible().catch(() => false)) {
|
||||
await besked.getByRole('button', { name: 'Behold den' }).click();
|
||||
}
|
||||
} else {
|
||||
await page.getByLabel(/E-mail/).fill(KONTO.email);
|
||||
await page.getByLabel(/Adgangskode/).fill(KONTO.password);
|
||||
await page.getByRole('button', { name: 'Log ind' }).click();
|
||||
}
|
||||
await expect(page.getByRole('navigation', { name: 'Hovedmenu' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('instansen siger selv, at den er noter-udgaven', async ({ page }) => {
|
||||
// Fladen kan ikke lukke af for noget, den ikke får at vide, og den skal have
|
||||
// det at vide, før nogen er logget ind.
|
||||
const svar = await page.request.get('/api/v1/auth/setup');
|
||||
expect((await svar.json()).edition).toBe('notes');
|
||||
});
|
||||
|
||||
test('der er ingen vej til opgaverne i menuen', async ({ page }) => {
|
||||
const menu = page.getByRole('navigation', { name: 'Hovedmenu' });
|
||||
await expect(menu).toBeVisible();
|
||||
|
||||
// Udeladt, ikke gråtonet: ruterne bag dem er ikke monteret på serveren.
|
||||
for (const navn of ['I dag', 'Kommende', 'Kalender', 'Uddelegeret']) {
|
||||
await expect(menu.getByRole('link', { name: navn })).toHaveCount(0);
|
||||
}
|
||||
|
||||
// Og det, udgaven ER til, står der. En menu uden opgaver OG uden noter ville
|
||||
// også bestå prøven ovenfor, og den ville være en sletning frem for en skakt.
|
||||
await expect(menu.getByRole('link', { name: 'Noter', exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test('forsiden er noterne, ikke en tom opgaveliste', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveURL(/\/noter$/);
|
||||
});
|
||||
|
||||
test('en gemt adresse til en opgaveside fører til noterne i stedet for et blindt spor', async ({
|
||||
page
|
||||
}) => {
|
||||
// Nogen, der har gemt et link fra en fuld instans, skal lande et sted, der
|
||||
// giver mening. Der er ikke sket noget forkert — adressen hører bare til et
|
||||
// andet program end det, der kører her.
|
||||
for (const sti of ['/upcoming', '/kalender', '/uddelegeret', '/faerdige']) {
|
||||
await page.goto(sti);
|
||||
await expect(page).toHaveURL(/\/noter$/);
|
||||
}
|
||||
});
|
||||
|
||||
test('⌘K finder noter og ikke opgaver', async ({ page }) => {
|
||||
await page.goto('/noter');
|
||||
await page.getByRole('button', { name: 'Ny note' }).click();
|
||||
const felt = page.getByRole('textbox', { name: 'Notens tekst' });
|
||||
await expect(felt).toHaveText('');
|
||||
await felt.click();
|
||||
await page.keyboard.type('Kvartalsregnskabet');
|
||||
await felt.blur();
|
||||
await expect(page.locator('.notes button.row strong').filter({ hasText: /^Kvartalsregnskabet$/ })).toHaveCount(1);
|
||||
|
||||
await page.getByRole('button', { name: /Søg/ }).click();
|
||||
const palette = page.getByRole('dialog');
|
||||
await palette.getByRole('textbox').fill('Kvartalsregnskabet');
|
||||
|
||||
// Noten findes — ellers beviser det næste ingenting.
|
||||
await expect(palette.locator('li button', { hasText: 'Kvartalsregnskabet' })).toBeVisible();
|
||||
// Og søgningen på serveren svarer uden opgaver overhovedet.
|
||||
const svar = await page.request.get('/api/v1/search?q=Kvartalsregnskabet');
|
||||
expect((await svar.json()).tasks ?? []).toEqual([]);
|
||||
});
|
||||
|
||||
test('opgaveruterne findes slet ikke på serveren', async ({ page }) => {
|
||||
// Det her er forskellen på skjult og fraværende, målt udefra.
|
||||
for (const sti of ['/api/v1/tasks', '/api/v1/labels', '/api/v1/upcoming', '/api/v1/filters']) {
|
||||
const svar = await page.request.get(sti);
|
||||
expect(svar.status(), `${sti} svarede ${svar.status()}`).toBe(404);
|
||||
}
|
||||
// Kontrollen: en rute, der ER monteret, svarer ikke 404 — ellers ville
|
||||
// ovenstående bestå på en server, der var helt væk.
|
||||
expect((await page.request.get('/api/v1/notes')).status()).toBe(200);
|
||||
});
|
||||
+83
-36
@@ -20,6 +20,7 @@ import { dirname, join } from 'node:path';
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const repo = join(here, '..');
|
||||
const dataDir = join(here, '.playwright-data');
|
||||
const notesDataDir = join(here, '.playwright-data-notes');
|
||||
|
||||
// A clean instance every run: the first test creates the first account, which only
|
||||
// works on an empty database.
|
||||
@@ -31,9 +32,14 @@ const dataDir = join(here, '.playwright-data');
|
||||
// connection fail, with SQLITE_CANTOPEN. It looks exactly like a flaky auth bug.
|
||||
if (process.env.TEST_WORKER_INDEX === undefined) {
|
||||
rmSync(dataDir, { recursive: true, force: true });
|
||||
rmSync(notesDataDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const PORT = 8097;
|
||||
// Noter-udgaven kører som sin egen server ved siden af den fulde. Den kan ikke
|
||||
// dele port, og den må ikke dele database: de to er to instanser, og en prøve der
|
||||
// lod dem skrive i den samme ville måle en tilstand, ingen af dem ejer.
|
||||
const NOTES_PORT = 8098;
|
||||
// `localhost`, not `127.0.0.1`. WebAuthn refuses an IP address as a relying party
|
||||
// id — `SecurityError: This is an invalid domain` — and localhost is the exception
|
||||
// every browser makes. The passkey test cannot run against a loopback address.
|
||||
@@ -72,6 +78,10 @@ export default defineConfig({
|
||||
{ name: 'setup', testMatch: /.*\.setup\.js/ },
|
||||
{
|
||||
name: 'chromium',
|
||||
// Alt undtagen noter-udgaven, som har sin egen server på sin egen port.
|
||||
// Uden den her linje kørte den prøve også her — mod den FULDE udgave, hvor
|
||||
// den naturligvis fejler, og hvor fejlen læser som om skakten ikke virker.
|
||||
testIgnore: /edition\.spec\.js/,
|
||||
use: { ...devices['Desktop Chrome'], storageState: 'e2e/.auth/user.json' },
|
||||
dependencies: ['setup']
|
||||
},
|
||||
@@ -95,6 +105,21 @@ export default defineConfig({
|
||||
*
|
||||
* WebKit here is the same engine Safari is, which is the whole point.
|
||||
*/
|
||||
/**
|
||||
* Noter-udgaven, som sit eget program.
|
||||
*
|
||||
* Egen server, egen port, egen database, og ingen `storageState`: den her
|
||||
* instans har aldrig set den konto, den fulde udgaves opsætning lavede, og
|
||||
* at låne dens cookie ville prøve to instanser som var de én.
|
||||
*
|
||||
* Den deler ikke `setup` af samme grund — den laver sin egen første konto.
|
||||
*/
|
||||
{
|
||||
name: 'notes-edition',
|
||||
testMatch: /edition\.spec\.js/,
|
||||
use: { ...devices['Desktop Chrome'], baseURL: `http://127.0.0.1:${NOTES_PORT}` }
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit-forms',
|
||||
grep: /@forms/,
|
||||
@@ -103,42 +128,64 @@ export default defineConfig({
|
||||
}
|
||||
],
|
||||
|
||||
webServer: {
|
||||
// The whole chain, every run.
|
||||
//
|
||||
// The frontend is embedded in the binary at compile time, from a copy under
|
||||
// cmd/verdande/webbuild — so a suite that merely started the server would
|
||||
// test whatever frontend was copied there last, which during development is
|
||||
// nearly always the wrong one. Testing a stale build is worse than not
|
||||
// testing: it reports green on code that is not running.
|
||||
command: [
|
||||
'npm run build',
|
||||
'rm -rf ../cmd/verdande/webbuild',
|
||||
'cp -r build ../cmd/verdande/webbuild',
|
||||
'cd .. && go run -tags embedweb ./cmd/verdande'
|
||||
].join(' && '),
|
||||
cwd: here,
|
||||
url: `${baseURL}/healthz`,
|
||||
reuseExistingServer: false,
|
||||
// The first run compiles the binary and builds nothing else; a cold Go
|
||||
// build on a CI runner is comfortably under two minutes.
|
||||
timeout: 180_000,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
env: {
|
||||
VERDANDE_ADDR: `:${PORT}`,
|
||||
// `localhost`, while the suite itself drives `127.0.0.1`. These are two
|
||||
// different things: the suite's address is where the browser goes, and
|
||||
// this one is what the server calls itself — which is now also the
|
||||
// WebAuthn relying party id, and WebAuthn refuses an IP address.
|
||||
webServer: [
|
||||
{
|
||||
// The whole chain, every run.
|
||||
//
|
||||
// The passkey test navigates to localhost explicitly so its origin
|
||||
// matches. Everything else stays on the loopback address, where it has
|
||||
// always been.
|
||||
VERDANDE_BASE_URL: `http://localhost:${PORT}`,
|
||||
VERDANDE_DATA_DIR: dataDir,
|
||||
// Off: an update check would reach out to GitHub from a test run.
|
||||
VERDANDE_UPDATE_CHECK: 'false'
|
||||
// The frontend is embedded in the binary at compile time, from a copy under
|
||||
// cmd/verdande/webbuild — so a suite that merely started the server would
|
||||
// test whatever frontend was copied there last, which during development is
|
||||
// nearly always the wrong one. Testing a stale build is worse than not
|
||||
// testing: it reports green on code that is not running.
|
||||
command: [
|
||||
'npm run build',
|
||||
'rm -rf ../cmd/verdande/webbuild',
|
||||
'cp -r build ../cmd/verdande/webbuild',
|
||||
'cd .. && go run -tags embedweb ./cmd/verdande'
|
||||
].join(' && '),
|
||||
cwd: here,
|
||||
url: `${baseURL}/healthz`,
|
||||
reuseExistingServer: false,
|
||||
// The first run compiles the binary and builds nothing else; a cold Go
|
||||
// build on a CI runner is comfortably under two minutes.
|
||||
timeout: 180_000,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
env: {
|
||||
VERDANDE_ADDR: `:${PORT}`,
|
||||
// `localhost`, while the suite itself drives `127.0.0.1`. These are two
|
||||
// different things: the suite's address is where the browser goes, and
|
||||
// this one is what the server calls itself — which is now also the
|
||||
// WebAuthn relying party id, and WebAuthn refuses an IP address.
|
||||
//
|
||||
// The passkey test navigates to localhost explicitly so its origin
|
||||
// matches. Everything else stays on the loopback address, where it has
|
||||
// always been.
|
||||
VERDANDE_BASE_URL: `http://localhost:${PORT}`,
|
||||
VERDANDE_DATA_DIR: dataDir,
|
||||
// Off: an update check would reach out to GitHub from a test run.
|
||||
VERDANDE_UPDATE_CHECK: 'false'
|
||||
}
|
||||
},
|
||||
|
||||
// Den samme binær en gang til, med den anden udgave slået til. Bygningen er
|
||||
// allerede lavet af serveren ovenfor — `go run` genbruger byggecachen — så
|
||||
// det her koster en opstart og ikke en oversættelse.
|
||||
{
|
||||
command: 'cd .. && go run -tags embedweb ./cmd/verdande',
|
||||
cwd: here,
|
||||
url: `http://127.0.0.1:${NOTES_PORT}/healthz`,
|
||||
reuseExistingServer: false,
|
||||
timeout: 180_000,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
env: {
|
||||
VERDANDE_ADDR: `:${NOTES_PORT}`,
|
||||
VERDANDE_BASE_URL: `http://localhost:${NOTES_PORT}`,
|
||||
VERDANDE_DATA_DIR: notesDataDir,
|
||||
VERDANDE_EDITION: 'notes',
|
||||
VERDANDE_UPDATE_CHECK: 'false'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -85,7 +85,11 @@
|
||||
...settings.map((s) => ({ kind: 'setting', id: s.href, label: `${s.tab} · ${s.title}` })),
|
||||
...projects.map((p) => ({ kind: 'project', id: p.id, label: p.name })),
|
||||
...notes.map((n) => ({ kind: 'note', id: n.id, label: n.title || n.body.slice(0, 60) })),
|
||||
...tasks.map((t) => ({
|
||||
// Serveren sender ingen opgaver i noter-udgaven, så listen er alligevel tom.
|
||||
// Spurgt her også, fordi en tom liste af den rigtige grund og en tom liste
|
||||
// ved et tilfælde ser ens ud — og den dag nogen mounter søgningen anderledes,
|
||||
// skal ⌘K ikke være stedet, det viser sig.
|
||||
...(app.notesOnly ? [] : tasks).map((t) => ({
|
||||
kind: 'task',
|
||||
id: t.id,
|
||||
label: t.content,
|
||||
|
||||
@@ -155,9 +155,12 @@
|
||||
// entry is a question the sidebar keeps asking and answering "no".
|
||||
let navItems = $derived(
|
||||
[
|
||||
{ key: 'today', href: '/', label: 'nav.today' },
|
||||
{ key: 'upcoming', href: '/upcoming', label: 'nav.upcoming' },
|
||||
app.projects.some((p) => p.shared)
|
||||
// Opgavernes egne indgange findes ikke i noter-udgaven. Ikke gråtonet og
|
||||
// ikke skjult med CSS — udeladt, fordi ruterne bag dem ikke er monteret
|
||||
// på serveren. Et link til noget, der ikke findes, er ikke en skønhedsfejl.
|
||||
app.notesOnly ? null : { key: 'today', href: '/', label: 'nav.today' },
|
||||
app.notesOnly ? null : { key: 'upcoming', href: '/upcoming', label: 'nav.upcoming' },
|
||||
!app.notesOnly && app.projects.some((p) => p.shared)
|
||||
? { key: 'delegated', href: '/uddelegeret', label: 'nav.delegated' }
|
||||
: null,
|
||||
// No href: the inbox is a project and is drawn by projectRow, which knows
|
||||
@@ -185,7 +188,7 @@
|
||||
// Sidst i rækken, fordi `navOrder` føjer en ukendt nøgle til bagest: står
|
||||
// den et andet sted her, ser en ny konto én rækkefølge og alle
|
||||
// eksisterende en anden.
|
||||
{ key: 'calendar', href: '/kalender', label: 'nav.calendar' }
|
||||
app.notesOnly ? null : { key: 'calendar', href: '/kalender', label: 'nav.calendar' }
|
||||
].filter(Boolean)
|
||||
);
|
||||
|
||||
|
||||
@@ -43,6 +43,27 @@ class AppState {
|
||||
*/
|
||||
unreachable = $state(false);
|
||||
|
||||
/**
|
||||
* Hvilken udgave af programmet den her instans er.
|
||||
*
|
||||
* `full` er opgaver, projekter og noter sammen. `notes` er notehalvdelen, hvor
|
||||
* opgaveruterne slet ikke er monteret på serveren — så fladen må ikke tilbyde
|
||||
* en vej til dem. Et link til en rute, der ikke findes, er ikke en skønhedsfejl:
|
||||
* det er en knap, der fører et sted hen, hvor der ikke er noget.
|
||||
*
|
||||
* Instansens form, ikke personens rettigheder. Derfor hentes den ved siden af
|
||||
* kontoen frem for efter den: den skal også være kendt, når ingen er logget ind.
|
||||
*
|
||||
* `full` som udgangspunkt, fordi det er det, programmet altid har været — en
|
||||
* instans, der ikke når at svare, skal ikke pludselig se ud som et andet
|
||||
* program.
|
||||
*/
|
||||
edition = $state('full');
|
||||
|
||||
get notesOnly() {
|
||||
return this.edition === 'notes';
|
||||
}
|
||||
|
||||
/** Transient messages: a failed save, a rolled-back change. */
|
||||
toasts = $state([]);
|
||||
connected = $state(false);
|
||||
@@ -108,6 +129,17 @@ class AppState {
|
||||
|
||||
async load() {
|
||||
this.loading = true;
|
||||
|
||||
// Formen hentes for sig, og en fejl på den tier. En 401 fra `me` må ikke
|
||||
// tage udgaven med sig — login-skærmen er også en flade i en udgave — og
|
||||
// en instans, der slet ikke svarer, melder `me` ordentligt nedenfor.
|
||||
const shape = api
|
||||
.setupState()
|
||||
.then((state) => {
|
||||
if (state?.edition) this.edition = state.edition;
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
try {
|
||||
this.user = await api.me();
|
||||
this.unreachable = false;
|
||||
@@ -131,6 +163,10 @@ class AppState {
|
||||
// arriving. A 401 is the server answering, and that is a real sign-out.
|
||||
this.unreachable = e?.code === 'offline';
|
||||
} finally {
|
||||
// Afventet her, så fladen aldrig tegnes, før den ved hvilket program den
|
||||
// er. Tegnet først og rettet bagefter ville vise opgaver i et sekund på
|
||||
// en instans, der ikke har nogen.
|
||||
await shape;
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,39 @@
|
||||
app.load();
|
||||
});
|
||||
|
||||
/**
|
||||
* Opgavernes sider findes ikke i noter-udgaven.
|
||||
*
|
||||
* Ét sted frem for en vagt i hver rute: listen ER beslutningen, og en vagt pr.
|
||||
* fil er syv steder at glemme den ottende. Skrevet som præfikser, fordi
|
||||
* `/opgave/<id>` og `/filter/<id>` er sider under dem.
|
||||
*
|
||||
* Sendt til noterne og ikke til en fejlside: der er ikke sket noget forkert.
|
||||
* Adressen hører bare til et andet program end det, der kører her — og nogen,
|
||||
* der har gemt et link fra en fuld instans, skal lande et sted, der giver
|
||||
* mening frem for i en blindgyde.
|
||||
*
|
||||
* `replaceState`, så Tilbage ikke fører hen til den side, man lige blev sendt
|
||||
* væk fra, og derfra tilbage igen.
|
||||
*/
|
||||
const TASK_PAGES = [
|
||||
'/',
|
||||
'/upcoming',
|
||||
'/uddelegeret',
|
||||
'/kalender',
|
||||
'/faerdige',
|
||||
'/opgave',
|
||||
'/filter',
|
||||
'/etiket'
|
||||
];
|
||||
|
||||
$effect(() => {
|
||||
if (!app.notesOnly) return;
|
||||
const here = $page.url.pathname;
|
||||
const taskPage = TASK_PAGES.some((p) => here === p || (p !== '/' && here.startsWith(p + '/')));
|
||||
if (taskPage) goto('/noter', { replaceState: true });
|
||||
});
|
||||
|
||||
/**
|
||||
* Links ud af programmet, åbnet i den browser, maskinen er sat op til.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user