Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 13s
CI & Build / integration (push) Successful in 29s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 1m5s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 1m52s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 7m22s
Restores42e06da, which was reverted only because Cargo.lock had not been updated for the new crate and every cargo invocation in CI passes `--locked`. Both desktop jobs failed on that line before compiling anything, so nothing about the code had been judged. The lockfile was generated in CI's own `ci-tauri:1.97` image — one container, `cargo fetch`, nothing built. `cargo fetch` and NOT `generate-lockfile`: the latter re-resolves from scratch and would have churned versions across the whole workspace to add one dependency. The diff is 67 insertions, zero deletions, six packages — tauri-plugin-global-shortcut plus global-hotkey, x11rb, x11rb-protocol, xkeysym and gethostname. Nothing existing moved. The feature itself, unchanged from42e06da: Press the combination anywhere and a small window arrives over whatever you were doing; type, Ctrl/Cmd+Enter, gone. The board never comes forward. There is no default shortcut on purpose — any default is a key combination taken away from something else on somebody's machine, silently, at install time. CommandOrControl+Shift+N is offered as a one-click suggestion. Stored and live are separate fields because they disagree: a combination another app holds is saved and does nothing when pressed, and a Wayland compositor may refuse global grabs outright. `capture_shortcut_set` registers before storing, so a refused combination is never written down as if it worked. The window hides rather than closes and keeps its text, so an interrupted capture is still there next press — which is what makes Escape safe. A failed save keeps it open too, rather than discarding the only copy of something just written in order to report a retryable problem. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c
125 lines
4.8 KiB
TypeScript
125 lines
4.8 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: "reminders", name: "reminders", component: () => import("../views/RemindersView.vue") },
|
|
{ path: "timeline", name: "timeline", component: () => import("../views/TimelineView.vue") },
|
|
],
|
|
},
|
|
{
|
|
// The quick-capture window (#1899). Its own route because it is its own
|
|
// WINDOW — no shell, no nav, one field. Desktop only: there is no global
|
|
// hotkey in a browser tab and nothing to summon it.
|
|
path: "/capture",
|
|
name: "capture",
|
|
component: () => import("../views/CaptureView.vue"),
|
|
meta: { requiresAuth: true, requiresDesktop: true },
|
|
},
|
|
{
|
|
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" };
|
|
}
|
|
// The capture window is opened at `index.html?capture=1` rather than at
|
|
// `/capture`, because the bundled assets are served as files and a path with no
|
|
// file behind it 404s in the production build — it only routes under the dev
|
|
// server. A query string survives that, and this is where it becomes a route.
|
|
if (to.query.capture === "1" && to.name !== "capture") {
|
|
return { name: "capture" };
|
|
}
|
|
// 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;
|