CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 12s
CI & Build / Build & push image (push) Successful in 39s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m38s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m21s
Desktop (Tauri) / Update manifest (push) Successful in 5s
Sign out was a trap on the desktop, not an action. It nulls the synthetic local user and redirects to /login, but the offline adapter rejects every sign-in with "there's no account to sign in to" — so the only way back into your own notes was to restart the app. There is nothing to sign out of; the notes are on this machine either way. Linked devices was a quieter version of the same thing: it lists the tokens a SERVER has issued to native clients, and the desktop is one of those clients, so offline the list is always empty and issuing a token rejects. Its actual relationship with a server already has a home at /sync. Also hid the account name, which named a login the app doesn't have. /account is now blocked in the router too, not merely hidden — the mirror of the existing requiresDesktop guard — so a typed URL or a restored history entry can't reach the dead end either. Deliberately not applied to /login and /register: bouncing those on desktop would loop against the requiresAuth guard whenever a session is missing. The launch flash is the window painting before the webview does, showing the platform default white through the gap — worst on a dark-mode desktop, and widened by the software rendering we force on Linux. Set from the live system theme rather than app.windows[].backgroundColor, because that config carries one static colour and either choice would fix half of users while introducing the same flash for the other half. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
111 lines
4.2 KiB
TypeScript
111 lines
4.2 KiB
TypeScript
import { createRouter, createWebHistory } from "vue-router";
|
|
import { useSessionStore } from "../stores/session";
|
|
import { useConfigStore } from "../stores/config";
|
|
import { isDesktop, logEvent } from "../desktop/bridge";
|
|
|
|
// One-time boot diagnostic: the first navigation is where config + session resolve,
|
|
// so it's the moment that tells us whether the app got past its startup gate.
|
|
let bootLogged = false;
|
|
|
|
const router = createRouter({
|
|
history: createWebHistory(),
|
|
routes: [
|
|
{
|
|
// Persistent authed shell (sidebar + top bar + search); children render in it.
|
|
path: "/",
|
|
component: () => import("../components/AppShell.vue"),
|
|
meta: { requiresAuth: true },
|
|
children: [
|
|
{ path: "", name: "board", component: () => import("../views/BoardView.vue") },
|
|
{ path: "archive", name: "archive", component: () => import("../views/BoardView.vue") },
|
|
{ path: "trash", name: "trash", component: () => import("../views/BoardView.vue") },
|
|
{ path: "label/:id", name: "label", component: () => import("../views/BoardView.vue") },
|
|
{ path: "search", name: "search", component: () => import("../views/SearchView.vue") },
|
|
{ path: "graph", name: "graph", component: () => import("../views/GraphView.vue") },
|
|
{ path: "reminders", name: "reminders", component: () => import("../views/RemindersView.vue") },
|
|
{ path: "timeline", name: "timeline", component: () => import("../views/TimelineView.vue") },
|
|
],
|
|
},
|
|
{
|
|
path: "/settings",
|
|
name: "settings",
|
|
component: () => import("../views/SettingsView.vue"),
|
|
meta: { requiresAuth: true, requiresAdmin: true },
|
|
},
|
|
{
|
|
// Desktop only: connect this app to a server. Meaningless in the web build,
|
|
// which IS a server's UI — there's nothing for it to link to.
|
|
path: "/sync",
|
|
name: "sync",
|
|
component: () => import("../views/SyncView.vue"),
|
|
meta: { requiresAuth: true, requiresDesktop: true },
|
|
},
|
|
{
|
|
// Per-user account: linked devices (native-client sync tokens). Any user.
|
|
//
|
|
// The mirror of `requiresDesktop` above: this one needs a SERVER. The desktop
|
|
// is itself one of the devices this page lists, so offline the list is always
|
|
// empty and issuing a token rejects — its server relationship lives at /sync.
|
|
// Guarded in the router, not just hidden in the shell, so a typed URL or a
|
|
// restored history entry can't land on a dead end either.
|
|
path: "/account",
|
|
name: "account",
|
|
component: () => import("../views/AccountView.vue"),
|
|
meta: { requiresAuth: true, requiresServer: true },
|
|
},
|
|
{
|
|
path: "/login",
|
|
name: "login",
|
|
component: () => import("../views/LoginView.vue"),
|
|
meta: { guestOnly: true },
|
|
},
|
|
{
|
|
path: "/register",
|
|
name: "register",
|
|
component: () => import("../views/RegisterView.vue"),
|
|
meta: { guestOnly: true },
|
|
},
|
|
],
|
|
});
|
|
|
|
router.beforeEach(async (to) => {
|
|
const session = useSessionStore();
|
|
const config = useConfigStore();
|
|
await config.load();
|
|
if (!session.loaded) {
|
|
await session.fetchMe();
|
|
}
|
|
if (!bootLogged) {
|
|
bootLogged = true;
|
|
logEvent(
|
|
"info",
|
|
`first route: config(site=${config.siteName}) session(user=${session.user?.email ?? "none"}) -> ${String(to.name ?? to.path)}`,
|
|
);
|
|
}
|
|
if (to.meta.requiresAuth && !session.user) {
|
|
return { name: "login", query: to.fullPath !== "/" ? { redirect: to.fullPath } : undefined };
|
|
}
|
|
if (to.meta.requiresAdmin && !session.user?.is_admin) {
|
|
return { name: "board" };
|
|
}
|
|
if (to.meta.requiresDesktop && !isDesktop()) {
|
|
return { name: "board" };
|
|
}
|
|
// Deliberately NOT applied to /login and /register: bouncing those on desktop
|
|
// would loop against the requiresAuth guard above the moment a session is
|
|
// missing. Nothing on the desktop navigates to them any more (AppShell's sign-out
|
|
// is web-only), and a fresh launch always resolves the local user.
|
|
if (to.meta.requiresServer && isDesktop()) {
|
|
return { name: "board" };
|
|
}
|
|
if (to.name === "register" && !config.allowRegistration) {
|
|
return { name: "login" };
|
|
}
|
|
if (to.meta.guestOnly && session.user) {
|
|
return { name: "board" };
|
|
}
|
|
return true;
|
|
});
|
|
|
|
export default router;
|