Commit6dec21bunknown_key
feat(admin): env-health link, advancement/security links, automation activity tile
feat(admin): env-health link, advancement/security links, automation activity tile - Add 'System Health' section to /admin dashboard with links to /admin/env-health (check env vars), /admin/advancement, /admin/security - Add 'Automation Activity (last 24h)' tile showing AI reviews posted, auto-merges completed, issues AI-triaged, and CI autofixes run - Query prComments.isAiReview, auditLog.action, issueComments.body in parallel with try/catch defaulting to 0 on error - Add admin-automation-* CSS classes for the new tile Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1Wr1gducAJ51tSHJuJ9tY
1 file changed+188−166dec21bdb96ac04694e86db274cf2c7579dc0fdb
1 changed file+188−16
Modifiedsrc/routes/admin.tsx+188−16View 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, repositories, users } from "../db/schema";
26import { aiCostEvents, auditLog, issueComments, prComments, repositories, users } from "../db/schema";
2727import { Layout } from "../views/layout";
2828import { softAuth } from "../middleware/auth";
2929import type { AuthEnv } from "../middleware/auth";
@@ -677,6 +677,90 @@ const adminStyles = `
677677 color: var(--text-strong);
678678 }
679679 .admin-403 p { color: var(--text-muted); margin: 0; font-size: 14px; }
680
681 /* ─── Automation activity tile ─── */
682 .admin-automation-tile {
683 position: relative;
684 padding: var(--space-4);
685 background: var(--bg-elevated);
686 border: 1px solid var(--border);
687 border-radius: 14px;
688 overflow: hidden;
689 margin-bottom: var(--space-5);
690 }
691 .admin-automation-tile::before {
692 content: '';
693 position: absolute;
694 top: 0; left: 0; right: 0;
695 height: 2px;
696 background: linear-gradient(90deg, transparent 0%, #8c6dff 30%, #36c5d6 70%, transparent 100%);
697 opacity: 0.5;
698 pointer-events: none;
699 }
700 .admin-automation-heading {
701 display: flex;
702 align-items: center;
703 gap: 8px;
704 margin-bottom: var(--space-3);
705 }
706 .admin-automation-heading-icon {
707 display: inline-flex;
708 align-items: center;
709 justify-content: center;
710 width: 26px; height: 26px;
711 border-radius: 8px;
712 background: rgba(140,109,255,0.12);
713 color: #b69dff;
714 box-shadow: inset 0 0 0 1px rgba(140,109,255,0.28);
715 }
716 .admin-automation-heading-label {
717 font-family: var(--font-display);
718 font-size: 14px;
719 font-weight: 700;
720 letter-spacing: -0.012em;
721 color: var(--text-strong);
722 }
723 .admin-automation-heading-sub {
724 font-size: 12px;
725 color: var(--text-muted);
726 margin-left: auto;
727 }
728 .admin-automation-grid {
729 display: grid;
730 grid-template-columns: repeat(4, 1fr);
731 gap: var(--space-3);
732 }
733 (max-width: 720px) {
734 .admin-automation-grid { grid-template-columns: 1fr 1fr; }
735 }
736 .admin-automation-stat {
737 padding: var(--space-3) var(--space-3);
738 background: rgba(255,255,255,0.02);
739 border: 1px solid var(--border-subtle);
740 border-radius: 10px;
741 display: flex;
742 flex-direction: column;
743 gap: 4px;
744 }
745 .admin-automation-stat-label {
746 font-size: 10.5px;
747 font-weight: 600;
748 letter-spacing: 0.06em;
749 text-transform: uppercase;
750 color: var(--text-muted);
751 }
752 .admin-automation-stat-value {
753 font-family: var(--font-display);
754 font-size: 28px;
755 font-weight: 800;
756 letter-spacing: -0.022em;
757 line-height: 1;
758 color: var(--text-strong);
759 }
760 .admin-automation-stat-hint {
761 font-size: 11px;
762 color: var(--text-faint);
763 }
680764`;
681765
682766/* ─────────────────────────────────────────────────────────────────────────
@@ -2713,22 +2797,61 @@ admin.get("/admin", async (c) => {
27132797 if (g instanceof Response) return g;
27142798 const { user } = g;
27152799
2716 const [uc] = await db.select({ n: sql<number>`count(*)::int` }).from(users);
2717 const [rc] = await db
2718 .select({ n: sql<number>`count(*)::int` })
2719 .from(repositories);
2720
2721 const recent = await db
2722 .select({
2723 id: users.id,
2724 username: users.username,
2725 createdAt: users.createdAt,
2726 })
2727 .from(users)
2728 .orderBy(desc(users.createdAt))
2729 .limit(10);
2800 const since24h = new Date(Date.now() - 24 * 60 * 60 * 1000);
27302801
2731 const admins = await listSiteAdmins();
2802 const [[uc], [rc], recent, admins] = await Promise.all([
2803 db.select({ n: sql<number>`count(*)::int` }).from(users),
2804 db.select({ n: sql<number>`count(*)::int` }).from(repositories),
2805 db
2806 .select({
2807 id: users.id,
2808 username: users.username,
2809 createdAt: users.createdAt,
2810 })
2811 .from(users)
2812 .orderBy(desc(users.createdAt))
2813 .limit(10),
2814 listSiteAdmins(),
2815 ]);
2816
2817 // Automation activity stats (last 24h) — wrapped in try/catch so a missing
2818 // table never breaks the dashboard.
2819 let aiReviewsCount = 0;
2820 let autoMergesCount = 0;
2821 let issueTriagedCount = 0;
2822 let ciAutofixCount = 0;
2823 try {
2824 const [aiReviewsRow] = await db
2825 .select({ n: sql<number>`count(*)::int` })
2826 .from(prComments)
2827 .where(
2828 sql`${prComments.isAiReview} = true AND ${prComments.createdAt} > ${since24h}`
2829 );
2830 const [autoMergesRow] = await db
2831 .select({ n: sql<number>`count(*)::int` })
2832 .from(auditLog)
2833 .where(
2834 sql`${auditLog.action} = 'auto_merge.merged' AND ${auditLog.createdAt} > ${since24h}`
2835 );
2836 const [issueTriagedRow] = await db
2837 .select({ n: sql<number>`count(*)::int` })
2838 .from(issueComments)
2839 .where(
2840 sql`${issueComments.body} LIKE '%gluecron:issue-triage%' AND ${issueComments.createdAt} > ${since24h}`
2841 );
2842 const [ciAutofixRow] = await db
2843 .select({ n: sql<number>`count(*)::int` })
2844 .from(auditLog)
2845 .where(
2846 sql`${auditLog.action} LIKE 'ci_autofix%' AND ${auditLog.createdAt} > ${since24h}`
2847 );
2848 aiReviewsCount = Number(aiReviewsRow?.n || 0);
2849 autoMergesCount = Number(autoMergesRow?.n || 0);
2850 issueTriagedCount = Number(issueTriagedRow?.n || 0);
2851 ciAutofixCount = Number(ciAutofixRow?.n || 0);
2852 } catch (_) {
2853 // Stats unavailable — defaults (0) already set above
2854 }
27322855
27332856 const msg = c.req.query("result") || c.req.query("error");
27342857 const isErr = !!c.req.query("error");
@@ -2896,6 +3019,55 @@ admin.get("/admin", async (c) => {
28963019 </form>
28973020 </div>
28983021
3022 <div class="admin-h3">
3023 <h3>System Health</h3>
3024 <span class="admin-h3-meta">Ops & security pages</span>
3025 </div>
3026 <div class="admin-actions" style="margin-bottom:var(--space-5)">
3027 <a href="/admin/env-health" class="admin-action is-primary" title="Check which features are enabled via env vars">
3028 <span class="admin-action-icon">{Icons.pulse}</span>
3029 Env health
3030 </a>
3031 <a href="/admin/advancement" class="admin-action" title="Platform advancement tracker">
3032 <span class="admin-action-icon">{Icons.trendingUp}</span>
3033 Advancement
3034 </a>
3035 <a href="/admin/security" class="admin-action" title="Security overview">
3036 <span class="admin-action-icon">{Icons.starShield}</span>
3037 Security
3038 </a>
3039 </div>
3040
3041 <div class="admin-automation-tile">
3042 <div class="admin-automation-heading">
3043 <span class="admin-automation-heading-icon" aria-hidden="true">{Icons.bot}</span>
3044 <span class="admin-automation-heading-label">Automation Activity</span>
3045 <span class="admin-automation-heading-sub">last 24h</span>
3046 </div>
3047 <div class="admin-automation-grid">
3048 <div class="admin-automation-stat">
3049 <div class="admin-automation-stat-label">AI Reviews</div>
3050 <div class="admin-automation-stat-value">{aiReviewsCount}</div>
3051 <div class="admin-automation-stat-hint">PR comments posted</div>
3052 </div>
3053 <div class="admin-automation-stat">
3054 <div class="admin-automation-stat-label">Auto-merges</div>
3055 <div class="admin-automation-stat-value">{autoMergesCount}</div>
3056 <div class="admin-automation-stat-hint">PRs auto-merged</div>
3057 </div>
3058 <div class="admin-automation-stat">
3059 <div class="admin-automation-stat-label">Issues Triaged</div>
3060 <div class="admin-automation-stat-value">{issueTriagedCount}</div>
3061 <div class="admin-automation-stat-hint">AI triage comments</div>
3062 </div>
3063 <div class="admin-automation-stat">
3064 <div class="admin-automation-stat-label">CI Autofixes</div>
3065 <div class="admin-automation-stat-value">{ciAutofixCount}</div>
3066 <div class="admin-automation-stat-hint">Autofix runs</div>
3067 </div>
3068 </div>
3069 </div>
3070
28993071 <div class="admin-h3">
29003072 <h3>Recent signups</h3>
29013073 <span class="admin-h3-meta">
29023074