From c0caec41de5775e7b76a6b6f7a42db58adee7eff Mon Sep 17 00:00:00 2001 From: pulk17 Date: Sun, 27 Sep 2026 13:27:30 +0530 Subject: [PATCH 1/3] fix console routes under /app/ the router matched paths from the domain root, so every page showed Not Found once served under /app/ and sidebar links pointed back at the old site. --- web/src/router.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/src/router.tsx b/web/src/router.tsx index e1cf931f1..dfae8c471 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -140,7 +140,13 @@ const routeTree = rootRoute.addChildren([ adminRoute, ]); -export const router = createRouter({ routeTree }); +// Served under /app/ in production, so routes are matched below that +// prefix. A relative base (the demo build) is served from the root. +const base = import.meta.env.BASE_URL; +export const router = createRouter({ + routeTree, + basepath: base.startsWith("/") ? base : "/", +}); declare module "@tanstack/react-router" { interface Register { From fd01f078d4cc2254be0cf988270be7cba4459fca Mon Sep 17 00:00:00 2001 From: pulk17 Date: Sun, 27 Sep 2026 13:37:55 +0530 Subject: [PATCH 2/3] send github linking to the classic account page the oauth callback is registered to the classic site and reads its cookie session, so starting it from the console crashed the callback. also moves two setState calls out of effects, which the oxlint in #1209 rejects. --- web/src/components/ResetPassword.tsx | 32 ++++++++++++---------------- web/src/pages/Account.tsx | 20 ++++++++++------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/web/src/components/ResetPassword.tsx b/web/src/components/ResetPassword.tsx index 38a8c011f..626230ac3 100644 --- a/web/src/components/ResetPassword.tsx +++ b/web/src/components/ResetPassword.tsx @@ -1,6 +1,6 @@ import { KeyRound, Loader2 } from "lucide-react"; import { motion } from "motion/react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -15,30 +15,26 @@ import { asset } from "@/lib/utils"; * signed link: they are handed straight back to the API, which is what * checks them. Nothing here can decide whether a link is good. */ +// Read from the URL rather than the router: the link is built by the +// platform's email template, and lands here under either history mode. +function readResetLink(): { uid: number; expires: number; mac: string } | null { + const raw = window.location.href; + const query = raw.slice(raw.indexOf("?") + 1); + const q = new URLSearchParams(raw.includes("?") ? query : ""); + const uid = Number(q.get("uid")); + const expires = Number(q.get("expires")); + const mac = q.get("mac"); + return uid && expires && mac ? { uid, expires, mac } : null; +} + export function ResetPassword() { - const [params, setParams] = useState<{ - uid: number; - expires: number; - mac: string; - } | null>(null); + const [params] = useState(readResetLink); const [password, setPassword] = useState(""); const [confirm, setConfirm] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [done, setDone] = useState(false); - // Read from the URL rather than the router: the link is built by the - // platform's email template, and lands here under either history mode. - useEffect(() => { - const raw = window.location.href; - const query = raw.slice(raw.indexOf("?") + 1); - const q = new URLSearchParams(raw.includes("?") ? query : ""); - const uid = Number(q.get("uid")); - const expires = Number(q.get("expires")); - const mac = q.get("mac"); - if (uid && expires && mac) setParams({ uid, expires, mac }); - }, []); - const submit = async (e: React.FormEvent) => { e.preventDefault(); if (!params) return; diff --git a/web/src/pages/Account.tsx b/web/src/pages/Account.tsx index fdfca2240..b197bbf69 100644 --- a/web/src/pages/Account.tsx +++ b/web/src/pages/Account.tsx @@ -1,6 +1,6 @@ import { useQueryClient } from "@tanstack/react-query"; import { ExternalLink, GitBranch, KeyRound, Loader2, UserRound, UserX } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { ConfirmDialog } from "@/components/ui/confirm"; @@ -61,11 +61,14 @@ function ProfileSection({ name, email }: Readonly<{ name: string; email: string const [saved, setSaved] = useState(false); // The query refetches after a save, so pick the server's values back up - // rather than leaving whatever was typed sitting in the inputs. - useEffect(() => { + // rather than leaving whatever was typed sitting in the inputs. Done while + // rendering, not in an effect, so the stale values never paint. + const [synced, setSynced] = useState({ name, email }); + if (synced.name !== name || synced.email !== email) { + setSynced({ name, email }); setDraftName(name); setDraftEmail(email); - }, [name, email]); + } const emailChanged = draftEmail !== email; const nameChanged = draftName !== name; @@ -279,18 +282,19 @@ function GithubSection() { {busy && } Disconnect ) : ( - + )} - {/* The redirect finishes on the classic site, so this page will - not know about it until it is loaded again. */} + {/* GitHub's callback is registered to the classic site and reads + its own sign-in, so the link is made there. This page will not + know about it until it is loaded again. */}

{data.linked ? "Disconnecting only forgets the platform's copy. Withdraw the authorisation itself from your GitHub applications page." - : "GitHub opens in a new tab. Reload this page once you are done there."} + : "Opens your account on the classic site in a new tab, where GitHub is connected. Reload this page once you are done there."}

)} From a53620432773f9a0ae5edcabfd2afe1aa6403eb2 Mon Sep 17 00:00:00 2001 From: pulk17 Date: Sun, 27 Sep 2026 13:37:55 +0530 Subject: [PATCH 3/3] redirect bare /app and keep hsts on console responses add_header in a location drops the server-level hsts header, and index.html was never revalidated after a deploy. --- install/nginx.conf | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/install/nginx.conf b/install/nginx.conf index 21381b764..3158c6421 100644 --- a/install/nginx.conf +++ b/install/nginx.conf @@ -47,6 +47,10 @@ server { # Web console: a static build served straight off disk. Everything it # talks to is under /api on this same host, so no CORS is involved. # + location = /app { + return 301 /app/; + } + # The directory is named to match the URL so plain root resolution # applies. "alias" with try_files appends the whole URI to the alias # rather than the remainder, which silently breaks the SPA fallback. @@ -54,13 +58,17 @@ server { include /etc/nginx/mime.types; root NGINX_DIR/web; try_files $uri $uri/ /app/index.html; + # index.html names the hashed bundles, so it must be revalidated or a + # browser keeps loading the previous build after a deploy. "expires" + # rather than add_header: any add_header here would stop the server + # block's HSTS header being inherited. + expires -1; } location ^~ /app/assets/ { include /etc/nginx/mime.types; root NGINX_DIR/web; - expires 30d; # hashed filenames - add_header Cache-Control "public, immutable"; + expires max; # hashed filenames } location / {