(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(),
+});