Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit9b776da

feat(settings): grouped sidebar nav across every settings page, was mostly missing

feat(settings): grouped sidebar nav across every settings page, was mostly missing

Only 8 of 23 real /settings/* pages rendered any navigation at all.
The nav component that did exist (SettingsSubnav, a flat 9-item pill
row) had its CSS defined inline on settings.tsx's own two pages only --
every other importer (passkeys, 2FA, sessions, integrations, agents,
deploy-targets, signing-keys) rendered it completely unstyled. The
remaining 14 pages -- tokens, billing, notifications, audit log, saved
replies, sponsors, OAuth apps/authorizations/developer-applications,
incident hooks -- had no navigation at all: landing on /settings/tokens
was a dead end, no way to reach any other settings page short of
backing out to /settings and re-navigating.

Replaces it with a single shared, grouped sidebar (SettingsNav +
settingsNavStyles in views/components.tsx, the same shared-component
file RepoHeader/RepoNav already live in) covering all real pages,
grouped the way any developer already expects from GitHub's own
settings sidebar: Account / Access & security / Applications /
Automation / Activity. Wired into all 19 settings page handlers
across 17 files. audit.tsx's AuditPage is shared between the personal
/settings/audit and repo-scoped /:owner/:repo/settings/audit variants,
so the sidebar is behind a `showSettingsNav` prop set only by the
personal call site -- the repo-scoped page is untouched.

bun test: 3107 pass, same 4 pre-existing unrelated failures, no
regressions.
ccantynz-alt committed on July 21, 2026Parent: 03e6f9b
18 files changed+3641409b776da794e886003653130253fb7f4e933e07a5
18 changed files+364−140
Modifiedsrc/routes/audit.tsx+50−30View fileUnifiedSplit
2626import type { AuthEnv } from "../middleware/auth";
2727import { requireAuth } from "../middleware/auth";
2828import { Layout } from "../views/layout";
29import { SettingsNav, settingsNavStyles } from "../views/components";
2930
3031const audit = new Hono<AuthEnv>();
3132
759760 eyebrowSuffix,
760761 rows,
761762 breadcrumb,
763 showSettingsNav,
762764}: {
763765 user: any;
764766 title: string;
766768 eyebrowSuffix: string;
767769 rows: AuditRow[];
768770 breadcrumb?: any;
771 /** Set only by the personal /settings/audit call site — the repo-scoped
772 * /:owner/:repo/settings/audit variant reuses this same component and
773 * must NOT show the account-settings sidebar. */
774 showSettingsNav?: boolean;
769775}) {
776 const body = (
777 <div class="audit-wrap">
778 <section class="audit-hero">
779 <div class="audit-hero-orb" aria-hidden="true" />
780 <div class="audit-hero-inner">
781 {breadcrumb && <div class="audit-breadcrumb">{breadcrumb}</div>}
782 <div class="audit-eyebrow">
783 <span class="audit-eyebrow-pill" aria-hidden="true">
784 <IconShield />
785 </span>
786 Audit log · <span class="audit-eyebrow-who">{user.username}</span>
787 {eyebrowSuffix && (
788 <>
789 {" "}· <span>{eyebrowSuffix}</span>
790 </>
791 )}
792 </div>
793 <h1 class="audit-title">
794 <span class="audit-title-grad">Trail.</span>
795 </h1>
796 <p class="audit-sub">{subtitle}</p>
797 <p class="audit-sub" style="margin-top: 8px; font-size: 13px;">
798 <strong>Also surfaces:</strong> findings from the AI proactive
799 monitor (<code>action=ai.monitor.*</code>) — stale TODOs, stuck
800 PRs, suspicious patterns it filed automatically. Filter by
801 actor <code>system</code> to see only the monitor's events.
802 </p>
803 </div>
804 </section>
805
806 <AuditList rows={rows} />
807 </div>
808 );
770809 return (
771810 <Layout title={title} user={user}>
772 <div class="audit-wrap">
773 <section class="audit-hero">
774 <div class="audit-hero-orb" aria-hidden="true" />
775 <div class="audit-hero-inner">
776 {breadcrumb && <div class="audit-breadcrumb">{breadcrumb}</div>}
777 <div class="audit-eyebrow">
778 <span class="audit-eyebrow-pill" aria-hidden="true">
779 <IconShield />
780 </span>
781 Audit log · <span class="audit-eyebrow-who">{user.username}</span>
782 {eyebrowSuffix && (
783 <>
784 {" "}· <span>{eyebrowSuffix}</span>
785 </>
786 )}
787 </div>
788 <h1 class="audit-title">
789 <span class="audit-title-grad">Trail.</span>
790 </h1>
791 <p class="audit-sub">{subtitle}</p>
792 <p class="audit-sub" style="margin-top: 8px; font-size: 13px;">
793 <strong>Also surfaces:</strong> findings from the AI proactive
794 monitor (<code>action=ai.monitor.*</code>) — stale TODOs, stuck
795 PRs, suspicious patterns it filed automatically. Filter by
796 actor <code>system</code> to see only the monitor's events.
797 </p>
811 {showSettingsNav ? (
812 <>
813 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
814 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
815 <SettingsNav active="audit" />
816 <div class="settings-content">{body}</div>
798817 </div>
799 </section>
800
801 <AuditList rows={rows} />
802 </div>
818 </>
819 ) : (
820 body
821 )}
803822 <style dangerouslySetInnerHTML={{ __html: AUDIT_CSS }} />
804823 <script dangerouslySetInnerHTML={{ __html: AUDIT_JS }} />
805824 </Layout>
845864 </>
846865 }
847866 rows={rows}
867 showSettingsNav
848868 />
849869 );
850870});
Modifiedsrc/routes/billing.tsx+6−1View fileUnifiedSplit
2020import { db } from "../db";
2121import { users, userQuotas } from "../db/schema";
2222import { Layout } from "../views/layout";
23import { SettingsNav, settingsNavStyles } from "../views/components";
2324import { softAuth, requireAuth } from "../middleware/auth";
2425import type { AuthEnv } from "../middleware/auth";
2526import { isSiteAdmin } from "../lib/admin";
603604 return c.html(
604605 <Layout title="Billing — Gluecron" user={user}>
605606 <style dangerouslySetInnerHTML={{ __html: styles }} />
606 <div class="bill-wrap">
607 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
608 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
609 <SettingsNav active="billing" />
610 <div class="bill-wrap settings-content" style="max-width:none;margin:0;padding:0">
607611 {/* ─── Hero ─── */}
608612 <section class="bill-hero">
609613 <div class="bill-hero-orb" aria-hidden="true" />
832836 <a href="/pricing">Detailed plan comparison &rarr;</a>
833837 </div>
834838 </div>
839 </div>
835840 </Layout>
836841 );
837842});
Modifiedsrc/routes/deploy-targets.tsx+6−3View fileUnifiedSplit
1616import { db } from "../db";
1717import { serverTargets } from "../db/schema";
1818import { Layout } from "../views/layout";
19import { SettingsSubnav } from "./settings";
19import { SettingsNav, settingsNavStyles } from "../views/components";
2020import type { AuthEnv } from "../middleware/auth";
2121import { requireAuth } from "../middleware/auth";
2222import {
417417 return c.html(
418418 <Layout title="Deploy targets — settings" user={user}>
419419 <style dangerouslySetInnerHTML={{ __html: styles }} />
420 <SettingsSubnav active="deploy-targets" />
421 <div class="dt-wrap">
420 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
421 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
422 <SettingsNav active="deploy-targets" />
423 <div class="dt-wrap settings-content" style="max-width:none;margin:0;padding:0">
422424 {/* ─── Hero ─── */}
423425 <div class="dt-hero">
424426 <div class="dt-hero-orb" aria-hidden="true" />
647649 </form>
648650 </section>
649651 </div>
652 </div>
650653 </Layout>
651654 );
652655});
Modifiedsrc/routes/developer-apps.tsx+6−1View fileUnifiedSplit
2222import { db } from "../db";
2323import { oauthApps } from "../db/schema";
2424import { Layout } from "../views/layout";
25import { SettingsNav, settingsNavStyles } from "../views/components";
2526import { requireAuth } from "../middleware/auth";
2627import type { AuthEnv } from "../middleware/auth";
2728import {
594595
595596 return c.html(
596597 <Layout title="OAuth applications" user={user}>
597 <div class="dev-wrap">
598 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
599 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
600 <SettingsNav active="applications" />
601 <div class="dev-wrap settings-content" style="max-width:none;margin:0;padding:0">
598602 <div class="dev-breadcrumb">
599603 <a href="/settings">settings</a>
600604 <span class="sep">/</span>
697701 </>
698702 )}
699703 </div>
704 </div>
700705 <style dangerouslySetInnerHTML={{ __html: devStyles }} />
701706 </Layout>
702707 );
Modifiedsrc/routes/incident-hooks.tsx+6−1View fileUnifiedSplit
3838import { softAuth, requireAuth } from "../middleware/auth";
3939import type { AuthEnv } from "../middleware/auth";
4040import { Layout } from "../views/layout";
41import { SettingsNav, settingsNavStyles } from "../views/components";
4142import {
4243 createOrUpdateFileOnBranch,
4344 getDefaultBranch,
10421043
10431044 return c.html(
10441045 <Layout title="Incident Auto-Fix — Settings" user={user}>
1045 <div class="ih-wrap">
1046 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
1047 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
1048 <SettingsNav active="incident-hooks" />
1049 <div class="ih-wrap settings-content" style="max-width:none;margin:0;padding:0">
10461050 <div class="ih-hero">
10471051 <div class="ih-hero-orb" aria-hidden="true" />
10481052 <div class="ih-hero-inner">
12131217 </div>
12141218 </div>
12151219 </div>
1220 </div>
12161221 <style dangerouslySetInnerHTML={{ __html: ihStyles }} />
12171222 </Layout>
12181223 );
Modifiedsrc/routes/marketplace.tsx+6−1View fileUnifiedSplit
2222import { db } from "../db";
2323import { appInstallations } from "../db/schema";
2424import { Layout } from "../views/layout";
25import { SettingsNav, settingsNavStyles } from "../views/components";
2526import { softAuth, requireAuth } from "../middleware/auth";
2627import type { AuthEnv } from "../middleware/auth";
2728import {
10271028 return c.html(
10281029 <Layout title="Installed apps — Gluecron" user={user}>
10291030 <style dangerouslySetInnerHTML={{ __html: styles }} />
1030 <div class="mkt-wrap">
1031 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
1032 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
1033 <SettingsNav active="apps" />
1034 <div class="mkt-wrap settings-content" style="max-width:none;margin:0;padding:0">
10311035 <section class="mkt-hero">
10321036 <div class="mkt-hero-orb" aria-hidden="true" />
10331037 <div class="mkt-hero-inner">
10981102 </div>
10991103 )}
11001104 </div>
1105 </div>
11011106 </Layout>
11021107 );
11031108});
Modifiedsrc/routes/oauth.tsx+6−1View fileUnifiedSplit
2929} from "../db/schema";
3030import type { User } from "../db/schema";
3131import { Layout } from "../views/layout";
32import { SettingsNav, settingsNavStyles } from "../views/components";
3233import { requireAuth } from "../middleware/auth";
3334import type { AuthEnv } from "../middleware/auth";
3435import {
12581259
12591260 return c.html(
12601261 <Layout title="Authorized applications" user={user}>
1261 <div class="oauth-wrap">
1262 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
1263 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
1264 <SettingsNav active="authorizations" />
1265 <div class="oauth-wrap settings-content" style="max-width:none;margin:0;padding:0">
12621266 <div class="oauth-breadcrumb">
12631267 <a href="/settings">settings</a>
12641268 <span class="sep">/</span>
13391343 rules, token TTLs — is at <a href="/api-docs">/api-docs</a>.
13401344 </div>
13411345 </div>
1346 </div>
13421347 <style dangerouslySetInnerHTML={{ __html: oauthStyles }} />
13431348 </Layout>
13441349 );
Modifiedsrc/routes/passkeys.tsx+6−3View fileUnifiedSplit
3030import type { AuthEnv } from "../middleware/auth";
3131import { requireAuth } from "../middleware/auth";
3232import { Layout } from "../views/layout";
33import { SettingsSubnav } from "./settings";
33import { SettingsNav, settingsNavStyles } from "../views/components";
3434import {
3535 startRegistration,
3636 finishRegistration,
639639
640640 return c.html(
641641 <Layout title="Passkeys" user={user}>
642 <SettingsSubnav active="passkeys" />
643 <div class="pk-wrap">
642 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
643 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
644 <SettingsNav active="passkeys" />
645 <div class="pk-wrap settings-content" style="max-width:none;margin:0;padding:0">
644646 <section class="pk-hero">
645647 <div class="pk-hero-orb" aria-hidden="true" />
646648 <div class="pk-hero-inner">
961963 }}
962964 />
963965 </div>
966 </div>
964967 <style dangerouslySetInnerHTML={{ __html: pkStyles }} />
965968 </Layout>
966969 );
Modifiedsrc/routes/saved-replies.tsx+6−1View fileUnifiedSplit
2222import type { AuthEnv } from "../middleware/auth";
2323import { requireAuth } from "../middleware/auth";
2424import { Layout } from "../views/layout";
25import { SettingsNav, settingsNavStyles } from "../views/components";
2526
2627const replies = new Hono<AuthEnv>();
2728
468469
469470 return c.html(
470471 <Layout title="Saved replies" user={user}>
471 <div class="sr-wrap">
472 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
473 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
474 <SettingsNav active="replies" />
475 <div class="sr-wrap settings-content" style="max-width:none;margin:0;padding:0">
472476 <section class="sr-hero">
473477 <div class="sr-hero-orb" aria-hidden="true" />
474478 <div class="sr-hero-inner">
658662 </section>
659663 )}
660664 </div>
665 </div>
661666 <style dangerouslySetInnerHTML={{ __html: srStyles }} />
662667 <script
663668 dangerouslySetInnerHTML={{
Modifiedsrc/routes/settings-2fa.tsx+6−3View fileUnifiedSplit
2222import type { AuthEnv } from "../middleware/auth";
2323import { requireAuth } from "../middleware/auth";
2424import { Layout } from "../views/layout";
25import { SettingsSubnav } from "./settings";
25import { SettingsNav, settingsNavStyles } from "../views/components";
2626import { verifyPassword } from "../lib/auth";
2727import {
2828 generateTotpSecret,
563563
564564 return c.html(
565565 <Layout title="Two-factor authentication" user={user}>
566 <SettingsSubnav active="2fa" />
567 <div class="tfa-wrap">
566 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
567 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
568 <SettingsNav active="2fa" />
569 <div class="tfa-wrap settings-content" style="max-width:none;margin:0;padding:0">
568570 <section class="tfa-hero">
569571 <div class="tfa-hero-orb" aria-hidden="true" />
570572 <div class="tfa-hero-inner">
750752 </>
751753 )}
752754 </div>
755 </div>
753756 <style dangerouslySetInnerHTML={{ __html: tfaStyles }} />
754757 </Layout>
755758 );
Modifiedsrc/routes/settings-agents.tsx+8−3View fileUnifiedSplit
1212import { db } from "../db";
1313import { agentLeases, agentSessions } from "../db/schema";
1414import { Layout } from "../views/layout";
15import { SettingsSubnav } from "./settings";
15import { SettingsNav, settingsNavStyles } from "../views/components";
1616import type { AuthEnv } from "../middleware/auth";
1717import { requireAuth } from "../middleware/auth";
1818import {
2828settingsAgents.use("/settings/agents/*", requireAuth);
2929
3030const styles = `
31 .sa-wrap { max-width: 1320px; margin: 0 auto; padding: var(--space-6) var(--space-4); }
31 .sa-wrap { max-width: none; margin: 0; padding: 0; }
3232 .sa-hero {
3333 border: 1px solid var(--border);
3434 border-radius: 14px;
366366
367367 return c.html(
368368 <Layout title="Agent sessions" user={user}>
369 <SettingsSubnav active="agents" />
369 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
370 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
371 <SettingsNav active="agents" />
372 <div class="settings-content">
370373 {raw(body)}
374 </div>
375 </div>
371376 </Layout>
372377 );
373378});
Modifiedsrc/routes/settings-integrations.tsx+6−3View fileUnifiedSplit
2121import { db } from "../db";
2222import { chatIntegrations, repositories } from "../db/schema";
2323import { Layout } from "../views/layout";
24import { SettingsSubnav } from "./settings";
24import { SettingsNav, settingsNavStyles } from "../views/components";
2525import { softAuth, requireAuth } from "../middleware/auth";
2626import type { AuthEnv } from "../middleware/auth";
2727import { notifyChatChannels } from "../lib/chat-notifier";
384384 return c.html(
385385 <Layout title="Chat integrations" user={user}>
386386 <style dangerouslySetInnerHTML={{ __html: chatiStyles }} />
387 <SettingsSubnav active="integrations" />
388 <div class="chati-wrap">
387 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
388 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
389 <SettingsNav active="integrations" />
390 <div class="chati-wrap settings-content" style="max-width:none;margin:0;padding:0">
389391 <div class="chati-hero">
390392 <div class="chati-hero-orb" aria-hidden="true" />
391393 <div class="chati-hero-inner">
642644 </form>
643645 </section>
644646 </div>
647 </div>
645648 </Layout>
646649 );
647650});
Modifiedsrc/routes/settings-sessions.tsx+6−3View fileUnifiedSplit
1414import { requireAuth } from "../middleware/auth";
1515import type { AuthEnv } from "../middleware/auth";
1616import { Layout } from "../views/layout";
17import { SettingsSubnav } from "./settings";
17import { SettingsNav, settingsNavStyles } from "../views/components";
1818
1919const settingsSessions = new Hono<AuthEnv>();
2020// SECURITY: "path*" (no slash before the *) doesn't match the bare path in
121121 return c.html(
122122 <Layout title="Active sessions" user={user}>
123123 <style dangerouslySetInnerHTML={{ __html: sessionsStyles }} />
124 <SettingsSubnav active="sessions" />
125 <div class="sessions-page">
124 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
125 <div class="settings-shell" style="max-width:1200px;margin:0 auto;padding:var(--space-4)">
126 <SettingsNav active="sessions" />
127 <div class="sessions-page settings-content" style="max-width:none;margin:0;padding:0">
126128 <div class="sessions-hero">
127129 <h1>Active sessions</h1>
128130 <p>
216218 </div>
217219 )}
218220 </div>
221 </div>
219222 </Layout>
220223 );
221224});
Modifiedsrc/routes/settings.tsx+18−81View fileUnifiedSplit
1010import type { AuthEnv } from "../middleware/auth";
1111import { requireAuth } from "../middleware/auth";
1212import { Layout } from "../views/layout";
13import { SettingsNav, settingsNavStyles } from "../views/components";
1314import { raw } from "hono/html";
1415import { composeDigest } from "../lib/email-digest";
1516import {
117118 line-height: 1.5;
118119 }
119120
120 /* ─── Sub-nav (pill row) ─── */
121 .settings-subnav {
122 display: flex;
123 gap: 4px;
124 flex-wrap: wrap;
125 margin-bottom: var(--space-5);
126 padding: 4px;
127 background: var(--bg-elevated);
128 border: 1px solid var(--border);
129 border-radius: 9999px;
130 width: fit-content;
131 max-width: 100%;
132 overflow-x: auto;
133 }
134 .settings-subnav a {
135 display: inline-flex;
136 align-items: center;
137 gap: 6px;
138 padding: 6px 14px;
139 font-size: 13px;
140 font-weight: 500;
141 color: var(--text-muted);
142 border-radius: 9999px;
143 text-decoration: none;
144 white-space: nowrap;
145 transition: all 120ms ease;
146 }
147 .settings-subnav a:hover {
148 color: var(--text-strong);
149 background: var(--bg-hover);
150 }
151 .settings-subnav a.is-active {
152 color: var(--text-strong);
153 background: rgba(91,110,232,0.16);
154 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.35);
155 }
156
157121 /* ─── Section cards ─── */
158122 .settings-section {
159123 background: var(--bg-elevated);
553517 .settings-section-foot,
554518 .settings-danger-head,
555519 .settings-danger-body { padding-left: var(--space-4); padding-right: var(--space-4); }
556 .settings-subnav { width: 100%; }
557 .settings-subnav a { min-height: 40px; padding: 10px 14px; }
558520 .settings-section-foot { justify-content: flex-start; flex-wrap: wrap; }
559521 .settings-section-foot .settings-foot-hint { margin-right: 0; width: 100%; margin-bottom: 6px; }
560522 .settings-danger-row { flex-direction: column; align-items: stretch; }
598560 );
599561}
600562
601/** Pill-row sub-navigation, hand-built so we don't depend on shared components. */
602export type SettingsSubnavKey =
603 | "profile"
604 | "keys"
605 | "agents"
606 | "deploy-targets"
607 | "2fa"
608 | "passkeys"
609 | "signing-keys"
610 | "sessions"
611 | "integrations";
612
613export function SettingsSubnav(props: { active: SettingsSubnavKey }) {
614 const items: Array<{ key: SettingsSubnavKey; href: string; label: string }> = [
615 { key: "profile", href: "/settings", label: "Profile" },
616 { key: "keys", href: "/settings/keys", label: "SSH keys" },
617 { key: "signing-keys", href: "/settings/signing-keys", label: "Signing keys" },
618 { key: "2fa", href: "/settings/2fa", label: "2FA" },
619 { key: "passkeys", href: "/settings/passkeys", label: "Passkeys" },
620 { key: "sessions", href: "/settings/sessions", label: "Sessions" },
621 { key: "integrations", href: "/settings/integrations", label: "Integrations" },
622 { key: "agents", href: "/settings/agents", label: "Agents" },
623 { key: "deploy-targets", href: "/settings/deploy-targets", label: "Deploy targets" },
624 ];
625 return (
626 <nav class="settings-subnav" aria-label="Settings sections">
627 {items.map((it) => (
628 <a
629 href={it.href}
630 class={it.key === props.active ? "is-active" : ""}
631 aria-current={it.key === props.active ? "page" : undefined}
632 >
633 {it.label}
634 </a>
635 ))}
636 </nav>
637 );
638}
639
640563function Banner(props: { kind: "success" | "error"; text: string }) {
641564 return (
642565 <div class={`settings-banner settings-banner-${props.kind}`} role="status">
670593 return c.html(
671594 <Layout title="Settings" user={user}>
672595 <style dangerouslySetInnerHTML={{ __html: settingsStyles }} />
596 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
673597 <div class="settings-container">
674598 <SettingsHero
675599 username={user.username}
677601 accent="settings"
678602 sub="Profile, notifications, sleep mode, and account controls — all in one place."
679603 />
680 <SettingsSubnav active="profile" />
604 <div class="settings-shell">
605 <SettingsNav active="profile" />
606 <div class="settings-content">
681607
682608 {/* Connect Claude · /connect/claude — visually distinct row that sits
683609 above the standard settings sections. Tiny, intentional break from
11541080
11551081 <script dangerouslySetInnerHTML={{ __html: pushDeviceScript }} />
11561082 {renderDeleteAccountSection({ user, csrfToken: c.get("csrfToken") })}
1083 </div>
1084 </div>
11571085 </div>
11581086 </Layout>
11591087 );
16991627
17001628 return c.html(
17011629 <Layout title="Notification preferences" user={user}>
1702 <div class="notifset-wrap">
1630 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
1631 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
1632 <SettingsNav active="notifications" />
1633 <div class="notifset-wrap settings-content" style="max-width:none;margin:0;padding:0">
17031634 <section class="notifset-hero">
17041635 <div class="notifset-hero-orb" aria-hidden="true" />
17051636 <div class="notifset-hero-inner">
20321963 </section>
20331964 </form>
20341965 </div>
1966 </div>
20351967 <style dangerouslySetInnerHTML={{ __html: notifsetStyles }} />
20361968 </Layout>
20371969 );
22522184 return c.html(
22532185 <Layout title="SSH Keys" user={user}>
22542186 <style dangerouslySetInnerHTML={{ __html: settingsStyles }} />
2187 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
22552188 <div class="settings-container">
22562189 <SettingsHero
22572190 username={user.username}
22592192 accent="keys"
22602193 sub="Authenticate git pushes and clones over SSH. Each device or workstation gets its own key."
22612194 />
2262 <SettingsSubnav active="keys" />
2195 <div class="settings-shell">
2196 <SettingsNav active="keys" />
2197 <div class="settings-content">
22632198
22642199 {success && (
22652200 <Banner kind="success" text={decodeURIComponent(success)} />
23692304 </div>
23702305 </form>
23712306 </section>
2307 </div>
2308 </div>
23722309 </div>
23732310 </Layout>
23742311 );
Modifiedsrc/routes/signing-keys.tsx+6−3View fileUnifiedSplit
2323
2424import { Hono } from "hono";
2525import { Layout } from "../views/layout";
26import { SettingsSubnav } from "./settings";
26import { SettingsNav, settingsNavStyles } from "../views/components";
2727import type { AuthEnv } from "../middleware/auth";
2828import { requireAuth } from "../middleware/auth";
2929import {
440440 const now = new Date();
441441 return c.html(
442442 <Layout title="Signing keys" user={user}>
443 <SettingsSubnav active="signing-keys" />
444 <div class="sk-wrap">
443 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
444 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
445 <SettingsNav active="signing-keys" />
446 <div class="sk-wrap settings-content" style="max-width:none;margin:0;padding:0">
445447 <header class="sk-head">
446448 <div class="sk-eyebrow">
447449 <span class="sk-eyebrow-pill" aria-hidden="true">
639641 </form>
640642 </section>
641643 </div>
644 </div>
642645 <style dangerouslySetInnerHTML={{ __html: signingKeyStyles }} />
643646 </Layout>
644647 );
Modifiedsrc/routes/sponsors.tsx+6−1View fileUnifiedSplit
2323 users,
2424} from "../db/schema";
2525import { Layout } from "../views/layout";
26import { SettingsNav, settingsNavStyles } from "../views/components";
2627import { softAuth, requireAuth } from "../middleware/auth";
2728import type { AuthEnv } from "../middleware/auth";
2829
916917
917918 return c.html(
918919 <Layout title="Sponsorship settings" user={user}>
919 <div class="spons-wrap">
920 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
921 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
922 <SettingsNav active="sponsors" />
923 <div class="spons-wrap settings-content" style="max-width:none;margin:0;padding:0">
920924 <section class="spons-hero">
921925 <div class="spons-hero-orb" aria-hidden="true" />
922926 <div class="spons-hero-inner">
11271131 </div>
11281132 </section>
11291133 </div>
1134 </div>
11301135 <style dangerouslySetInnerHTML={{ __html: sponsStyles }} />
11311136 </Layout>
11321137 );
Modifiedsrc/routes/tokens.tsx+6−1View fileUnifiedSplit
77import { db } from "../db";
88import { apiTokens } from "../db/schema";
99import { Layout } from "../views/layout";
10import { SettingsNav, settingsNavStyles } from "../views/components";
1011import { softAuth, requireAuth } from "../middleware/auth";
1112import type { AuthEnv } from "../middleware/auth";
1213
616617 return c.html(
617618 <Layout title="API Tokens" user={user}>
618619 <style dangerouslySetInnerHTML={{ __html: tokensStyles }} />
619 <div class="tokens-wrap">
620 <style dangerouslySetInnerHTML={{ __html: settingsNavStyles }} />
621 <div class="settings-shell" style="max-width:1500px;margin:0 auto;padding:var(--space-4)">
622 <SettingsNav active="tokens" />
623 <div class="tokens-wrap settings-content" style="max-width:none;margin:0;padding:0">
620624 {/* ─── Hero ─── */}
621625 <div class="tokens-hero">
622626 <div class="tokens-hero-bg" aria-hidden="true">
841845 </div>
842846 </div>
843847 </div>
848 </div>
844849 </Layout>
845850 );
846851});
Modifiedsrc/views/components.tsx+204−0View fileUnifiedSplit
885885 </div>
886886 </div>
887887);
888
889// ---------------------------------------------------------------------------
890// Settings navigation — shared across every /settings/* page.
891//
892// Previously each settings page either duplicated a flat "pill row" nav
893// (styled only on the two pages settings.tsx itself renders — every other
894// importer got unstyled raw links, since the CSS lived inline in that one
895// file) or, for most settings pages (tokens, billing, notifications, audit,
896// saved replies, sponsors, OAuth apps/authorizations, incident hooks...),
897// rendered no navigation at all. Landing on /settings/tokens was a dead end:
898// no way to reach any other settings page except backing out to /settings.
899//
900// This is the single source of truth for the settings IA: a grouped
901// sidebar (profile / access & security / applications / automation /
902// account / activity), matching the standard every developer already
903// knows from GitHub's own settings sidebar, covering every real
904// /settings/* page in the app.
905// ---------------------------------------------------------------------------
906
907export type SettingsNavKey =
908 | "profile"
909 | "notifications"
910 | "replies"
911 | "billing"
912 | "sponsors"
913 | "keys"
914 | "signing-keys"
915 | "2fa"
916 | "passkeys"
917 | "sessions"
918 | "tokens"
919 | "applications"
920 | "apps"
921 | "authorizations"
922 | "agents"
923 | "deploy-targets"
924 | "integrations"
925 | "incident-hooks"
926 | "audit";
927
928interface SettingsNavItem {
929 key: SettingsNavKey;
930 href: string;
931 label: string;
932}
933
934interface SettingsNavGroup {
935 label: string;
936 items: SettingsNavItem[];
937}
938
939const SETTINGS_NAV_GROUPS: SettingsNavGroup[] = [
940 {
941 label: "Account",
942 items: [
943 { key: "profile", href: "/settings", label: "Public profile" },
944 { key: "notifications", href: "/settings/notifications", label: "Notifications" },
945 { key: "replies", href: "/settings/replies", label: "Saved replies" },
946 { key: "billing", href: "/settings/billing", label: "Billing" },
947 { key: "sponsors", href: "/settings/sponsors", label: "Sponsorship" },
948 ],
949 },
950 {
951 label: "Access & security",
952 items: [
953 { key: "keys", href: "/settings/keys", label: "SSH keys" },
954 { key: "signing-keys", href: "/settings/signing-keys", label: "Signing keys" },
955 { key: "2fa", href: "/settings/2fa", label: "Two-factor auth" },
956 { key: "passkeys", href: "/settings/passkeys", label: "Passkeys" },
957 { key: "sessions", href: "/settings/sessions", label: "Sessions" },
958 { key: "tokens", href: "/settings/tokens", label: "Personal access tokens" },
959 ],
960 },
961 {
962 label: "Applications",
963 items: [
964 { key: "applications", href: "/settings/applications", label: "Developer applications" },
965 { key: "apps", href: "/settings/apps", label: "Installed apps" },
966 { key: "authorizations", href: "/settings/authorizations", label: "Authorized applications" },
967 ],
968 },
969 {
970 label: "Automation",
971 items: [
972 { key: "agents", href: "/settings/agents", label: "Agent sessions" },
973 { key: "deploy-targets", href: "/settings/deploy-targets", label: "Deploy targets" },
974 { key: "integrations", href: "/settings/integrations", label: "Chat integrations" },
975 { key: "incident-hooks", href: "/settings/incident-hooks", label: "Incident hooks" },
976 ],
977 },
978 {
979 label: "Activity",
980 items: [{ key: "audit", href: "/settings/audit", label: "Audit log" }],
981 },
982];
983
984export const SettingsNav: FC<{ active: SettingsNavKey }> = ({ active }) => (
985 <nav class="stgnav" aria-label="Settings sections">
986 {SETTINGS_NAV_GROUPS.map((group) => (
987 <div class="stgnav-group">
988 <div class="stgnav-label">{group.label}</div>
989 {group.items.map((it) => (
990 <a
991 href={it.href}
992 class={it.key === active ? "is-active" : ""}
993 aria-current={it.key === active ? "page" : undefined}
994 >
995 {it.label}
996 </a>
997 ))}
998 </div>
999 ))}
1000 </nav>
1001);
1002
1003/**
1004 * Shared shell CSS. Every /settings/* page includes this once (it's cheap,
1005 * inline, and idempotent — duplicate <style> blocks with the same rules
1006 * are harmless) and wraps its content in `.settings-shell` /
1007 * `.settings-content` alongside a `<SettingsNav active="...">`.
1008 */
1009export const settingsNavStyles = `
1010 .settings-shell {
1011 display: grid;
1012 grid-template-columns: 220px minmax(0, 1fr);
1013 gap: var(--space-6);
1014 align-items: start;
1015 }
1016 .stgnav {
1017 position: sticky;
1018 top: var(--space-4);
1019 display: flex;
1020 flex-direction: column;
1021 gap: var(--space-5);
1022 }
1023 .stgnav-group {
1024 display: flex;
1025 flex-direction: column;
1026 gap: 1px;
1027 }
1028 .stgnav-label {
1029 font-size: 11px;
1030 font-weight: 700;
1031 letter-spacing: 0.06em;
1032 text-transform: uppercase;
1033 color: var(--text-muted);
1034 padding: 4px 10px;
1035 margin-bottom: 2px;
1036 }
1037 .stgnav a {
1038 display: block;
1039 padding: 6px 10px;
1040 border-radius: 6px;
1041 font-size: 13.5px;
1042 font-weight: 500;
1043 color: var(--text-muted);
1044 text-decoration: none;
1045 white-space: nowrap;
1046 overflow: hidden;
1047 text-overflow: ellipsis;
1048 transition: background 120ms ease, color 120ms ease;
1049 }
1050 .stgnav a:hover {
1051 color: var(--text-strong);
1052 background: var(--bg-hover);
1053 }
1054 .stgnav a.is-active {
1055 color: var(--text-strong);
1056 background: rgba(91,110,232,0.14);
1057 box-shadow: inset 0 0 0 1px rgba(91,110,232,0.3);
1058 font-weight: 600;
1059 }
1060 .settings-content {
1061 min-width: 0;
1062 }
1063 @media (max-width: 860px) {
1064 .settings-shell {
1065 display: block;
1066 }
1067 .stgnav {
1068 position: static;
1069 flex-direction: row;
1070 flex-wrap: wrap;
1071 gap: var(--space-3);
1072 margin-bottom: var(--space-5);
1073 padding: 4px;
1074 background: var(--bg-elevated);
1075 border: 1px solid var(--border);
1076 border-radius: var(--radius);
1077 }
1078 .stgnav-group {
1079 flex-direction: row;
1080 flex-wrap: wrap;
1081 gap: 2px;
1082 }
1083 .stgnav-label {
1084 display: none;
1085 }
1086 .stgnav a {
1087 padding: 6px 12px;
1088 border-radius: 9999px;
1089 }
1090 }
1091`;
8881092