Commit0ca5660unknown_key
Merge pull request #124 from ccantynz-alt/claude/serene-edison-rj87we
Merge pull request #124 from ccantynz-alt/claude/serene-edison-rj87we Claude/serene edison rj87we
5 files changed+120−3050ca56604010691e14e93f9f0ba3472e11ee9260c
5 changed files+120−305
Modifiedsrc/db/schema.ts+29−0View fileUnifiedSplit
@@ -4672,3 +4672,32 @@ export const testGapCache = pgTable(
46724672);
46734673
46744674export type TestGapCache = typeof testGapCache.$inferSelect;
4675
4676// ---------------------------------------------------------------------------
4677// Migration 0106 — Per-repo automation settings
4678// Controls mode (off/suggest/auto) for each push/PR/issue automation.
4679// No row = AUTOMATION_DEFAULTS (pre-0106 behavior preserved exactly).
4680// ---------------------------------------------------------------------------
4681export const repoAutomationSettings = pgTable(
4682 "repo_automation_settings",
4683 {
4684 id: uuid("id").primaryKey().defaultRandom(),
4685 repositoryId: uuid("repository_id")
4686 .notNull()
4687 .unique()
4688 .references(() => repositories.id, { onDelete: "cascade" }),
4689 aiReviewMode: text("ai_review_mode").notNull().default("suggest"),
4690 prTriageMode: text("pr_triage_mode").notNull().default("suggest"),
4691 issueTriageMode: text("issue_triage_mode").notNull().default("suggest"),
4692 autoMergeMode: text("auto_merge_mode").notNull().default("auto"),
4693 ciAutofixMode: text("ci_autofix_mode").notNull().default("suggest"),
4694 createdAt: timestamp("created_at").defaultNow(),
4695 updatedAt: timestamp("updated_at").defaultNow(),
4696 },
4697 (table) => [
4698 index("idx_repo_automation_settings_repo").on(table.repositoryId),
4699 ]
4700);
4701
4702export type RepoAutomationSettings = typeof repoAutomationSettings.$inferSelect;
4703export type NewRepoAutomationSettings = typeof repoAutomationSettings.$inferInsert;
Modifiedsrc/routes/admin.tsx+76−170View fileUnifiedSplit
@@ -23,7 +23,7 @@
2323import { Hono } from "hono";
2424import { and, desc, eq, gte, ilike, or, sql } from "drizzle-orm";
2525import { db } from "../db";
26import { aiCostEvents, auditLog, issueComments, prComments, repositories, users } from "../db/schema";
26import { aiCostEvents, auditLog, issueComments, issues, prComments, pullRequests, repositories, users } from "../db/schema";
2727import { Layout } from "../views/layout";
2828import { softAuth } from "../middleware/auth";
2929import type { AuthEnv } from "../middleware/auth";
@@ -82,21 +82,6 @@ const adminStyles = `
8282 pointer-events: none;
8383 z-index: 0;
8484 }
85 .admin-hero-orb {
86 position: absolute;
87 inset: 0;
88 background: radial-gradient(circle, rgba(91,110,232,0.20), rgba(95,143,160,0.10) 45%, transparent 70%);
89 filter: blur(80px);
90 opacity: 0.7;
91 animation: adminHeroOrb 14s ease-in-out infinite;
92 }
93 adminHeroOrb {
94 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
95 50% { transform: scale(1.1) translate(-10px, 8px); opacity: 0.85; }
96 }
97 (prefers-reduced-motion: reduce) {
98 .admin-hero-orb { animation: none; }
99 }
10085 .admin-hero-inner {
10186 position: relative;
10287 z-index: 1;
@@ -137,11 +122,10 @@ const adminStyles = `
137122 }
138123 .admin-hero-title .gradient-text,
139124 .admin-hero-title-grad {
140 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
141 -webkit-background-clip: text;
142 background-clip: text;
143 -webkit-text-fill-color: transparent;
144 color: transparent;
125 color: var(--text-strong);
126 background: none;
127 -webkit-background-clip: unset;
128 background-clip: unset;
145129 }
146130 .admin-hero-sub {
147131 font-size: 15px;
@@ -804,24 +788,6 @@ const admUsersStyles = `
804788 opacity: 0.7;
805789 pointer-events: none;
806790 }
807 .adm-users-hero-orb {
808 position: absolute;
809 inset: -20% -10% auto auto;
810 width: 380px; height: 380px;
811 background: radial-gradient(circle, rgba(91,110,232,0.20), rgba(95,143,160,0.10) 45%, transparent 70%);
812 filter: blur(80px);
813 opacity: 0.7;
814 pointer-events: none;
815 z-index: 0;
816 animation: admUsersOrb 14s ease-in-out infinite;
817 }
818 admUsersOrb {
819 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
820 50% { transform: scale(1.1) translate(-10px, 8px); opacity: 0.85; }
821 }
822 (prefers-reduced-motion: reduce) {
823 .adm-users-hero-orb { animation: none; }
824 }
825791 .adm-users-hero-inner {
826792 position: relative;
827793 z-index: 1;
@@ -861,11 +827,10 @@ const admUsersStyles = `
861827 color: var(--text-strong);
862828 }
863829 .adm-users-title-grad {
864 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
865 -webkit-background-clip: text;
866 background-clip: text;
867 -webkit-text-fill-color: transparent;
868 color: transparent;
830 color: var(--text-strong);
831 background: none;
832 -webkit-background-clip: unset;
833 background-clip: unset;
869834 }
870835 .adm-users-sub {
871836 font-size: 15px;
@@ -1184,24 +1149,6 @@ const admReposStyles = `
11841149 opacity: 0.7;
11851150 pointer-events: none;
11861151 }
1187 .adm-repos-hero-orb {
1188 position: absolute;
1189 inset: -20% -10% auto auto;
1190 width: 380px; height: 380px;
1191 background: radial-gradient(circle, rgba(91,110,232,0.20), rgba(95,143,160,0.10) 45%, transparent 70%);
1192 filter: blur(80px);
1193 opacity: 0.7;
1194 pointer-events: none;
1195 z-index: 0;
1196 animation: admReposOrb 14s ease-in-out infinite;
1197 }
1198 admReposOrb {
1199 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
1200 50% { transform: scale(1.1) translate(-10px, 8px); opacity: 0.85; }
1201 }
1202 (prefers-reduced-motion: reduce) {
1203 .adm-repos-hero-orb { animation: none; }
1204 }
12051152 .adm-repos-hero-inner {
12061153 position: relative;
12071154 z-index: 1;
@@ -1241,11 +1188,10 @@ const admReposStyles = `
12411188 color: var(--text-strong);
12421189 }
12431190 .adm-repos-title-grad {
1244 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
1245 -webkit-background-clip: text;
1246 background-clip: text;
1247 -webkit-text-fill-color: transparent;
1248 color: transparent;
1191 color: var(--text-strong);
1192 background: none;
1193 -webkit-background-clip: unset;
1194 background-clip: unset;
12491195 }
12501196 .adm-repos-sub {
12511197 font-size: 15px;
@@ -1488,24 +1434,6 @@ const admFlagsStyles = `
14881434 opacity: 0.7;
14891435 pointer-events: none;
14901436 }
1491 .adm-flags-hero-orb {
1492 position: absolute;
1493 inset: -20% -10% auto auto;
1494 width: 380px; height: 380px;
1495 background: radial-gradient(circle, rgba(91,110,232,0.20), rgba(95,143,160,0.10) 45%, transparent 70%);
1496 filter: blur(80px);
1497 opacity: 0.7;
1498 pointer-events: none;
1499 z-index: 0;
1500 animation: admFlagsOrb 14s ease-in-out infinite;
1501 }
1502 admFlagsOrb {
1503 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
1504 50% { transform: scale(1.1) translate(-10px, 8px); opacity: 0.85; }
1505 }
1506 (prefers-reduced-motion: reduce) {
1507 .adm-flags-hero-orb { animation: none; }
1508 }
15091437 .adm-flags-hero-inner {
15101438 position: relative;
15111439 z-index: 1;
@@ -1545,11 +1473,10 @@ const admFlagsStyles = `
15451473 color: var(--text-strong);
15461474 }
15471475 .adm-flags-title-grad {
1548 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
1549 -webkit-background-clip: text;
1550 background-clip: text;
1551 -webkit-text-fill-color: transparent;
1552 color: transparent;
1476 color: var(--text-strong);
1477 background: none;
1478 -webkit-background-clip: unset;
1479 background-clip: unset;
15531480 }
15541481 .adm-flags-sub {
15551482 font-size: 15px;
@@ -1722,24 +1649,6 @@ const admDigestsStyles = `
17221649 opacity: 0.7;
17231650 pointer-events: none;
17241651 }
1725 .adm-digests-hero-orb {
1726 position: absolute;
1727 inset: -20% -10% auto auto;
1728 width: 380px; height: 380px;
1729 background: radial-gradient(circle, rgba(91,110,232,0.20), rgba(95,143,160,0.10) 45%, transparent 70%);
1730 filter: blur(80px);
1731 opacity: 0.7;
1732 pointer-events: none;
1733 z-index: 0;
1734 animation: admDigestsOrb 14s ease-in-out infinite;
1735 }
1736 admDigestsOrb {
1737 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
1738 50% { transform: scale(1.1) translate(-10px, 8px); opacity: 0.85; }
1739 }
1740 (prefers-reduced-motion: reduce) {
1741 .adm-digests-hero-orb { animation: none; }
1742 }
17431652 .adm-digests-hero-inner {
17441653 position: relative;
17451654 z-index: 1;
@@ -1779,11 +1688,10 @@ const admDigestsStyles = `
17791688 color: var(--text-strong);
17801689 }
17811690 .adm-digests-title-grad {
1782 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
1783 -webkit-background-clip: text;
1784 background-clip: text;
1785 -webkit-text-fill-color: transparent;
1786 color: transparent;
1691 color: var(--text-strong);
1692 background: none;
1693 -webkit-background-clip: unset;
1694 background-clip: unset;
17871695 }
17881696 .adm-digests-sub {
17891697 font-size: 15px;
@@ -2092,24 +2000,6 @@ const admAutopilotStyles = `
20922000 opacity: 0.7;
20932001 pointer-events: none;
20942002 }
2095 .adm-autopilot-hero-orb {
2096 position: absolute;
2097 inset: -20% -10% auto auto;
2098 width: 380px; height: 380px;
2099 background: radial-gradient(circle, rgba(91,110,232,0.20), rgba(95,143,160,0.10) 45%, transparent 70%);
2100 filter: blur(80px);
2101 opacity: 0.7;
2102 pointer-events: none;
2103 z-index: 0;
2104 animation: admAutopilotOrb 14s ease-in-out infinite;
2105 }
2106 admAutopilotOrb {
2107 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
2108 50% { transform: scale(1.1) translate(-10px, 8px); opacity: 0.85; }
2109 }
2110 (prefers-reduced-motion: reduce) {
2111 .adm-autopilot-hero-orb { animation: none; }
2112 }
21132003 .adm-autopilot-hero-inner {
21142004 position: relative;
21152005 z-index: 1;
@@ -2149,11 +2039,10 @@ const admAutopilotStyles = `
21492039 color: var(--text-strong);
21502040 }
21512041 .adm-autopilot-title-grad {
2152 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
2153 -webkit-background-clip: text;
2154 background-clip: text;
2155 -webkit-text-fill-color: transparent;
2156 color: transparent;
2042 color: var(--text-strong);
2043 background: none;
2044 -webkit-background-clip: unset;
2045 background-clip: unset;
21572046 }
21582047 .adm-autopilot-sub {
21592048 font-size: 15px;
@@ -2525,22 +2414,6 @@ const admAnalyticsStyles = `
25252414 opacity: 0.7;
25262415 pointer-events: none;
25272416 }
2528 .adm-analytics-hero-orb {
2529 position: absolute;
2530 inset: -20% -10% auto auto;
2531 width: 380px; height: 380px;
2532 background: radial-gradient(circle, rgba(91,110,232,0.20), rgba(95,143,160,0.10) 45%, transparent 70%);
2533 filter: blur(80px);
2534 opacity: 0.7;
2535 pointer-events: none;
2536 z-index: 0;
2537 animation: admAnalyticsOrb 14s ease-in-out infinite;
2538 }
2539 admAnalyticsOrb {
2540 0%, 100% { transform: scale(1) translate(0, 0); opacity: 0.6; }
2541 50% { transform: scale(1.1) translate(-10px, 8px); opacity: 0.85; }
2542 }
2543 (prefers-reduced-motion: reduce) { .adm-analytics-hero-orb { animation: none; } }
25442417 .adm-analytics-hero-inner {
25452418 position: relative; z-index: 1;
25462419 display: flex; align-items: flex-end; justify-content: space-between;
@@ -2563,9 +2436,10 @@ const admAnalyticsStyles = `
25632436 margin: 0 0 var(--space-2); color: var(--text-strong);
25642437 }
25652438 .adm-analytics-title-grad {
2566 background-image: linear-gradient(135deg, #5b6ee8 0%, #5b6ee8 50%, #5f8fa0 100%);
2567 -webkit-background-clip: text; background-clip: text;
2568 -webkit-text-fill-color: transparent; color: transparent;
2439 color: var(--text-strong);
2440 background: none;
2441 -webkit-background-clip: unset;
2442 background-clip: unset;
25692443 }
25702444 .adm-analytics-sub { font-size: 14px; color: var(--text-muted); margin: 0; line-height: 1.5; }
25712445 .adm-analytics-back {
@@ -2767,6 +2641,20 @@ const Icons = {
27672641 <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
27682642 </svg>
27692643 ),
2644 gitMerge: (
2645 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
2646 <circle cx="18" cy="18" r="3" />
2647 <circle cx="6" cy="6" r="3" />
2648 <path d="M6 21V9a9 9 0 0 0 9 9" />
2649 </svg>
2650 ),
2651 issue: (
2652 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
2653 <circle cx="12" cy="12" r="10" />
2654 <line x1="12" y1="8" x2="12" y2="12" />
2655 <line x1="12" y1="16" x2="12.01" y2="16" />
2656 </svg>
2657 ),
27702658};
27712659
27722660/** First-letter avatar helper. */
@@ -2799,9 +2687,19 @@ admin.get("/admin", async (c) => {
27992687
28002688 const since24h = new Date(Date.now() - 24 * 60 * 60 * 1000);
28012689
2802 const [[uc], [rc], recent, admins] = await Promise.all([
2690 const [[uc], [rc], [prc], [ic], recent, admins] = await Promise.all([
28032691 db.select({ n: sql<number>`count(*)::int` }).from(users),
28042692 db.select({ n: sql<number>`count(*)::int` }).from(repositories),
2693 db
2694 .select({ n: sql<number>`count(*)::int` })
2695 .from(pullRequests)
2696 .where(eq(pullRequests.state, "open"))
2697 .catch(() => [{ n: 0 }] as { n: number }[]),
2698 db
2699 .select({ n: sql<number>`count(*)::int` })
2700 .from(issues)
2701 .where(eq(issues.state, "open"))
2702 .catch(() => [{ n: 0 }] as { n: number }[]),
28052703 db
28062704 .select({
28072705 id: users.id,
@@ -2858,15 +2756,15 @@ admin.get("/admin", async (c) => {
28582756
28592757 const userCount = Number(uc?.n || 0);
28602758 const repoCount = Number(rc?.n || 0);
2759 const openPrCount = Number((prc as { n: number }[])[0]?.n || 0);
2760 const openIssueCount = Number((ic as { n: number }[])[0]?.n || 0);
28612761 const adminCount = admins.length;
28622762
28632763 return c.html(
28642764 <Layout title="Admin — Gluecron" user={user}>
28652765 <div class="admin-wrap">
28662766 <section class="admin-hero">
2867 <div class="admin-hero-bg" aria-hidden="true">
2868 <div class="admin-hero-orb" />
2869 </div>
2767 <div class="admin-hero-bg" aria-hidden="true" />
28702768 <div class="admin-hero-inner">
28712769 <div class="admin-hero-eyebrow">
28722770 <span class="admin-shield" aria-hidden="true">{Icons.shield}</span>
@@ -2916,6 +2814,22 @@ admin.get("/admin", async (c) => {
29162814 <div class="admin-stat-value">{adminCount}</div>
29172815 <div class="admin-stat-hint">Site admins</div>
29182816 </div>
2817 <div class="admin-stat">
2818 <div class="admin-stat-head">
2819 <span class="admin-stat-label">Open PRs</span>
2820 <span class="admin-stat-icon">{Icons.gitMerge}</span>
2821 </div>
2822 <div class="admin-stat-value">{openPrCount}</div>
2823 <div class="admin-stat-hint">Platform-wide</div>
2824 </div>
2825 <div class="admin-stat">
2826 <div class="admin-stat-head">
2827 <span class="admin-stat-label">Open Issues</span>
2828 <span class="admin-stat-icon">{Icons.issue}</span>
2829 </div>
2830 <div class="admin-stat-value">{openIssueCount}</div>
2831 <div class="admin-stat-hint">Platform-wide</div>
2832 </div>
29192833 </div>
29202834
29212835 <div class="admin-actions">
@@ -2935,9 +2849,9 @@ admin.get("/admin", async (c) => {
29352849 <span class="admin-action-icon">{Icons.ops}</span>
29362850 Deploys
29372851 </a>
2938 <a href="/admin/diagnose" class="admin-action">
2852 <a href="/admin/env-health" class="admin-action">
29392853 <span class="admin-action-icon">{Icons.pulse}</span>
2940 Diagnose
2854 Env health
29412855 </a>
29422856 <a href="/admin/self-host" class="admin-action">
29432857 <span class="admin-action-icon">{Icons.ops}</span>
@@ -2945,7 +2859,7 @@ admin.get("/admin", async (c) => {
29452859 </a>
29462860 <a href="/admin/status" class="admin-action">
29472861 <span class="admin-action-icon">{Icons.pulse}</span>
2948 Live activity stream
2862 Platform monitor
29492863 </a>
29502864 <a href="/admin/users" class="admin-action">
29512865 <span class="admin-action-icon">{Icons.users}</span>
@@ -3174,7 +3088,6 @@ admin.get("/admin/users", async (c) => {
31743088 <Layout title="Admin — Users" user={user}>
31753089 <div class="adm-users-wrap">
31763090 <section class="adm-users-hero">
3177 <div class="adm-users-hero-orb" aria-hidden="true" />
31783091 <div class="adm-users-hero-inner">
31793092 <div class="adm-users-hero-text">
31803093 <div class="adm-users-eyebrow">
@@ -3352,7 +3265,6 @@ admin.get("/admin/repos", async (c) => {
33523265 <Layout title="Admin — Repos" user={user}>
33533266 <div class="adm-repos-wrap">
33543267 <section class="adm-repos-hero">
3355 <div class="adm-repos-hero-orb" aria-hidden="true" />
33563268 <div class="adm-repos-hero-inner">
33573269 <div class="adm-repos-hero-text">
33583270 <div class="adm-repos-eyebrow">
@@ -3494,7 +3406,6 @@ admin.get("/admin/flags", async (c) => {
34943406 <Layout title="Admin — Flags" user={user}>
34953407 <div class="adm-flags-wrap">
34963408 <section class="adm-flags-hero">
3497 <div class="adm-flags-hero-orb" aria-hidden="true" />
34983409 <div class="adm-flags-hero-inner">
34993410 <div class="adm-flags-hero-text">
35003411 <div class="adm-flags-eyebrow">
@@ -3605,7 +3516,6 @@ admin.get("/admin/digests", async (c) => {
36053516 <Layout title="Admin — Digests" user={user}>
36063517 <div class="adm-digests-wrap">
36073518 <section class="adm-digests-hero">
3608 <div class="adm-digests-hero-orb" aria-hidden="true" />
36093519 <div class="adm-digests-hero-inner">
36103520 <div class="adm-digests-hero-text">
36113521 <div class="adm-digests-eyebrow">
@@ -3816,7 +3726,6 @@ admin.get("/admin/autopilot", async (c) => {
38163726 <Layout title="Autopilot — admin" user={user}>
38173727 <div class="adm-autopilot-wrap">
38183728 <section class="adm-autopilot-hero">
3819 <div class="adm-autopilot-hero-orb" aria-hidden="true" />
38203729 <div class="adm-autopilot-hero-inner">
38213730 <div class="adm-autopilot-hero-text">
38223731 <div class="adm-autopilot-eyebrow">
@@ -4103,7 +4012,6 @@ admin.get("/admin/ai-costs", async (c) => {
41034012 <Layout title="Admin — AI Costs" user={user}>
41044013 <div class="adm-analytics-wrap">
41054014 <section class="adm-analytics-hero">
4106 <div class="adm-analytics-hero-orb" aria-hidden="true" />
41074015 <div class="adm-analytics-hero-inner">
41084016 <div class="adm-analytics-hero-text">
41094017 <div class="adm-analytics-eyebrow">
@@ -4306,7 +4214,6 @@ admin.get("/admin/autopilot/health", async (c) => {
43064214 <Layout title="Autopilot Health — admin" user={user}>
43074215 <div class="adm-analytics-wrap">
43084216 <section class="adm-analytics-hero">
4309 <div class="adm-analytics-hero-orb" aria-hidden="true" />
43104217 <div class="adm-analytics-hero-inner">
43114218 <div class="adm-analytics-hero-text">
43124219 <div class="adm-analytics-eyebrow">
@@ -4479,7 +4386,6 @@ admin.get("/admin/growth", async (c) => {
44794386 <Layout title="Admin — User Growth" user={user}>
44804387 <div class="adm-analytics-wrap">
44814388 <section class="adm-analytics-hero">
4482 <div class="adm-analytics-hero-orb" aria-hidden="true" />
44834389 <div class="adm-analytics-hero-inner">
44844390 <div class="adm-analytics-hero-text">
44854391 <div class="adm-analytics-eyebrow">
Modifiedsrc/routes/marketing.tsx+6−26View fileUnifiedSplit
@@ -34,7 +34,6 @@ const PricingPage: FC = () => (
3434 <style dangerouslySetInnerHTML={{ __html: pricingCss }} />
3535 <div class="mkt-page-pricing mkt-root">
3636 <header class="mkt-hero">
37 <div class="mkt-hero-orb" aria-hidden="true" />
3837 <div class="mkt-hero-inner">
3938 <div class="mkt-eyebrow">
4039 <span class="mkt-eyebrow-pill" aria-hidden="true">
@@ -135,7 +134,7 @@ const PricingPage: FC = () => (
135134 self-hosters of any size.
136135 </p>
137136 </div>
138 <div class="mkt-selfhost-card surface-glow">
137 <div class="mkt-selfhost-card">
139138 <div class="mkt-selfhost-grid">
140139 <div class="mkt-selfhost-cell">
141140 <div class="mkt-selfhost-num">$0</div>
@@ -272,7 +271,6 @@ const FeaturesPage: FC = () => (
272271 <style dangerouslySetInnerHTML={{ __html: featuresCss }} />
273272 <div class="mkt-page-features mkt-root">
274273 <header class="mkt-hero">
275 <div class="mkt-hero-orb" aria-hidden="true" />
276274 <div class="mkt-hero-inner">
277275 <div class="mkt-eyebrow">
278276 <span class="mkt-eyebrow-pill" aria-hidden="true">
@@ -575,7 +573,6 @@ const AboutPage: FC = () => (
575573 <style dangerouslySetInnerHTML={{ __html: aboutCss }} />
576574 <div class="mkt-page-about mkt-root">
577575 <header class="mkt-hero">
578 <div class="mkt-hero-orb" aria-hidden="true" />
579576 <div class="mkt-hero-inner">
580577 <div class="mkt-eyebrow">
581578 <span class="mkt-eyebrow-pill" aria-hidden="true">
@@ -746,7 +743,7 @@ const CtaBlock: FC = () => (
746743 <div class="eyebrow">Get started</div>
747744 <h2 class="mkt-cta-title">
748745 Stop maintaining the platform.<br />
749 <span class="gradient-text">Start shipping the product.</span>
746 Start shipping the product.
750747 </h2>
751748 <div class="mkt-cta-buttons">
752749 <a href="/register" class="btn btn-primary btn-xl">
@@ -802,18 +799,6 @@ const sharedMktCss = `
802799 pointer-events: none;
803800 z-index: 2;
804801 }
805 .mkt-page-pricing .mkt-hero-orb,
806 .mkt-page-features .mkt-hero-orb,
807 .mkt-page-about .mkt-hero-orb {
808 position: absolute;
809 inset: -28% -10% auto auto;
810 width: 520px; height: 520px;
811 background: radial-gradient(circle, rgba(91,110,232,0.08), transparent 70%);
812 filter: blur(80px);
813 opacity: 0.6;
814 pointer-events: none;
815 z-index: 0;
816 }
817802 .mkt-page-pricing .mkt-hero-inner,
818803 .mkt-page-features .mkt-hero-inner,
819804 .mkt-page-about .mkt-hero-inner { position: relative; z-index: 1; }
@@ -847,11 +832,10 @@ const sharedMktCss = `
847832 .mkt-page-pricing .mkt-grad,
848833 .mkt-page-features .mkt-grad,
849834 .mkt-page-about .mkt-grad {
850 background-image: linear-gradient(135deg, var(--accent-hover) 0%, var(--accent) 100%);
851 -webkit-background-clip: text;
852 background-clip: text;
853 -webkit-text-fill-color: transparent;
854 color: transparent;
835 color: var(--accent);
836 background: none;
837 -webkit-background-clip: unset;
838 background-clip: unset;
855839 }
856840 .mkt-page-pricing .mkt-hero-title,
857841 .mkt-page-features .mkt-hero-title,
@@ -1040,10 +1024,6 @@ const pricingCss = sharedMktCss + `
10401024 letter-spacing: -0.03em;
10411025 color: var(--text-strong);
10421026 line-height: 1;
1043 background: var(--accent-gradient);
1044 -webkit-background-clip: text;
1045 background-clip: text;
1046 -webkit-text-fill-color: transparent;
10471027 }
10481028 .mkt-selfhost-label {
10491029 font-family: var(--font-mono);
Modifiedsrc/views/landing.tsx+4−57View fileUnifiedSplit
@@ -136,7 +136,6 @@ export const LandingHero: FC<LandingPageProps> = ({
136136 <div class="landing-hero-bg" aria-hidden="true">
137137 <div class="landing-hero-blob landing-hero-blob-1" />
138138 <div class="landing-hero-blob landing-hero-blob-2" />
139 <div class="landing-hero-blob landing-hero-orb" />
140139 <div class="landing-hero-grid" />
141140 </div>
142141
@@ -147,7 +146,7 @@ export const LandingHero: FC<LandingPageProps> = ({
147146 </div>
148147
149148 <h1 class="landing-hero-title display">
150 <span class="gradient-text">Write the spec. Gluecron ships it.</span>
149 Write the spec. Gluecron ships it.
151150 </h1>
152151
153152 <p class="landing-hero-sub">
@@ -289,10 +288,6 @@ export const LandingHero: FC<LandingPageProps> = ({
289288 {stats!.users === 1 ? " developer" : " developers"}
290289 </span>
291290 )}
292 <span class="landing-stats-sep">·</span>
293 <span>
294 <strong>100%</strong> AI-native
295 </span>
296291 </p>
297292 )}
298293 </div>
@@ -643,7 +638,7 @@ export const LandingHero: FC<LandingPageProps> = ({
643638 <div class="eyebrow">Ready when you are</div>
644639 <h2 class="landing-cta-title">
645640 Stop maintaining the platform.<br />
646 <span class="gradient-text">Start shipping the product.</span>
641 Start shipping the product.
647642 </h2>
648643 <p class="landing-cta-sub">
649644 Free to start, self-hosted-friendly, MCP-native. Migrate from
@@ -1658,18 +1653,8 @@ const landingCss = `
16581653 overflow: hidden;
16591654 }
16601655 .landing-hero-blob-1 {
1661 animation: hero-blob-drift-1 18s var(--ease, ease) infinite alternate;
16621656 }
16631657 .landing-hero-blob-2 {
1664 animation: hero-blob-drift-2 22s var(--ease, ease) infinite alternate;
1665 }
1666 hero-blob-drift-1 {
1667 0% { transform: translate(0, 0) scale(1); opacity: 0.55; }
1668 100% { transform: translate(8%, 6%) scale(1.18); opacity: 0.75; }
1669 }
1670 hero-blob-drift-2 {
1671 0% { transform: translate(0, 0) scale(1); opacity: 0.40; }
1672 100% { transform: translate(-10%, -4%) scale(1.25); opacity: 0.60; }
16731658 }
16741659
16751660 /* ---------- Hero product visual: live AI PR review card ---------- */
@@ -1875,10 +1860,6 @@ const landingCss = `
18751860 border: 1.5px solid var(--border);
18761861 border-top-color: var(--accent);
18771862 border-radius: 50%;
1878 animation: hero-spin 800ms linear infinite;
1879 }
1880 hero-spin {
1881 to { transform: rotate(360deg); }
18821863 }
18831864
18841865 /* Floating accent badges around the card */
@@ -1902,20 +1883,10 @@ const landingCss = `
19021883 .hero-float-1 {
19031884 top: -14px;
19041885 left: -8px;
1905 animation: hero-float-bob-1 5s var(--ease, ease) infinite alternate;
19061886 }
19071887 .hero-float-2 {
19081888 bottom: -14px;
19091889 right: -8px;
1910 animation: hero-float-bob-2 6s var(--ease, ease) infinite alternate;
1911 }
1912 hero-float-bob-1 {
1913 from { transform: translate(0, 0); }
1914 to { transform: translate(-8px, -10px); }
1915 }
1916 hero-float-bob-2 {
1917 from { transform: translate(0, 0); }
1918 to { transform: translate(8px, 8px); }
19191890 }
19201891
19211892 @media (max-width: 720px) {
@@ -1936,19 +1907,6 @@ const landingCss = `
19361907 .landing-hero-blob { display: none; }
19371908 .landing-hero-blob-1 { display: none; }
19381909 .landing-hero-blob-2 { display: none; }
1939 .landing-hero-orb {
1940 position: absolute;
1941 top: -20%;
1942 left: 50%;
1943 transform: translateX(-50%);
1944 width: 900px;
1945 height: 500px;
1946 background: radial-gradient(ellipse at center, rgba(91,110,232,0.08) 0%, transparent 70%);
1947 pointer-events: none;
1948 }
1949 :root[data-theme='light'] .landing-hero-orb {
1950 background: radial-gradient(ellipse at center, rgba(67,83,201,0.05) 0%, transparent 70%);
1951 }
19521910 .landing-hero-grid { display: none; }
19531911
19541912 .landing-hero-inner {
@@ -1969,11 +1927,7 @@ const landingCss = `
19691927 border-radius: 50%;
19701928 background: var(--green);
19711929 flex-shrink: 0;
1972 animation: heroPulse 2.4s ease-in-out infinite;
1973 }
1974 heroPulse {
1975 0%, 100% { opacity: 1; }
1976 50% { opacity: 0.45; }
1930 opacity: 0.8;
19771931 }
19781932
19791933 .landing-hero-title {
@@ -1989,14 +1943,7 @@ const landingCss = `
19891943 color: var(--text-strong);
19901944 }
19911945 .landing-hero-title .gradient-text {
1992 background-image: linear-gradient(135deg, var(--text-strong) 0%, var(--accent) 100%);
1993 -webkit-background-clip: text;
1994 background-clip: text;
1995 -webkit-text-fill-color: transparent;
1996 color: transparent;
1997 }
1998 :root[data-theme='light'] .landing-hero-title .gradient-text {
1999 background-image: linear-gradient(135deg, #0e1020 0%, #4353c9 100%);
1946 color: var(--text-strong);
20001947 }
20011948
20021949 .landing-hero-sub {
Modifiedsrc/views/layout.tsx+5−52View fileUnifiedSplit
@@ -329,8 +329,7 @@ export const Layout: FC<
329329 <div class="footer-brand">
330330 <a href="/" class="logo">gluecron</a>
331331 <p class="footer-tag">
332 AI-native code intelligence. Self-hosted git, automated CI,
333 push-time gates. Software that ships itself.
332 Git hosting with automated CI and AI code review.
334333 </p>
335334 </div>
336335 <div class="footer-links">
@@ -2021,8 +2020,6 @@ const css = `
20212020
20222021 .btn-primary {
20232022 background: var(--accent-gradient);
2024 background-size: 200% 100%;
2025 background-position: 0% 50%;
20262023 border-color: transparent;
20272024 color: #fff;
20282025 font-weight: 600;
@@ -2032,29 +2029,15 @@ const css = `
20322029 inset 0 -1px 0 rgba(0,0,0,0.10),
20332030 0 1px 2px rgba(0,0,0,0.40),
20342031 0 0 0 1px rgba(91,110,232,0.30);
2035 /* U2 — slower 600ms transition on background-position so the
2036 primary CTA shimmers when the cursor lands. */
20372032 transition:
2038 background-position 600ms ease,
2033 background 180ms ease,
20392034 transform 180ms ease,
20402035 box-shadow 180ms ease,
20412036 color 180ms ease;
20422037 }
2043 .btn-primary::before {
2044 content: '';
2045 position: absolute;
2046 inset: 0;
2047 border-radius: inherit;
2048 background: linear-gradient(180deg, rgba(255,255,255,0.18), transparent 60%);
2049 opacity: 0;
2050 transition: opacity var(--t-fast) var(--ease);
2051 pointer-events: none;
2052 }
20532038 .btn-primary:hover {
20542039 color: #fff;
2055 background: var(--accent-gradient);
2056 background-size: 200% 100%;
2057 background-position: 100% 50%;
2040 background: var(--accent-hover);
20582041 border-color: transparent;
20592042 transform: translateY(-1px);
20602043 box-shadow:
@@ -2063,7 +2046,6 @@ const css = `
20632046 0 6px 18px -6px rgba(0,0,0,0.45),
20642047 0 0 0 1px rgba(91,110,232,0.40);
20652048 }
2066 .btn-primary:hover::before { opacity: 1; }
20672049 .btn-primary:active { transform: translateY(0); transition-duration: 80ms; }
20682050 /* U2 — primary focus ring uses the same accent box-shadow recipe. */
20692051 .btn-primary:focus-visible {
@@ -2206,20 +2188,6 @@ const css = `
22062188 position: relative;
22072189 overflow: hidden;
22082190 }
2209 .auth-container::before {
2210 /* Hairline gradient accent on the top edge — signals 'AI-native'
2211 without shouting. Pointer-events disabled so it never interferes
2212 with form interactions. */
2213 content: '';
2214 position: absolute;
2215 top: 0;
2216 left: 0;
2217 right: 0;
2218 height: 2px;
2219 background: linear-gradient(90deg, transparent 0%, rgba(91,110,232,0.55) 50%, transparent 100%);
2220 opacity: 0.6;
2221 pointer-events: none;
2222 }
22232191 .auth-container h2 {
22242192 margin: 0 0 8px;
22252193 font-size: 28px;
@@ -2845,22 +2813,11 @@ const css = `
28452813 overflow: hidden;
28462814 isolation: isolate;
28472815 }
2848 .card::before {
2849 content: '';
2850 position: absolute;
2851 inset: 0;
2852 background: linear-gradient(135deg, rgba(91,110,232,0.06), transparent 50%);
2853 opacity: 0;
2854 transition: opacity var(--t-base) var(--ease);
2855 pointer-events: none;
2856 z-index: -1;
2857 }
28582816 .card:hover {
28592817 border-color: var(--border-strong);
28602818 box-shadow: var(--elev-2);
28612819 transform: translateY(-2px);
28622820 }
2863 .card:hover::before { opacity: 1; }
28642821 .card h3 { font-size: var(--t-md); margin-bottom: 6px; letter-spacing: -0.012em; }
28652822 .card h3 a { color: var(--text); font-weight: 600; }
28662823 .card h3 a:hover { color: var(--text-link); }
@@ -3308,13 +3265,9 @@ const css = `
33083265 /* Badges + commit-sha: smoother transition */
33093266 .commit-sha, .badge { transition: all 120ms cubic-bezier(0.16,1,0.3,1); }
33103267
3311 /* Gradient text utility — kept for back-compat, now renders as a
3312 quiet near-flat indigo rather than a purple→cyan sweep. */
3268 /* Gradient text utility — kept for back-compat, renders as plain accent color. */
33133269 .gradient-text {
3314 background: linear-gradient(135deg, var(--accent-hover) 0%, var(--accent) 100%);
3315 -webkit-background-clip: text;
3316 background-clip: text;
3317 -webkit-text-fill-color: transparent;
3270 color: var(--accent);
33183271 }
33193272
33203273 /* Custom scrollbars (subtle, themed) */
33213274