Commit91a0204unknown_key
feat: helpful empty state for new repos, health score badge on repo header
feat: helpful empty state for new repos, health score badge on repo header - Replace bare empty-state with three-option panel (push/import/spec-to-PR) - Add computeHealthScore call (fire-and-forget) to repo overview handler - Render grade badge (Elite/Strong/Improving/Needs Attention) with colour next to RepoHeader on both empty and non-empty repo views - Badge links to /:owner/:repo/insights/health https://claude.ai/code/session_01DzJMTFASjMHt2f5ze4cNLR
1 file changed+344−4191a02040be9d7dacb2f9e7cbc03d453cd765b970
1 changed file+344−41
Modifiedsrc/routes/web.tsx+344−41View fileUnifiedSplit
@@ -47,6 +47,8 @@ import {
4747} from "../git/repository";
4848import { renderMarkdown, markdownCss } from "../lib/markdown";
4949import { highlightCode } from "../lib/highlight";
50import { computeHealthScore } from "../lib/health-score";
51import type { HealthScore } from "../lib/health-score";
5052import { softAuth, requireAuth } from "../middleware/auth";
5153import type { AuthEnv } from "../middleware/auth";
5254import { trackByName } from "../lib/traffic";
@@ -60,6 +62,7 @@ import {
6062 countAiReviewsSince,
6163 listDemoActivityFeed,
6264} from "../lib/demo-activity";
65import { computeHealthScore, type HealthScore } from "../lib/health-score";
6366
6467const web = new Hono<AuthEnv>();
6568
@@ -2380,6 +2383,18 @@ web.get("/:owner/:repo", async (c) => {
23802383 repoOwnerId,
23812384 } = starInfo;
23822385
2386 // Health score badge — fire-and-forget, best-effort. If the DB call fails
2387 // or repoId is null (anonymous view of non-DB repo), healthScore stays null
2388 // and the badge simply doesn't render.
2389 let healthScore: HealthScore | null = null;
2390 if (repoId) {
2391 try {
2392 healthScore = await computeHealthScore(repoId);
2393 } catch {
2394 // swallow — badge is optional
2395 }
2396 }
2397
23832398 // Pending-comments banner data (lazy + best-effort). Only the repo
23842399 // owner sees the banner, so non-owner views skip the DB hit entirely.
23852400 let repoHomePendingCount = 0;
@@ -2712,6 +2727,131 @@ web.get("/:owner/:repo", async (c) => {
27122727 .repo-home-empty-snippet .repo-home-cmt { color: var(--text-faint); }
27132728 .repo-home-empty-snippet .repo-home-cmd { color: var(--accent); }
27142729
2730 /* Health score badge */
2731 .repo-health-badge {
2732 display: inline-flex;
2733 align-items: center;
2734 gap: 5px;
2735 padding: 3px 10px;
2736 border-radius: 999px;
2737 font-size: 11.5px;
2738 font-weight: 700;
2739 font-family: var(--font-mono);
2740 text-decoration: none;
2741 border: 1px solid transparent;
2742 transition: filter 120ms ease, opacity 120ms ease;
2743 vertical-align: middle;
2744 }
2745 .repo-health-badge:hover { filter: brightness(1.15); text-decoration: none; opacity: 0.9; }
2746 .repo-health-badge-elite {
2747 background: rgba(52,211,153,0.15);
2748 color: #6ee7b7;
2749 border-color: rgba(52,211,153,0.30);
2750 }
2751 .repo-health-badge-strong {
2752 background: rgba(96,165,250,0.15);
2753 color: #93c5fd;
2754 border-color: rgba(96,165,250,0.30);
2755 }
2756 .repo-health-badge-improving {
2757 background: rgba(251,191,36,0.15);
2758 color: #fde68a;
2759 border-color: rgba(251,191,36,0.30);
2760 }
2761 .repo-health-badge-needs-attention {
2762 background: rgba(248,113,113,0.15);
2763 color: #fca5a5;
2764 border-color: rgba(248,113,113,0.30);
2765 }
2766
2767 /* Three-option empty-state panel */
2768 .repo-empty-options {
2769 display: grid;
2770 grid-template-columns: repeat(3, 1fr);
2771 gap: var(--space-3);
2772 margin-top: var(--space-5);
2773 }
2774 (max-width: 760px) {
2775 .repo-empty-options { grid-template-columns: 1fr; }
2776 }
2777 .repo-empty-option {
2778 position: relative;
2779 background: var(--bg-elevated);
2780 border: 1px solid var(--border);
2781 border-radius: 14px;
2782 padding: var(--space-4) var(--space-4);
2783 display: flex;
2784 flex-direction: column;
2785 gap: var(--space-2);
2786 overflow: hidden;
2787 transition: border-color 140ms ease, background 140ms ease;
2788 }
2789 .repo-empty-option:hover {
2790 border-color: rgba(140,109,255,0.45);
2791 background: rgba(140,109,255,0.04);
2792 }
2793 .repo-empty-option::before {
2794 content: '';
2795 position: absolute;
2796 top: 0; left: 0; right: 0;
2797 height: 2px;
2798 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
2799 opacity: 0.55;
2800 pointer-events: none;
2801 }
2802 .repo-empty-option-label {
2803 font-size: 10px;
2804 font-family: var(--font-mono);
2805 font-weight: 700;
2806 letter-spacing: 0.12em;
2807 text-transform: uppercase;
2808 color: var(--accent);
2809 margin-bottom: 2px;
2810 }
2811 .repo-empty-option-title {
2812 font-family: var(--font-display);
2813 font-weight: 700;
2814 font-size: 16px;
2815 letter-spacing: -0.01em;
2816 color: var(--text-strong);
2817 margin: 0;
2818 }
2819 .repo-empty-option-sub {
2820 font-size: 13px;
2821 color: var(--text-muted);
2822 line-height: 1.5;
2823 margin: 0;
2824 flex: 1;
2825 }
2826 .repo-empty-option-snippet {
2827 background: var(--bg);
2828 border: 1px solid var(--border);
2829 border-radius: 8px;
2830 padding: var(--space-2) var(--space-3);
2831 font-family: var(--font-mono);
2832 font-size: 11.5px;
2833 line-height: 1.75;
2834 color: var(--text-strong);
2835 overflow-x: auto;
2836 white-space: pre;
2837 margin: var(--space-1) 0 0;
2838 }
2839 .repo-empty-option-snippet .reo-cmt { color: var(--text-faint); }
2840 .repo-empty-option-snippet .reo-cmd { color: var(--accent); }
2841 .repo-empty-option-cta {
2842 display: inline-flex;
2843 align-items: center;
2844 gap: 6px;
2845 margin-top: auto;
2846 padding-top: var(--space-2);
2847 font-size: 13px;
2848 font-weight: 600;
2849 color: var(--accent);
2850 text-decoration: none;
2851 transition: color 120ms ease;
2852 }
2853 .repo-empty-option-cta:hover { color: var(--text-strong); text-decoration: none; }
2854
27152855 (max-width: 720px) {
27162856 .repo-home-hero { padding: var(--space-4) var(--space-4); }
27172857 .repo-home-clone-body { flex-direction: column; align-items: stretch; }
@@ -2764,6 +2904,113 @@ web.get("/:owner/:repo", async (c) => {
27642904 return c.html(
27652905 <Layout title={`${owner}/${repo}`} user={user}>
27662906 <style dangerouslySetInnerHTML={{ __html: repoHomeCss }} />
2907 <style dangerouslySetInnerHTML={{ __html: `
2908 .empty-options-grid {
2909 display: grid;
2910 grid-template-columns: repeat(3, 1fr);
2911 gap: var(--space-4);
2912 margin-top: var(--space-4);
2913 }
2914 @media (max-width: 800px) {
2915 .empty-options-grid { grid-template-columns: 1fr; }
2916 }
2917 .empty-option-card {
2918 position: relative;
2919 background: var(--bg-elevated);
2920 border: 1px solid var(--border);
2921 border-radius: 14px;
2922 padding: var(--space-5);
2923 display: flex;
2924 flex-direction: column;
2925 gap: var(--space-3);
2926 overflow: hidden;
2927 transition: border-color 140ms ease, box-shadow 140ms ease;
2928 }
2929 .empty-option-card:hover {
2930 border-color: rgba(140,109,255,0.45);
2931 box-shadow: 0 8px 24px -8px rgba(140,109,255,0.18);
2932 }
2933 .empty-option-card::before {
2934 content: '';
2935 position: absolute;
2936 top: 0; left: 0; right: 0;
2937 height: 2px;
2938 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
2939 opacity: 0.55;
2940 pointer-events: none;
2941 }
2942 .empty-option-label {
2943 font-size: 10.5px;
2944 font-family: var(--font-mono);
2945 text-transform: uppercase;
2946 letter-spacing: 0.14em;
2947 color: var(--accent);
2948 font-weight: 700;
2949 }
2950 .empty-option-title {
2951 font-family: var(--font-display);
2952 font-weight: 700;
2953 font-size: 17px;
2954 letter-spacing: -0.01em;
2955 color: var(--text-strong);
2956 margin: 0;
2957 line-height: 1.25;
2958 }
2959 .empty-option-body {
2960 font-size: 13px;
2961 color: var(--text-muted);
2962 line-height: 1.55;
2963 margin: 0;
2964 flex: 1;
2965 }
2966 .empty-option-snippet {
2967 background: var(--bg);
2968 border: 1px solid var(--border);
2969 border-radius: 8px;
2970 padding: var(--space-3) var(--space-4);
2971 font-family: var(--font-mono);
2972 font-size: 11.5px;
2973 line-height: 1.75;
2974 color: var(--text-strong);
2975 overflow-x: auto;
2976 white-space: pre;
2977 margin: 0;
2978 }
2979 .empty-option-snippet .ec { color: var(--text-faint); }
2980 .empty-option-snippet .em { color: var(--accent); }
2981 .empty-option-cta {
2982 display: inline-flex;
2983 align-items: center;
2984 gap: 6px;
2985 padding: 8px 16px;
2986 font-size: 13px;
2987 font-weight: 600;
2988 color: var(--text-strong);
2989 background: var(--bg-secondary);
2990 border: 1px solid var(--border);
2991 border-radius: 9999px;
2992 text-decoration: none;
2993 align-self: flex-start;
2994 transition: border-color 140ms ease, background 140ms ease, color 140ms ease;
2995 }
2996 .empty-option-cta:hover {
2997 border-color: rgba(140,109,255,0.55);
2998 background: rgba(140,109,255,0.08);
2999 color: var(--accent);
3000 text-decoration: none;
3001 }
3002 .empty-option-cta-accent {
3003 background: linear-gradient(135deg, #8c6dff, #36c5d6);
3004 border-color: transparent;
3005 color: #fff;
3006 }
3007 .empty-option-cta-accent:hover {
3008 background: linear-gradient(135deg, #7c5df0, #28b4c8);
3009 border-color: transparent;
3010 color: #fff;
3011 box-shadow: 0 6px 18px -6px rgba(140,109,255,0.55);
3012 }
3013 ` }} />
27673014 <div class="repo-home-hero">
27683015 <div class="repo-home-hero-orb-wrap" aria-hidden="true">
27693016 <div class="repo-home-hero-orb" />
@@ -2772,16 +3019,33 @@ web.get("/:owner/:repo", async (c) => {
27723019 <div class="repo-home-eyebrow">
27733020 <strong>Repository</strong> · {owner}
27743021 </div>
2775 <RepoHeader
2776 owner={owner}
2777 repo={repo}
2778 starCount={starCount}
2779 starred={starred}
2780 forkCount={forkCount}
2781 currentUser={user?.username}
2782 archived={archived}
2783 isTemplate={isTemplate}
2784 />
3022 <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
3023 <RepoHeader
3024 owner={owner}
3025 repo={repo}
3026 starCount={starCount}
3027 starred={starred}
3028 forkCount={forkCount}
3029 currentUser={user?.username}
3030 archived={archived}
3031 isTemplate={isTemplate}
3032 />
3033 {healthScore && (() => {
3034 const gradeLabel: Record<string, string> = {
3035 elite: "Elite", strong: "Strong",
3036 improving: "Improving", "needs-attention": "Needs Attention",
3037 };
3038 return (
3039 <a
3040 href={`/${owner}/${repo}/insights/health`}
3041 class={`repo-health-badge repo-health-badge-${healthScore!.grade}`}
3042 title={`Health score: ${healthScore!.total}/100`}
3043 >
3044 {gradeLabel[healthScore!.grade] ?? healthScore!.grade}
3045 </a>
3046 );
3047 })()}
3048 </div>
27853049 {description ? (
27863050 <p class="repo-home-description">{description}</p>
27873051 ) : (
@@ -2793,31 +3057,53 @@ web.get("/:owner/:repo", async (c) => {
27933057 </div>
27943058 </div>
27953059 <RepoNav owner={owner} repo={repo} active="code" />
2796 <div class="repo-home-empty">
2797 <div class="repo-home-empty-eyebrow">Getting started</div>
3060 <div style="margin-top:var(--space-4)">
3061 <div style="font-size:12px;font-family:var(--font-mono);color:var(--accent);letter-spacing:0.12em;text-transform:uppercase;font-weight:600;margin-bottom:var(--space-2)">
3062 Getting started
3063 </div>
27983064 <h2 class="repo-home-empty-title">
2799 Push your first commit to{" "}
2800 <span class="gradient-text">{repo}</span>.
3065 <span class="gradient-text">{repo}</span> is ready — make your first move.
28013066 </h2>
28023067 <p class="repo-home-empty-sub">
2803 This repository is empty. Paste the snippet below in an existing
2804 project directory to wire it up to Gluecron — your push triggers
2805 gate checks and AI review automatically.
3068 Choose how you want to kick things off. Every push wires up gate
3069 checks and AI review automatically.
28063070 </p>
2807 <pre class="repo-home-empty-snippet">
2808 <span class="repo-home-cmt">
2809 # from an existing project directory
2810 </span>
2811 {"\n"}
2812 <span class="repo-home-cmd">git remote add</span>
2813 {` origin ${cloneHttpsUrl}`}
2814 {"\n"}
2815 <span class="repo-home-cmd">git branch</span>
2816 {` -M main`}
2817 {"\n"}
2818 <span class="repo-home-cmd">git push</span>
2819 {` -u origin main`}
2820 </pre>
3071 <div class="empty-options-grid">
3072 <div class="empty-option-card">
3073 <div class="empty-option-label">Option A</div>
3074 <h3 class="empty-option-title">Push your first commit</h3>
3075 <p class="empty-option-body">
3076 Wire up an existing project in seconds. Run these commands from
3077 your project directory.
3078 </p>
3079 <pre class="empty-option-snippet"><span class="ec"># from your project directory</span>
3080<span class="em">git remote add</span> origin {cloneHttpsUrl}
3081<span class="em">git branch</span> -M main
3082<span class="em">git push</span> -u origin main</pre>
3083 </div>
3084 <div class="empty-option-card">
3085 <div class="empty-option-label">Option B</div>
3086 <h3 class="empty-option-title">Import from GitHub</h3>
3087 <p class="empty-option-body">
3088 Mirror an existing GitHub repository here in one click. Gluecron
3089 syncs the full history and branches.
3090 </p>
3091 <a href="/import" class="empty-option-cta">
3092 Import repository
3093 </a>
3094 </div>
3095 <div class="empty-option-card">
3096 <div class="empty-option-label">Option C</div>
3097 <h3 class="empty-option-title">Try Spec-to-PR</h3>
3098 <p class="empty-option-body">
3099 Let AI write your first feature. Describe what you want to build
3100 and Gluecron opens a pull request with the code.
3101 </p>
3102 <a href={`/${owner}/${repo}/specs`} class="empty-option-cta empty-option-cta-accent">
3103 Let AI write your first feature
3104 </a>
3105 </div>
3106 </div>
28213107 </div>
28223108 </Layout>
28233109 );
@@ -2840,16 +3126,33 @@ web.get("/:owner/:repo", async (c) => {
28403126 <div class="repo-home-eyebrow">
28413127 <strong>Repository</strong> · {owner}
28423128 </div>
2843 <RepoHeader
2844 owner={owner}
2845 repo={repo}
2846 starCount={starCount}
2847 starred={starred}
2848 forkCount={forkCount}
2849 currentUser={user?.username}
2850 archived={archived}
2851 isTemplate={isTemplate}
2852 />
3129 <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
3130 <RepoHeader
3131 owner={owner}
3132 repo={repo}
3133 starCount={starCount}
3134 starred={starred}
3135 forkCount={forkCount}
3136 currentUser={user?.username}
3137 archived={archived}
3138 isTemplate={isTemplate}
3139 />
3140 {healthScore && (() => {
3141 const gradeLabel: Record<string, string> = {
3142 elite: "Elite", strong: "Strong",
3143 improving: "Improving", "needs-attention": "Needs Attention",
3144 };
3145 return (
3146 <a
3147 href={`/${owner}/${repo}/insights/health`}
3148 class={`repo-health-badge repo-health-badge-${healthScore!.grade}`}
3149 title={`Health score: ${healthScore!.total}/100`}
3150 >
3151 {gradeLabel[healthScore!.grade] ?? healthScore!.grade}
3152 </a>
3153 );
3154 })()}
3155 </div>
28533156 {description ? (
28543157 <p class="repo-home-description">{description}</p>
28553158 ) : (
28563159