Vibecoded CI/CD
Some checks failed
Build and Deploy Lean / build (push) Failing after 16m44s
Build and Deploy Nuxt / build (push) Failing after 48s

This commit is contained in:
2026-10-06 00:23:17 +02:00
parent a57770619e
commit e2c2c66d7f
79 changed files with 6044 additions and 47 deletions

View File

@@ -3,7 +3,10 @@
A session is a bearer token the API handed us, kept in a cookie rather than
localStorage so the server can see it too: the sidebar then knows who is
signed in on the very first draw, before any script runs. The token itself is
never decoded here — the API is asked who it belongs to (see restoreSession). */
never decoded here — the API is asked who it belongs to (see restoreSession).
In the desktop build the token is kept twice — see keepTokenRemembered, at
the foot of this file. */
export interface AuthUser {
id: string
@@ -34,6 +37,11 @@ export function useAuth() {
maxAge: WEEK_IN_SECONDS,
sameSite: 'lax',
})
/* A window is not owed a cookie jar: its own address is one a webview is not
everywhere obliged to store cookies against, and a session that could not
outlive the window would ask the person to sign in every time they opened
the app. A no-op anywhere else. */
keepTokenRemembered(token, TOKEN_COOKIE)
const user = useState<AuthUser | null>('auth:user', () => null)
const loggedIn = computed(() => user.value !== null)

View File

@@ -0,0 +1,65 @@
/* The window's own habits.
These are the two things the wiki does differently once it is a desktop app
rather than a page of the web — see utils/desktop.ts for the asking, and
src-tauri/ for the shell. The web build takes the first line and returns:
nothing here changes how a browser sees the wiki.
Runs after the auth plugin (plugins come in name order), which matters: by
now a signed-in person's account has already had its say about the interface
language, and an account outranks this device. */
import { isDesktop, leadsOutward, openExternal, storeGet, storeSet } from '~/utils/desktop'
/** Where a window keeps the interface language it was last shown. Also the
* name of the cookie the web build writes it to — the same choice, remembered
* the way this kind of host can be sure of remembering it. */
const LOCALE_KEY = 'mathew-locale'
export default defineNuxtPlugin(() => {
if (!import.meta.client || !isDesktop()) return
/* Links. The wiki's own addresses are relative and the router turns them as
it always does; an absolute one is about something outside this window —
and a window has one page, no tab strip, no back button a reader would
trust after the wiki had been replaced by some other site's document. So
those go out to the system's browser instead, and the window keeps
showing the wiki it was asked to show.
Captured on the way in, so it also catches the links drawn from article
Markdown — the rendered text belongs to the article, which has no reason
to know it is not in a browser. */
document.addEventListener(
'click',
(event) => {
if (event.defaultPrevented || event.button !== 0) return
// a modified click is the reader asking for the browser's own gesture
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
const target = event.target as Element | null
const href = target?.closest?.('a')?.getAttribute('href')
if (!href || !leadsOutward(href)) return
event.preventDefault()
void openExternal(href)
},
true,
)
/* The interface language, kept where a window can be certain of keeping it.
On the web the choice rides on the `mathew-locale` cookie, which the server
reads so the first draw already speaks it. A window draws nothing on a
server, and its own address — `tauri://localhost` — is one a webview is
not everywhere obliged to keep cookies for, so the choice is mirrored into
localStorage: a lost cookie costs a language switch at the next start, and
that is all.
An account's own choice outranks the mirror, as it outranks the cookie —
hence the guard, the auth plugin having run first. Whatever the UI ends up
speaking is then written back, so the mirror always says what this window
last showed. */
const { locale, choose } = useAppLocale()
const { user } = useAuth()
const remembered = storeGet(LOCALE_KEY)
if (remembered && !user.value?.locale) void choose(remembered)
watch(locale, (code) => storeSet(LOCALE_KEY, code))
})

View File

@@ -0,0 +1,116 @@
/* Running as a desktop app.
One bundle answers both a browser and an app window (Tauri — see
src-tauri/), so nothing in `app/` is desktop code: it is the wiki asking at
run time whether it happens to be sitting in a window. The answer comes from
the shell, which marks the page it loads and marks no other — so the same
dist-desktop/ files opened in an ordinary browser behave as a web page, and
a web page that somehow met Tauri would only gain the shell's habits.
What a window does differently, then, is small and all about being a window
with one page and no browser chrome: a link leads out to the system's own
browser rather than replacing the wiki in the only view there is, and the
few bytes the app remembers of a person live where a window can be certain
of keeping them. */
import type { Ref } from 'vue'
import { watch } from 'vue'
/** The handle every Tauri webview injects into its page — present before the
* npm API is imported, and without it. Nothing else sets this name. */
export function isDesktop(): boolean {
if (!import.meta.client) return false
return (window as Window & { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ != null
}
/* A window is expected to have localStorage. A private browser may throw on
being asked for it at all, and a setting that cannot be remembered is worth
far less than a page that still draws — so the three doors below are the
only place any of it is touched, and none of them lets a throw through. */
export function storeGet(key: string): string | null {
try {
return localStorage.getItem(key)
} catch {
return null
}
}
export function storeSet(key: string, value: string): void {
try {
localStorage.setItem(key, value)
} catch {
/* nothing remembered; nothing else depends on it */
}
}
export function storeClear(key: string): void {
try {
localStorage.removeItem(key)
} catch {
/* as good as cleared, for as long as this page lives */
}
}
/** Whether an address leads outside this app — to a document on some other
* site, or to a mail client, or to a phone dialler. The app's own pages are
* the relative `/wiki/…` the router already knows; an absolute address that
* happens to name the wiki's public site counts as outward too, and opens
* there in the browser, which is the honest reading of a link written out in
* full. */
export function leadsOutward(href: string): boolean {
if (!import.meta.client || !href) return false
let url: URL
try {
url = new URL(href, window.location.href)
} catch {
return false
}
if (url.protocol === 'mailto:' || url.protocol === 'tel:') return true
return (url.protocol === 'http:' || url.protocol === 'https:') && url.origin !== window.location.origin
}
/** Hand a document to whatever machine-level application owns it: the browser
* for a page, the mail client for `mailto:`. In a browser a link already does
* this, so the fallback is the browser's own new tab; in a window there is no
* other tab, and following the link in place would leave the wiki's page
* gone from the only view there is. */
export async function openExternal(url: string): Promise<void> {
if (!import.meta.client || !url) return
if (isDesktop()) {
try {
const { openUrl } = await import('@tauri-apps/plugin-opener')
await openUrl(url)
return
} catch {
/* no shell to hand it to, or this window is not allowed this one — the
least worse thing left is the browser's way out */
}
}
window.open(url, '_blank', 'noopener')
}
/** Keep a session token where a window cannot lose it.
On the web a cookie is the whole of it: the server reads it, and the browser
keeps it as long as the API said the token lasts. A window's own address
(`tauri://localhost`) is one a webview is not everywhere obliged to keep
cookies for, and a session dying with the window would mean signing in again
every time the app was opened — for a token the API hands out for a week. So
here it is mirrored into localStorage and taken back from there whenever the
cookie comes up empty, whichever of the two this webview chose to keep.
The cookie still leads: it is the one already at hand, the one a web build
would have read, and the one the sign-out line clears. A browser asked this
question of the same files ignores all of it — there is nothing to mirror
where cookies work — so the wiki's behavior there is untouched. */
export function keepTokenRemembered(token: Ref<string | null>, key: string): void {
if (!import.meta.client || !isDesktop()) return
if (!token.value) {
const kept = storeGet(key)
if (kept) token.value = kept
}
// sync, so signing out clears the mirror in the same breath, not on a tick
watch(token, (value) => (value ? storeSet(key, value) : storeClear(key)), { flush: 'sync' })
}