diff --git a/scripts/check-code-health.mjs b/scripts/check-code-health.mjs
index 512f4a6..ce3890f 100644
--- a/scripts/check-code-health.mjs
+++ b/scripts/check-code-health.mjs
@@ -28,11 +28,11 @@ const baselines = {
unlisted: 0,
unresolved: 0,
},
- complexity: { violations: 35, maxCcn: 56, maxLength: 397, maxParams: 17 },
+ complexity: { violations: 34, maxCcn: 56, maxLength: 397, maxParams: 17 },
duplication: {
- clones: 34,
- duplicatedLines: 532,
- percentage: 2.698452954603094,
+ clones: 27,
+ duplicatedLines: 369,
+ percentage: 1.8774,
},
suppressions: 8,
dependencies: { critical: 0, highIds: 0, highFindings: 0 },
diff --git a/scripts/seed-popular.ts b/scripts/seed-popular.ts
index 5385a83..e3870e2 100644
--- a/scripts/seed-popular.ts
+++ b/scripts/seed-popular.ts
@@ -36,7 +36,7 @@ import type { DbClient as Client, InStatement } from '../src/db/client';
import { createD1RestClientFromEnv } from '../src/db/rest-client';
import { isRetryableDbError } from '../src/lib/db-retry';
-import { buildRepoEmbeddingText, generateEmbeddings, textHash } from '../src/lib/embeddings';
+import { buildEmbeddingFromRow, generateEmbeddings } from '../src/lib/embeddings';
import {
enumeratePopularCatalog,
GITHUB_SEARCH_PAGE_SIZE,
@@ -277,22 +277,7 @@ async function embedPending(db: Client, limit: number): Promise {
const toEmbed: { id: number; text: string; hash: string }[] = [];
for (const row of pending.rows) {
- const text = buildRepoEmbeddingText({
- full_name: row.full_name as string,
- description: row.description as string | null,
- language: row.language as string | null,
- topics: row.topics as string,
- ai: row.summary
- ? {
- summary: row.summary as string,
- category: row.category as string,
- subcategories: row.subcategories as string,
- use_cases: row.use_cases as string,
- keywords: row.keywords as string,
- }
- : null,
- });
- const hash = textHash(text);
+ const { text, hash } = buildEmbeddingFromRow(row);
if (row.text_hash !== hash) {
toEmbed.push({ id: row.id as number, text, hash });
}
diff --git a/src/app/api/internal/embed-pending/route.ts b/src/app/api/internal/embed-pending/route.ts
index 0acb523..9d99b35 100644
--- a/src/app/api/internal/embed-pending/route.ts
+++ b/src/app/api/internal/embed-pending/route.ts
@@ -2,7 +2,7 @@ import type { InStatement } from '@/db/client';
import { NextResponse } from 'next/server';
import { db } from '@/db';
-import { buildRepoEmbeddingText, generateEmbeddings, textHash } from '@/lib/embeddings';
+import { buildEmbeddingFromRow, generateEmbeddings } from '@/lib/embeddings';
import { hasValidOperatorToken } from '@/lib/operator-auth';
import { repoVectors } from '@/lib/repo-vectors';
@@ -77,22 +77,7 @@ export async function POST(request: Request) {
const pending: { id: number; text: string; hash: string }[] = [];
for (const row of repos.rows) {
- const text = buildRepoEmbeddingText({
- full_name: row.full_name as string,
- description: row.description as string | null,
- language: row.language as string | null,
- topics: row.topics as string,
- ai: row.summary
- ? {
- summary: row.summary as string,
- category: row.category as string,
- subcategories: row.subcategories as string,
- use_cases: row.use_cases as string,
- keywords: row.keywords as string,
- }
- : null,
- });
- const hash = textHash(text);
+ const { text, hash } = buildEmbeddingFromRow(row);
if (row.text_hash !== hash) {
pending.push({ id: row.id as number, text, hash });
}
diff --git a/src/app/api/repos/[repoId]/route.ts b/src/app/api/repos/[repoId]/route.ts
index d2f741d..be21201 100644
--- a/src/app/api/repos/[repoId]/route.ts
+++ b/src/app/api/repos/[repoId]/route.ts
@@ -2,22 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server';
import { db } from '@/db';
-import { resolveRepoId } from '../resolve';
-
-interface GitHubRepoResponse {
- id: number;
- name: string;
- full_name: string;
- owner: { login: string; avatar_url: string };
- html_url: string;
- description: string | null;
- language: string | null;
- stargazers_count: number;
- archived?: boolean;
- topics?: string[];
- created_at: string;
- updated_at: string;
-}
+import { type GitHubRepoResponse, resolveRepoId, upsertRepoFromGitHub } from '../resolve';
export async function GET(
request: NextRequest,
@@ -82,33 +67,7 @@ export async function GET(
const gh = (await ghRes.json()) as GitHubRepoResponse;
- await db.execute({
- sql: `INSERT INTO repos (id, name, full_name, owner_login, owner_avatar, html_url,
- description, language, stargazers_count, archived, topics, repo_created_at, repo_updated_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- ON CONFLICT(id) DO UPDATE SET
- name = excluded.name, full_name = excluded.full_name,
- owner_login = excluded.owner_login, owner_avatar = excluded.owner_avatar,
- html_url = excluded.html_url, description = excluded.description,
- language = excluded.language, stargazers_count = excluded.stargazers_count,
- archived = excluded.archived, topics = excluded.topics, repo_created_at = excluded.repo_created_at,
- repo_updated_at = excluded.repo_updated_at`,
- args: [
- gh.id,
- gh.name,
- gh.full_name,
- gh.owner.login,
- gh.owner.avatar_url,
- gh.html_url,
- gh.description ?? null,
- gh.language ?? null,
- gh.stargazers_count,
- gh.archived ? 1 : 0,
- JSON.stringify(gh.topics ?? []),
- gh.created_at,
- gh.updated_at,
- ],
- });
+ await upsertRepoFromGitHub(gh);
repoResult = await db.execute({
sql: 'SELECT * FROM repos WHERE id = ?',
diff --git a/src/app/api/repos/resolve.ts b/src/app/api/repos/resolve.ts
index 6cb1fcc..1b32903 100644
--- a/src/app/api/repos/resolve.ts
+++ b/src/app/api/repos/resolve.ts
@@ -1,6 +1,6 @@
import { db } from '@/db';
-interface GitHubRepoResponse {
+export interface GitHubRepoResponse {
id: number;
name: string;
full_name: string;
@@ -15,6 +15,42 @@ interface GitHubRepoResponse {
updated_at: string;
}
+const REPO_UPSERT_SQL = `INSERT INTO repos (id, name, full_name, owner_login, owner_avatar, html_url,
+ description, language, stargazers_count, archived, topics, repo_created_at, repo_updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT(id) DO UPDATE SET
+ name = excluded.name, full_name = excluded.full_name,
+ owner_login = excluded.owner_login, owner_avatar = excluded.owner_avatar,
+ html_url = excluded.html_url, description = excluded.description,
+ language = excluded.language, stargazers_count = excluded.stargazers_count,
+ archived = excluded.archived, topics = excluded.topics, repo_created_at = excluded.repo_created_at,
+ repo_updated_at = excluded.repo_updated_at`;
+
+/**
+ * Upsert a GitHub repo row into D1. Shared by resolveRepoId and the
+ * [repoId] GET route so the INSERT/ON CONFLICT shape stays in one place.
+ */
+export async function upsertRepoFromGitHub(gh: GitHubRepoResponse): Promise {
+ await db.execute({
+ sql: REPO_UPSERT_SQL,
+ args: [
+ gh.id,
+ gh.name,
+ gh.full_name,
+ gh.owner.login,
+ gh.owner.avatar_url,
+ gh.html_url,
+ gh.description ?? null,
+ gh.language ?? null,
+ gh.stargazers_count,
+ gh.archived ? 1 : 0,
+ JSON.stringify(gh.topics ?? []),
+ gh.created_at,
+ gh.updated_at,
+ ],
+ });
+}
+
/**
* Resolve owner/repo slug to a numeric repo ID.
* If the repo isn't in our DB yet, fetches from GitHub and inserts it.
@@ -42,33 +78,7 @@ export async function resolveRepoId(owner: string, repo: string): Promise
-
-
-
-
-
-
-
-
-
-
-
-
- {Array.from({ length: 6 }).map((_, i) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
-
- >
- );
-}
-
export default function DiscoverClient({
initialData,
initialUrl,
diff --git a/src/app/discover/error.tsx b/src/app/discover/error.tsx
index 6ebb9ba..ca6519c 100644
--- a/src/app/discover/error.tsx
+++ b/src/app/discover/error.tsx
@@ -2,6 +2,7 @@
import { useEffect } from 'react';
+import { ErrorActions } from '@/components/error-actions';
import { captureError } from '@/lib/foundry-monitoring';
export default function DiscoverError({
@@ -23,18 +24,7 @@ export default function DiscoverError({
Something went wrong while loading the discover feed — try again.
-
-
-
-
- {error.digest ?
Reference: {error.digest}
: null}
+
);
diff --git a/src/app/error.tsx b/src/app/error.tsx
index 1c9c424..7613112 100644
--- a/src/app/error.tsx
+++ b/src/app/error.tsx
@@ -2,6 +2,7 @@
import { useEffect } from 'react';
+import { ErrorActions } from '@/components/error-actions';
import { captureError } from '@/lib/foundry-monitoring';
export default function Error({
@@ -25,18 +26,7 @@ export default function Error({
An unexpected error occurred on our end. Your data is safe — try again, and if it keeps
happening, come back in a few minutes.
-
-
-
-
- {error.digest ?
Reference: {error.digest}
: null}
+
);
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 2fcb9ff..f4fb1d6 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -72,8 +72,9 @@ export default function RootLayout({
{/* fleet-jsonld:start (generated by apply-agent-surfaces; edit registry, not this) */}
{/* fleet-jsonld:end */}
diff --git a/src/app/stars/error.tsx b/src/app/stars/error.tsx
index d7091d8..185d4bd 100644
--- a/src/app/stars/error.tsx
+++ b/src/app/stars/error.tsx
@@ -2,6 +2,7 @@
import { useEffect } from 'react';
+import { ErrorActions } from '@/components/error-actions';
import { captureError } from '@/lib/foundry-monitoring';
export default function StarsError({
@@ -23,18 +24,7 @@ export default function StarsError({
Something went wrong while loading your starred repos. Your data is safe — try again.
-
-
-
-
- {error.digest ?
Reference: {error.digest}
: null}
+
);
diff --git a/src/app/stars/page.tsx b/src/app/stars/page.tsx
index 4f71358..5d02614 100644
--- a/src/app/stars/page.tsx
+++ b/src/app/stars/page.tsx
@@ -10,13 +10,13 @@ import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
import { ActiveFilterChips } from '@/components/active-filter-chips';
import { BulkActionBar } from '@/components/bulk-action-bar';
import { CompareSheet } from '@/components/compare-sheet';
+import { PageSkeleton } from '@/components/page-skeleton';
import { RepoGrid } from '@/components/repo-grid';
import { Sidebar } from '@/components/sidebar';
import { SyncAnimation, SyncProgressBar } from '@/components/sync-animation';
import { TopBar } from '@/components/top-bar';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Sheet, SheetContent, SheetDescription, SheetTitle } from '@/components/ui/sheet';
-import { Skeleton } from '@/components/ui/skeleton';
import { useLists } from '@/hooks/use-lists';
import { useStarredRepos } from '@/hooks/use-starred-repos';
import { summarizeSyncRepoNames } from '@/lib/sync-performance';
@@ -29,64 +29,6 @@ const sortOptions = [
'name-az',
] as const;
-function PageSkeleton() {
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
- {Array.from({ length: 6 }).map((_, i) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
-
- >
- );
-}
-
export default function StarsPage() {
const { status } = useSession();
const router = useRouter();
diff --git a/src/components/error-actions.tsx b/src/components/error-actions.tsx
new file mode 100644
index 0000000..8bede0d
--- /dev/null
+++ b/src/components/error-actions.tsx
@@ -0,0 +1,29 @@
+/**
+ * Shared error-recovery actions rendered by the per-route `error.tsx`
+ * boundaries. The "Try again" + "Home" button row and optional digest
+ * reference are identical across every error surface.
+ */
+export function ErrorActions({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ return (
+ <>
+
+
+
+
+ {error.digest ?
Reference: {error.digest}
: null}
+ >
+ );
+}
diff --git a/src/components/page-skeleton.tsx b/src/components/page-skeleton.tsx
new file mode 100644
index 0000000..7452f92
--- /dev/null
+++ b/src/components/page-skeleton.tsx
@@ -0,0 +1,64 @@
+import { Skeleton } from '@/components/ui/skeleton';
+
+/**
+ * Shared loading skeleton for the stars and discover list surfaces.
+ * Both pages render an identical header + sidebar + card grid placeholder
+ * while session or data is resolving.
+ */
+export function PageSkeleton() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ >
+ );
+}
diff --git a/src/db/seed-embeddings.ts b/src/db/seed-embeddings.ts
index 9fbd9a4..9bc78a7 100644
--- a/src/db/seed-embeddings.ts
+++ b/src/db/seed-embeddings.ts
@@ -1,6 +1,6 @@
import type { InStatement } from './client';
import { createD1RestClientFromEnv } from './rest-client';
-import { buildRepoEmbeddingText, generateEmbeddings, textHash } from '../lib/embeddings';
+import { buildEmbeddingFromRow, generateEmbeddings } from '../lib/embeddings';
import { createVectorizeRestWriterFromEnv } from '../lib/repo-vectors-rest';
const BATCH_SIZE = 50;
@@ -44,22 +44,7 @@ async function seed() {
const toEmbed: { id: number; text: string; hash: string }[] = [];
for (const row of repos.rows) {
- const text = buildRepoEmbeddingText({
- full_name: row.full_name as string,
- description: row.description as string | null,
- language: row.language as string | null,
- topics: row.topics as string,
- ai: row.summary
- ? {
- summary: row.summary as string,
- category: row.category as string,
- subcategories: row.subcategories as string,
- use_cases: row.use_cases as string,
- keywords: row.keywords as string,
- }
- : null,
- });
- const hash = textHash(text);
+ const { text, hash } = buildEmbeddingFromRow(row);
if (existingHashes.get(row.id as number) !== hash) {
toEmbed.push({ id: row.id as number, text, hash });
}
diff --git a/src/lib/embeddings.ts b/src/lib/embeddings.ts
index fc4892b..70b688f 100644
--- a/src/lib/embeddings.ts
+++ b/src/lib/embeddings.ts
@@ -185,6 +185,34 @@ function parseStringList(value: string | string[] | null | undefined): string[]
}
}
+/**
+ * Build embedding text + hash from a D1 repo row. Consolidates the identical
+ * row-to-text mapping used by the embed-pending route, seed-embeddings, and
+ * seed-popular jobs. The row is `Record` because D1 returns
+ * untyped rows; casts happen once inside the helper.
+ */
+export function buildEmbeddingFromRow(row: Record): {
+ text: string;
+ hash: string;
+} {
+ const text = buildRepoEmbeddingText({
+ full_name: row.full_name as string,
+ description: row.description as string | null,
+ language: row.language as string | null,
+ topics: row.topics as string,
+ ai: row.summary
+ ? {
+ summary: row.summary as string,
+ category: row.category as string,
+ subcategories: row.subcategories as string,
+ use_cases: row.use_cases as string,
+ keywords: row.keywords as string,
+ }
+ : null,
+ });
+ return { text, hash: textHash(text) };
+}
+
/** Simple hash to detect when repo text changes. */
export function textHash(text: string): string {
let h = 0;