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

Add GitHub unlink route + restructure notification preferences into categories

Add GitHub unlink route + restructure notification preferences into categories

- POST /settings/github/unlink: removes the github: sso_user_links row(s)
  for the current user, audits the action, and redirects with a success
  banner. Leaves enterprise OIDC and google: links intact.
- GET /settings shows a "Disconnect GitHub" button section only when the
  user has an active GitHub OAuth link (queried via sso_user_links).
- /settings/notifications reorganised from flat email/push channel lists
  into 4 semantic categories: AI activity, CI/CD, Code review, Mentions.
  Same underlying DB fields (no migration) — only the UI grouping changed.
- Email channel pill count updated from 4 to 5 to include
  notify_email_on_pending_comment.

https://claude.ai/code/session_01DzJMTFASjMHt2f5ze4cNLR
Claude committed on June 6, 2026Parent: c78704e
1 file changed+2178691d778c790916fc201cda367870ebdb3528f3f8c
1 changed file+217−86
Modifiedsrc/routes/settings.tsx+217−86View fileUnifiedSplit
55import { Hono } from "hono";
66import { eq } from "drizzle-orm";
77import { db } from "../db";
8import { users, sshKeys } from "../db/schema";
8import { users, sshKeys, ssoUserLinks } from "../db/schema";
99import { getStandupPrefs, setStandupPrefs } from "../lib/ai-standup";
1010import type { AuthEnv } from "../middleware/auth";
1111import { requireAuth } from "../middleware/auth";
2222 daysUntilPurge,
2323} from "../lib/account-deletion";
2424import { deleteCookie } from "hono/cookie";
25import { audit } from "../lib/notify";
2526
2627const settings = new Hono<AuthEnv>();
2728
637638settings.get("/settings", async (c) => {
638639 const user = c.get("user")!;
639640 const success = c.req.query("success");
641 // Check if the user has a GitHub OAuth link (subject starts with "github:")
642 const githubLink = await db
643 .select({ id: ssoUserLinks.id, subject: ssoUserLinks.subject })
644 .from(ssoUserLinks)
645 .where(eq(ssoUserLinks.userId, user.id))
646 .then((rows) => rows.find((r) => r.subject.startsWith("github:")));
647 const hasGithubLinked = !!githubLink;
640648 const standupPrefs = (await getStandupPrefs(user.id)) || {
641649 dailyEnabled: false,
642650 weeklyEnabled: false,
686694 <Banner kind="success" text={decodeURIComponent(success)} />
687695 )}
688696
697 {/* ─── GitHub account link ─── */}
698 {hasGithubLinked && (
699 <section class="settings-section">
700 <div class="settings-section-head">
701 <div class="settings-section-eyebrow">Connected account</div>
702 <h2 class="settings-section-title">GitHub</h2>
703 <p class="settings-section-desc">
704 Your Gluecron account is linked to a GitHub account.
705 Disconnecting removes the link but does not delete your
706 Gluecron account or any of your repositories.
707 </p>
708 </div>
709 <div class="settings-section-body">
710 <div
711 style={
712 "display:flex;align-items:center;gap:12px;padding:12px 14px;" +
713 "background:rgba(52,211,153,0.06);border:1px solid rgba(52,211,153,0.22);" +
714 "border-radius:10px;"
715 }
716 >
717 <span
718 style={
719 "display:inline-flex;align-items:center;justify-content:center;" +
720 "width:32px;height:32px;border-radius:8px;" +
721 "background:rgba(52,211,153,0.14);color:#6ee7b7;font-weight:700;font-size:15px;"
722 }
723 aria-hidden="true"
724 >
725
726 </span>
727 <span style="font-size:14px;color:var(--text)">
728 GitHub account connected
729 </span>
730 </div>
731 </div>
732 <div class="settings-section-foot">
733 <form method="post" action="/settings/github/unlink">
734 <button type="submit" class="btn btn-danger btn-sm">
735 Disconnect GitHub
736 </button>
737 </form>
738 </div>
739 </section>
740 )}
741
689742 {/* ─── Profile ─── */}
690743 <section class="settings-section">
691744 <div class="settings-section-head">
16161669 const success = c.req.query("success");
16171670
16181671 // Count how many event toggles are enabled per channel (just for the pill).
1672 // Email: mention, assign, gate_fail, digest_weekly, pending_comment (5 total)
16191673 const emailOnCount =
16201674 (user.notifyEmailOnMention ? 1 : 0) +
16211675 (user.notifyEmailOnAssign ? 1 : 0) +
16221676 (user.notifyEmailOnGateFail ? 1 : 0) +
1623 (user.notifyEmailDigestWeekly ? 1 : 0);
1677 (user.notifyEmailDigestWeekly ? 1 : 0) +
1678 (user.notifyEmailOnPendingComment ? 1 : 0);
1679 // Push: mention, assign, review_request, deploy_failed (4 total)
16241680 const pushOnCount =
16251681 (user.notifyPushOnMention ? 1 : 0) +
16261682 (user.notifyPushOnAssign ? 1 : 0) +
16811737 </div>
16821738 <div class="notifset-channel-text">
16831739 <h3 class="notifset-channel-name">Email</h3>
1684 <p class="notifset-channel-meta">{emailOnCount} of 4 events enabled</p>
1740 <p class="notifset-channel-meta">{emailOnCount} of 5 events enabled</p>
16851741 </div>
16861742 <span class={"notifset-channel-pill " + (emailOnCount > 0 ? "is-on" : "")}>
16871743 <span class="dot" aria-hidden="true" />
17031759 </div>
17041760 </div>
17051761
1706 {/* ─── Event rules form ─── */}
1762 {/* ─── Event rules form — 4 categories ─── */}
17071763 <form method="post" action="/settings/notifications">
1764
1765 {/* ── Category 1: AI activity ── */}
17081766 <section class="notifset-section">
17091767 <header class="notifset-section-head">
17101768 <h3 class="notifset-section-title">
17111769 <span class="notifset-section-icon" aria-hidden="true">
1712 <NotifsetMailIcon />
1770 <NotifsetInappIcon />
17131771 </span>
1714 Email rules
1772 AI activity
17151773 </h3>
17161774 <p class="notifset-section-sub">
1717 Pick which events email you. In-app notifications continue
1718 regardless of what you toggle here.
1775 Be notified when Claude acts on your behalf — reviews, spec
1776 builds, CI self-heals, and auto-merges. In-app always fires;
1777 email and push are opt-in.
17191778 </p>
17201779 </header>
17211780 <div class="notifset-section-body">
17221781 <label class="notifset-rule">
17231782 <input
17241783 type="checkbox"
1725 name="notify_email_on_mention"
1784 name="notify_email_digest_weekly"
17261785 value="1"
1727 checked={user.notifyEmailOnMention}
1728 aria-label="Someone @mentions me or requests a review"
1786 checked={user.notifyEmailDigestWeekly}
1787 aria-label="Weekly AI activity digest"
17291788 />
17301789 <span class="notifset-rule-text">
1731 Someone <code>@mentions</code> me or requests a review
1790 Weekly AI activity digest (email) &mdash;{" "}
1791 <a href="/settings/digest/preview" style="color:var(--accent)">preview</a>
17321792 <span class="notifset-rule-hint">
1733 Direct pings on issues, PRs, and code comments.
1793 A Monday summary of AI reviews posted, specs built, CI heals,
1794 and auto-merges from the past week.
17341795 </span>
17351796 </span>
17361797 </label>
17371798 <label class="notifset-rule">
17381799 <input
17391800 type="checkbox"
1740 name="notify_email_on_assign"
1801 name="sleep_mode_enabled"
17411802 value="1"
1742 checked={user.notifyEmailOnAssign}
1743 aria-label="I am assigned to an issue or PR"
1803 checked={user.sleepModeEnabled}
1804 aria-label="Enable Sleep Mode overnight digest"
17441805 />
17451806 <span class="notifset-rule-text">
1746 I am assigned to an issue or PR
1807 Enable Sleep Mode overnight digest (email)
17471808 <span class="notifset-rule-hint">
1748 Email when someone hands you something to work on.
1809 Claude operates autonomously overnight and sends a daily
1810 report of what shipped — PRs auto-merged, issues built,
1811 AI reviews, security auto-fixes, gate repairs.{" "}
1812 <a href="/sleep-mode" style="color:var(--accent)">Learn more</a>.
17491813 </span>
17501814 </span>
17511815 </label>
1752 <label class="notifset-rule">
1816 <div class="notifset-hour">
1817 <label for="sleep_mode_digest_hour_utc">
1818 Morning digest hour (UTC 0–23):
1819 </label>
17531820 <input
1754 type="checkbox"
1755 name="notify_email_on_gate_fail"
1756 value="1"
1757 checked={user.notifyEmailOnGateFail}
1758 aria-label="A gate fails on one of my repositories"
1821 type="number"
1822 id="sleep_mode_digest_hour_utc"
1823 name="sleep_mode_digest_hour_utc"
1824 min={0}
1825 max={23}
1826 step={1}
1827 value={String(user.sleepModeDigestHourUtc)}
1828 aria-label="Sleep Mode digest UTC hour"
17591829 />
1760 <span class="notifset-rule-text">
1761 A gate fails on one of my repositories
1762 <span class="notifset-rule-hint">
1763 Security, test, or build gate alerts on your repos.
1764 </span>
1830 <a href="/settings/sleep-mode/preview">Preview digest now</a>
1831 </div>
1832 </div>
1833 </section>
1834
1835 {/* ── Category 2: CI/CD ── */}
1836 <section class="notifset-section">
1837 <header class="notifset-section-head">
1838 <h3 class="notifset-section-title">
1839 <span class="notifset-section-icon" aria-hidden="true">
1840 <NotifsetPushIcon />
17651841 </span>
1766 </label>
1842 CI/CD
1843 </h3>
1844 <p class="notifset-section-sub">
1845 Workflow runs, gate results, and deploy outcomes on repositories
1846 you own. Email and push are opt-in per channel.
1847 </p>
1848 </header>
1849 <div class="notifset-section-body">
17671850 <label class="notifset-rule">
17681851 <input
17691852 type="checkbox"
1770 name="notify_email_digest_weekly"
1853 name="notify_email_on_gate_fail"
17711854 value="1"
1772 checked={user.notifyEmailDigestWeekly}
1773 aria-label="Weekly digest email"
1855 checked={user.notifyEmailOnGateFail}
1856 aria-label="A gate fails on one of my repositories — email"
17741857 />
17751858 <span class="notifset-rule-text">
1776 Weekly digest &mdash; <a href="/settings/digest/preview" style="color:var(--accent)">preview</a>
1859 Gate / workflow fails (email)
17771860 <span class="notifset-rule-hint">
1778 A Monday summary of what shipped last week.
1861 Security, test, or build gate failures on your repos —
1862 triggered on push and PR.
17791863 </span>
17801864 </span>
17811865 </label>
17821866 <label class="notifset-rule">
17831867 <input
17841868 type="checkbox"
1785 name="notify_email_on_pending_comment"
1869 name="notify_push_on_deploy_failed"
17861870 value="1"
1787 checked={user.notifyEmailOnPendingComment}
1788 aria-label="Someone leaves a comment on a public repo you own and it needs your approval"
1871 checked={user.notifyPushOnDeployFailed}
1872 aria-label="A deploy fails — web push"
17891873 />
17901874 <span class="notifset-rule-text">
1791 Pending comment requests on my repos
1875 Deploy fails (web push)
17921876 <span class="notifset-rule-hint">
1793 A non-collaborator left a comment on a public repo you
1794 own. Comments stay hidden until you review them.
1877 Push notification when a deploy to production fails on
1878 one of your repositories.
17951879 </span>
17961880 </span>
17971881 </label>
17981882 </div>
17991883 </section>
18001884
1885 {/* ── Category 3: Code review ── */}
18011886 <section class="notifset-section">
18021887 <header class="notifset-section-head">
18031888 <h3 class="notifset-section-title">
18041889 <span class="notifset-section-icon" aria-hidden="true">
1805 <NotifsetPushIcon />
1890 <NotifsetMailIcon />
18061891 </span>
1807 Web push rules
1892 Code review
18081893 </h3>
18091894 <p class="notifset-section-sub">
1810 Install Gluecron as a PWA, then subscribe a device from{" "}
1811 <a href="/settings" style="color:var(--accent)">main settings</a>. These
1812 toggles control which event kinds trigger a push notification.
1895 PR comments, review requests, assignments, and merges.
18131896 </p>
18141897 </header>
18151898 <div class="notifset-section-body">
18161899 <label class="notifset-rule">
18171900 <input
18181901 type="checkbox"
1819 name="notify_push_on_mention"
1902 name="notify_push_on_review_request"
18201903 value="1"
1821 checked={user.notifyPushOnMention}
1822 aria-label="Someone @mentions me"
1904 checked={user.notifyPushOnReviewRequest}
1905 aria-label="Someone requests a review from me — web push"
18231906 />
18241907 <span class="notifset-rule-text">
1825 Someone <code>@mentions</code> me
1908 Review requested (web push)
1909 <span class="notifset-rule-hint">
1910 Push notification when you are added as a reviewer to a PR.
1911 </span>
18261912 </span>
18271913 </label>
18281914 <label class="notifset-rule">
18291915 <input
18301916 type="checkbox"
1831 name="notify_push_on_assign"
1917 name="notify_email_on_assign"
18321918 value="1"
1833 checked={user.notifyPushOnAssign}
1834 aria-label="I am assigned to an issue or PR"
1919 checked={user.notifyEmailOnAssign}
1920 aria-label="I am assigned to an issue or PR — email"
18351921 />
1836 <span class="notifset-rule-text">I am assigned to an issue or PR</span>
1922 <span class="notifset-rule-text">
1923 Assigned to an issue or PR (email)
1924 <span class="notifset-rule-hint">
1925 Email when someone hands you a PR or issue to work on.
1926 </span>
1927 </span>
18371928 </label>
18381929 <label class="notifset-rule">
18391930 <input
18401931 type="checkbox"
1841 name="notify_push_on_review_request"
1932 name="notify_push_on_assign"
18421933 value="1"
1843 checked={user.notifyPushOnReviewRequest}
1844 aria-label="Someone requests a review from me"
1934 checked={user.notifyPushOnAssign}
1935 aria-label="I am assigned to an issue or PR — web push"
18451936 />
1846 <span class="notifset-rule-text">Someone requests a review from me</span>
1937 <span class="notifset-rule-text">
1938 Assigned to an issue or PR (web push)
1939 <span class="notifset-rule-hint">
1940 Push notification when you are assigned to a PR or issue.
1941 </span>
1942 </span>
18471943 </label>
18481944 <label class="notifset-rule">
18491945 <input
18501946 type="checkbox"
1851 name="notify_push_on_deploy_failed"
1947 name="notify_email_on_pending_comment"
18521948 value="1"
1853 checked={user.notifyPushOnDeployFailed}
1854 aria-label="A deploy fails"
1949 checked={user.notifyEmailOnPendingComment}
1950 aria-label="Pending comment approval needed on my repos"
18551951 />
18561952 <span class="notifset-rule-text">
1857 A deploy fails on one of my repositories
1953 Pending comment needs approval (email)
1954 <span class="notifset-rule-hint">
1955 A non-collaborator left a comment on a public repo you
1956 own. Comments stay hidden until you review them.
1957 </span>
18581958 </span>
18591959 </label>
18601960 </div>
18611961 </section>
18621962
1963 {/* ── Category 4: Mentions ── */}
18631964 <section class="notifset-section">
18641965 <header class="notifset-section-head">
18651966 <h3 class="notifset-section-title">
18661967 <span class="notifset-section-icon" aria-hidden="true">
1867 <NotifsetInappIcon />
1968 <NotifsetBellIcon />
18681969 </span>
1869 Sleep Mode
1970 Mentions
18701971 </h3>
18711972 <p class="notifset-section-sub">
1872 Toggle Sleep Mode and pick when your morning digest lands.
1873 <a href="/sleep-mode" style="color:var(--accent);margin-left:4px">Learn more</a>.
1973 Direct <code style="font-family:var(--font-mono);font-size:12px;background:rgba(255,255,255,0.04);border:1px solid var(--border);padding:1px 6px;border-radius:4px">@mentions</code> in
1974 issues and PR comments. In-app always fires; email and push are opt-in.
18741975 </p>
18751976 </header>
18761977 <div class="notifset-section-body">
18771978 <label class="notifset-rule">
18781979 <input
18791980 type="checkbox"
1880 name="sleep_mode_enabled"
1981 name="notify_email_on_mention"
18811982 value="1"
1882 checked={user.sleepModeEnabled}
1883 aria-label="Enable Sleep Mode"
1983 checked={user.notifyEmailOnMention}
1984 aria-label="Someone @mentions me or requests a review — email"
18841985 />
18851986 <span class="notifset-rule-text">
1886 Enable Sleep Mode (daily &ldquo;overnight&rdquo; digest)
1987 <code>@mention</code> in issue or PR comment (email)
18871988 <span class="notifset-rule-hint">
1888 Claude operates autonomously between digests and reports
1889 back on the schedule you pick.
1989 Email when someone tags you directly in a thread or
1990 requests a review.
18901991 </span>
18911992 </span>
18921993 </label>
1893 <div class="notifset-hour">
1894 <label for="sleep_mode_digest_hour_utc">
1895 Morning digest hour (UTC 0–23):
1896 </label>
1994 <label class="notifset-rule">
18971995 <input
1898 type="number"
1899 id="sleep_mode_digest_hour_utc"
1900 name="sleep_mode_digest_hour_utc"
1901 min={0}
1902 max={23}
1903 step={1}
1904 value={String(user.sleepModeDigestHourUtc)}
1905 aria-label="Sleep Mode digest UTC hour"
1996 type="checkbox"
1997 name="notify_push_on_mention"
1998 value="1"
1999 checked={user.notifyPushOnMention}
2000 aria-label="Someone @mentions me — web push"
19062001 />
1907 <a href="/settings/sleep-mode/preview">Preview digest now</a>
1908 </div>
2002 <span class="notifset-rule-text">
2003 <code>@mention</code> in issue or PR comment (web push)
2004 <span class="notifset-rule-hint">
2005 Push notification when someone tags you in a thread.
2006 </span>
2007 </span>
2008 </label>
19092009 </div>
19102010 <div class="notifset-section-foot">
19112011 <span class="notifset-foot-hint">
23772477 return c.json({ deleted: true });
23782478});
23792479
2480// ─── /settings/github/unlink — disconnect GitHub OAuth ───────────────────────
2481// Alias for /settings/sso/unlink scoped to just the "github:" prefixed links.
2482// Deletes only the github: sso_user_links row(s) for the current user, leaving
2483// any enterprise-OIDC or google: links intact.
2484settings.post("/settings/github/unlink", requireAuth, async (c) => {
2485 const user = c.get("user")!;
2486 // Fetch all SSO links for this user, then filter for GitHub ones in JS
2487 // (avoids a raw SQL LIKE in the ORM layer).
2488 const allLinks = await db
2489 .select({ id: ssoUserLinks.id, subject: ssoUserLinks.subject })
2490 .from(ssoUserLinks)
2491 .where(eq(ssoUserLinks.userId, user.id));
2492 const githubLinks = allLinks.filter((l) => l.subject.startsWith("github:"));
2493 if (githubLinks.length === 0) {
2494 return c.redirect(
2495 "/settings?error=" + encodeURIComponent("No GitHub account is linked.")
2496 );
2497 }
2498 for (const link of githubLinks) {
2499 await db.delete(ssoUserLinks).where(eq(ssoUserLinks.id, link.id));
2500 }
2501 await audit({
2502 userId: user.id,
2503 action: "auth.github.unlink",
2504 metadata: { removedLinks: githubLinks.length },
2505 });
2506 return c.redirect(
2507 "/settings?success=" + encodeURIComponent("GitHub account disconnected.")
2508 );
2509});
2510
23802511export default settings;
23812512