diff --git a/AGENTS.md b/AGENTS.md index 897395b..bda0ee4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,11 +22,11 @@ Contents of the README.md file: --- -Jules Task Queue is a GitHub-integrated service that solves the "5 concurrent task" bottleneck when using the Google Labs - Jules AI coding assistant. It automatically queues tasks when Jules hits its limit and retries them later, allowing you to seamlessly utilize your full daily quota. +Jules Task Queue is a GitHub-integrated service that solves the "3 concurrent task" bottleneck when using the Google Labs - Jules AI coding assistant. It automatically queues tasks when Jules hits its limit and retries them later, allowing you to seamlessly utilize your full daily quota. -## The Problem: The 5-Task Bottleneck +## The Problem: The 3-Task Bottleneck -> "Jules gives you 60 tasks per day but only 5 concurrent slots. So you're constantly babysitting the queue, manually re-adding labels every time it hits the limit. There has to be a better way." +> "Jules gives you 15 tasks per day but only 3 concurrent slots.\* So you're constantly babysitting the queue, manually re-adding labels every time it hits the limit. There has to be a better way." > — Every Jules power user, probably This tool is the better way. It transforms Jules from a tool you have to manage into a true "set it and forget it" automation partner. diff --git a/GEMINI.md b/GEMINI.md index 897395b..bda0ee4 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -22,11 +22,11 @@ Contents of the README.md file: --- -Jules Task Queue is a GitHub-integrated service that solves the "5 concurrent task" bottleneck when using the Google Labs - Jules AI coding assistant. It automatically queues tasks when Jules hits its limit and retries them later, allowing you to seamlessly utilize your full daily quota. +Jules Task Queue is a GitHub-integrated service that solves the "3 concurrent task" bottleneck when using the Google Labs - Jules AI coding assistant. It automatically queues tasks when Jules hits its limit and retries them later, allowing you to seamlessly utilize your full daily quota. -## The Problem: The 5-Task Bottleneck +## The Problem: The 3-Task Bottleneck -> "Jules gives you 60 tasks per day but only 5 concurrent slots. So you're constantly babysitting the queue, manually re-adding labels every time it hits the limit. There has to be a better way." +> "Jules gives you 15 tasks per day but only 3 concurrent slots.\* So you're constantly babysitting the queue, manually re-adding labels every time it hits the limit. There has to be a better way." > — Every Jules power user, probably This tool is the better way. It transforms Jules from a tool you have to manage into a true "set it and forget it" automation partner. diff --git a/README.md b/README.md index 1f2e040..f2cba54 100644 --- a/README.md +++ b/README.md @@ -19,11 +19,11 @@ --- -Jules Task Queue is a GitHub-integrated service that solves the "5 concurrent task" bottleneck when using the Google Labs - Jules AI coding assistant. It automatically queues tasks when Jules hits its limit and retries them later, allowing you to seamlessly utilize your full daily quota. +Jules Task Queue is a GitHub-integrated service that solves the "3 concurrent task" bottleneck when using the Google Labs - Jules AI coding assistant. It automatically queues tasks when Jules hits its limit and retries them later, allowing you to seamlessly utilize your full daily quota. -## The Problem: The 5-Task Bottleneck +## The Problem: The 3-Task Bottleneck -> "Jules gives you 60 tasks per day but only 5 concurrent slots. So you're constantly babysitting the queue, manually re-adding labels every time it hits the limit. There has to be a better way." +> "Jules gives you 15 tasks per day but only 3 concurrent slots.\* So you're constantly babysitting the queue, manually re-adding labels every time it hits the limit. There has to be a better way." > — Every Jules power user, probably This tool is the better way. It transforms Jules from a tool you have to manage into a true "set it and forget it" automation partner. diff --git a/prisma/migrations/20250817230111_add_composite_index_for_stats_performance/migration.sql b/prisma/migrations/20250817230111_add_composite_index_for_stats_performance/migration.sql new file mode 100644 index 0000000..c2501d7 --- /dev/null +++ b/prisma/migrations/20250817230111_add_composite_index_for_stats_performance/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "jules_tasks_flaggedForRetry_createdAt_idx" ON "jules_tasks"("flaggedForRetry", "createdAt"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 239bb6f..f216435 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -39,6 +39,7 @@ model JulesTask { @@index([repoOwner, repoName]) @@index([createdAt]) @@index([installationId]) + @@index([flaggedForRetry, createdAt]) // Composite index for stats queries @@map("jules_tasks") } diff --git a/src/app/api/stats/repositories/route.ts b/src/app/api/stats/repositories/route.ts new file mode 100644 index 0000000..9556d9a --- /dev/null +++ b/src/app/api/stats/repositories/route.ts @@ -0,0 +1,20 @@ +import { db } from "@/server/db"; +import { NextResponse } from "next/server"; + +export async function GET() { + try { + const totalRepositories = await db.installationRepository.count({ + where: { + removedAt: null, // only active repositories + }, + }); + + return NextResponse.json({ totalRepositories }); + } catch (error) { + console.error("Failed to fetch repository stats:", error); + return NextResponse.json( + { error: "Failed to fetch stats" }, + { status: 500 }, + ); + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 09ef00e..916b5a5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -15,7 +15,7 @@ const geistMono = Geist_Mono({ export const metadata: Metadata = { title: "Google Jules Task Queue", - description: "Break free from the 5-task bottleneck and ship more.", + description: "Break free from the concurrent task bottleneck and ship more.", }; export default function RootLayout({ diff --git a/src/app/page.tsx b/src/app/page.tsx index 0a0f7fb..e159ca4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,6 +6,7 @@ import { HowItWorks, Navigation, ProblemSection, + StatsSection, } from "@/components/landing"; export default function Home() { @@ -14,6 +15,7 @@ export default function Home() { + diff --git a/src/components/landing/hero-section.tsx b/src/components/landing/hero-section.tsx index 24064ee..fc1b87b 100644 --- a/src/components/landing/hero-section.tsx +++ b/src/components/landing/hero-section.tsx @@ -10,12 +10,14 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { useRepositoryStats } from "@/hooks/use-repository-stats"; import { Copy, ExternalLink, Zap } from "lucide-react"; import { useState } from "react"; import { GitHubDashboard } from "./github-dashboard"; export function HeroSection() { const [copied, setCopied] = useState(null); + const { stats, isLoading } = useRepositoryStats(); const copyToClipboard = async (text: string, id: string) => { await navigator.clipboard.writeText(text); @@ -66,11 +68,13 @@ export function HeroSection() {

Queue tasks and avoid the{" "} - 5-task bottleneck + + concurrent task bottleneck +

- Jules gives you 60 tasks per day but only 5 concurrent slots. Jules + Jules gives you 15 tasks per day but only 3 concurrent slots.* Jules Task Queue automatically manages the queue so you can{" "} actually use them all @@ -80,6 +84,19 @@ export function HeroSection() { + {/* Stats validation */} +

+ {!isLoading && stats && ( +

+ Trusted by{" "} + + {stats.totalRepositories.toLocaleString()} + {" "} + repositories +

+ )} +
+
- “Jules gives you 60 tasks per day but only 5 concurrent slots. + “Jules gives you 15 tasks per day but only 3 concurrent slots.* So you’re constantly babysitting the queue, manually re-adding labels every time it hits the limit. There has to be a better way.” diff --git a/src/components/landing/stats-section.tsx b/src/components/landing/stats-section.tsx new file mode 100644 index 0000000..92096c2 --- /dev/null +++ b/src/components/landing/stats-section.tsx @@ -0,0 +1,80 @@ +import { Card, CardContent } from "@/components/ui/card"; +import { appRouter } from "@/server/api/root"; +import { createCallerFactory } from "@/server/api/trpc"; +import { db } from "@/server/db"; +import { env } from "@/lib/env"; + +export async function StatsSection() { + const createCaller = createCallerFactory(appRouter); + const caller = createCaller({ + headers: new Headers(), + db, + env, + }); + + let stats; + try { + stats = await caller.tasks.publicStats(); + } catch (error) { + console.error("Failed to fetch stats:", error); + return null; + } + + const statItems = [ + { + label: "Total Tasks Processed", + value: stats.totalTasks.toLocaleString(), + description: "GitHub issues processed through the queue", + borderColor: "border-jules-primary", + textColor: "text-jules-primary", + }, + { + label: "Total Retries Handled", + value: stats.totalRetries.toLocaleString(), + description: "Tasks automatically retried when Jules hit limits", + borderColor: "border-jules-pink", + textColor: "text-jules-pink", + }, + { + label: "Repositories Connected", + value: stats.totalRepositories.toLocaleString(), + description: "Repositories with the queue integration", + borderColor: "border-jules-cyan", + textColor: "text-jules-cyan", + }, + ]; + + return ( +
+
+
+

+ Jules Queue in Numbers +

+

+ Statistics from our hosted deployment +

+
+ +
+ {statItems.map((stat) => ( + + +
+ {stat.value} +
+
{stat.label}
+
+ {stat.description} +
+
+
+ ))} +
+
+
+ ); +} diff --git a/src/components/success/success-state.tsx b/src/components/success/success-state.tsx index 3c0a217..d8abc2d 100644 --- a/src/components/success/success-state.tsx +++ b/src/components/success/success-state.tsx @@ -136,7 +136,7 @@ export function SuccessState({ Smart queue management: {" "} When you add the "jules" label to an issue, - we'll automatically queue it and manage the 5-task limit. + we'll automatically queue it and manage the 3-task limit.
  • diff --git a/src/hooks/use-repository-stats.ts b/src/hooks/use-repository-stats.ts new file mode 100644 index 0000000..13938f8 --- /dev/null +++ b/src/hooks/use-repository-stats.ts @@ -0,0 +1,35 @@ +import { useEffect, useState } from "react"; + +interface RepositoryStats { + totalRepositories: number; +} + +export function useRepositoryStats() { + const [stats, setStats] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchStats = async () => { + try { + setIsLoading(true); + const response = await fetch("/api/stats/repositories"); + if (!response.ok) { + throw new Error("Failed to fetch stats"); + } + const data = await response.json(); + setStats(data); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to fetch stats"); + setStats(null); + } finally { + setIsLoading(false); + } + }; + + fetchStats(); + }, []); + + return { stats, isLoading, error }; +} diff --git a/src/server/api/routers/tasks.ts b/src/server/api/routers/tasks.ts index a462707..505bf23 100644 --- a/src/server/api/routers/tasks.ts +++ b/src/server/api/routers/tasks.ts @@ -40,43 +40,61 @@ export const tasksRouter = createTRPCRouter({ }; }), - // Get task statistics - stats: publicProcedure.query(async ({ ctx }) => { - const [totalTasks, queuedTasks, activeTasks, completedToday] = - await Promise.all([ - // Total tasks - ctx.db.julesTask.count(), - - // Queued tasks (flagged for retry) - ctx.db.julesTask.count({ - where: { flaggedForRetry: true }, - }), - - // Active tasks (created in last hour, not flagged for retry) - ctx.db.julesTask.count({ - where: { - flaggedForRetry: false, - createdAt: { - gte: new Date(Date.now() - 60 * 60 * 1000), // last hour - }, - }, - }), - - // Completed today (approximate - tasks that were retried today) - ctx.db.julesTask.count({ - where: { - lastRetryAt: { - gte: new Date(new Date().setHours(0, 0, 0, 0)), // start of today - }, + // Get public project statistics + publicStats: publicProcedure.query(async ({ ctx }) => { + const [ + totalTasks, + totalRetries, + queuedTasks, + activeTasks, + totalInstallations, + totalRepositories, + ] = await Promise.all([ + // Total tasks ever created + ctx.db.julesTask.count(), + + // Total retry count across all tasks + ctx.db.julesTask.aggregate({ + _sum: { retryCount: true }, + }), + + // Currently queued tasks (flagged for retry) + ctx.db.julesTask.count({ + where: { flaggedForRetry: true }, + }), + + // Active tasks (created in last 24 hours, not flagged for retry) + ctx.db.julesTask.count({ + where: { + flaggedForRetry: false, + createdAt: { + gte: new Date(Date.now() - 24 * 60 * 60 * 1000), // last 24 hours }, - }), - ]); + }, + }), + + // Total GitHub installations + ctx.db.gitHubInstallation.count({ + where: { + suspendedAt: null, // only active installations + }, + }), + + // Total repositories with active installations + ctx.db.installationRepository.count({ + where: { + removedAt: null, // only active repositories + }, + }), + ]); return { totalTasks, + totalRetries: totalRetries._sum.retryCount ?? 0, queuedTasks, activeTasks, - completedToday, + totalInstallations, + totalRepositories, }; }), diff --git a/src/types/schemas.ts b/src/types/schemas.ts index 4512885..e13bb39 100644 --- a/src/types/schemas.ts +++ b/src/types/schemas.ts @@ -103,3 +103,13 @@ export const CleanupTasksSchema = z.object({ export const SystemHealthSchema = z.object({ includeDetails: z.boolean().default(false), }); + +// Public stats schema +export const PublicStatsSchema = z.object({ + totalTasks: z.number(), + totalRetries: z.number(), + queuedTasks: z.number(), + activeTasks: z.number(), + totalInstallations: z.number(), + totalRepositories: z.number(), +});