diff --git a/platform/lib/github.ts b/platform/lib/github.ts index 4b444d0..c8625c8 100644 --- a/platform/lib/github.ts +++ b/platform/lib/github.ts @@ -3,8 +3,6 @@ * Tokens come from the user's NextAuth session. */ -import { normalizeRepoSlug } from "./repo-slug"; - const API = "https://api.github.com"; export interface GitHubOrgSummary { @@ -180,43 +178,3 @@ export async function listUserOrgs( return detailed; } - -interface RawRepoDetail { - archived: boolean; -} - -/** - * Checks GitHub's `archived` flag for each given remote URL, using the - * caller's own OAuth token. Ephemeral: nothing here is persisted, and - * nothing calls this automatically — it's meant to run on-demand for - * whatever repos are currently on screen, not as a background sync. - * - * The login scope has no `repo` grant, so a private repo 404s; that comes - * back as `null` (unknown) for that entry rather than failing the batch — - * archived status for private repos just can't be determined this way. - */ -export async function checkArchivedStatus( - remoteUrls: (string | null | undefined)[], - accessToken: string, -): Promise> { - const pathBySlug = new Map(); - for (const url of remoteUrls) { - const slug = normalizeRepoSlug(url ?? null); - if (!slug || !slug.startsWith("github.com/")) continue; - const path = slug.slice("github.com/".length); - if (path.split("/").length === 2) pathBySlug.set(slug, path); - } - - const entries = await Promise.all( - Array.from(pathBySlug.entries()).map(async ([slug, path]) => { - try { - const detail = await call(`/repos/${path}`, accessToken); - return [slug, detail.archived] as const; - } catch { - return [slug, null] as const; - } - }), - ); - - return Object.fromEntries(entries); -} diff --git a/platform/lib/integrations/datadog/sync.ts b/platform/lib/integrations/datadog/sync.ts index 4125db4..2d33108 100644 --- a/platform/lib/integrations/datadog/sync.ts +++ b/platform/lib/integrations/datadog/sync.ts @@ -25,9 +25,6 @@ import { import { logger } from "@/lib/debug"; import { decryptCredentials } from "@/lib/encryption"; -import { normalizeRepoSlug } from "@/lib/repo-slug"; - -export { normalizeRepoSlug }; const DEFAULT_BACKFILL_DAYS = 30; const PAGE_LIMIT = 100; @@ -246,6 +243,28 @@ async function loadRepoLookup( return { byNormalizedSlug }; } +/** + * Normalize a git remote URL or Datadog slug into "host/path" form + * (lowercased, no scheme, no `.git`, no trailing slash). Returns `null` + * for empty input. + */ +export function normalizeRepoSlug( + input: string | null | undefined, +): string | null { + if (!input) return null; + let s = input.trim().toLowerCase(); + if (!s) return null; + // git@github.com:org/repo.git → github.com/org/repo + s = s.replace(/^git@([^:]+):/, "$1/"); + // ssh://git@host/org/repo or https://host/org/repo → host/org/repo + s = s.replace(/^[a-z]+:\/\//, ""); + s = s.replace(/^git@/, ""); + s = s.replace(/^www\./, ""); + s = s.replace(/\.git$/, ""); + s = s.replace(/\/+$/, ""); + return s || null; +} + /** * Return the first occurrence of each item keyed by `idFn`. * diff --git a/platform/lib/repo-slug.ts b/platform/lib/repo-slug.ts deleted file mode 100644 index c1f156a..0000000 --- a/platform/lib/repo-slug.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Pure repo-slug normalization — no dependencies, safe to import from - * client components. Kept out of lib/integrations/datadog/sync.ts (which - * originally defined it) because that module transitively imports - * lib/supabase.ts (server-only, uses next/headers); a client component - * importing anything from that file at all pulls the whole server-only - * chain into the browser bundle and fails the build. - */ -export function normalizeRepoSlug( - input: string | null | undefined, -): string | null { - if (!input) return null; - let s = input.trim().toLowerCase(); - if (!s) return null; - // git@github.com:org/repo.git → github.com/org/repo - s = s.replace(/^git@([^:]+):/, "$1/"); - // ssh://git@host/org/repo or https://host/org/repo → host/org/repo - s = s.replace(/^[a-z]+:\/\//, ""); - s = s.replace(/^git@/, ""); - s = s.replace(/^www\./, ""); - s = s.replace(/\.git$/, ""); - s = s.replace(/\/+$/, ""); - return s || null; -} diff --git a/platform/lib/translations.ts b/platform/lib/translations.ts index 3b9e013..7d8f1aa 100644 --- a/platform/lib/translations.ts +++ b/platform/lib/translations.ts @@ -456,11 +456,6 @@ export const translations = { ai: "AI", sortByAi: "Most AI first", staleOnly: "Stale (90d+)", - checkArchived: "Check archived", - checkingArchived: "Checking...", - hideArchived: "Hide archived", - archivedTag: "archived", - noGithubLink: "Sign in with GitHub to check archived status", }, }, investHere: { @@ -1881,12 +1876,6 @@ export const translations = { ai: "IA", sortByAi: "Mais IA primeiro", staleOnly: "Parados (90d+)", - checkArchived: "Checar arquivados", - checkingArchived: "Checando...", - hideArchived: "Ocultar arquivados", - archivedTag: "arquivado", - noGithubLink: - "Faça login com GitHub para checar status de arquivamento", }, }, investHere: { diff --git a/platform/src/app/[tenant]/dashboard/repo-list.tsx b/platform/src/app/[tenant]/dashboard/repo-list.tsx index bd1da84..313413d 100644 --- a/platform/src/app/[tenant]/dashboard/repo-list.tsx +++ b/platform/src/app/[tenant]/dashboard/repo-list.tsx @@ -4,19 +4,11 @@ import { useState } from "react"; import Link from "next/link"; -import { - Archive, - ArrowDownWideNarrow, - Clock, - Search, - Trash2, -} from "lucide-react"; -import { useSession } from "next-auth/react"; +import { ArrowDownWideNarrow, Clock, Search, Trash2 } from "lucide-react"; import { DeleteRepositoryDialog } from "@/components/repos/DeleteRepositoryDialog"; import { Button } from "@/components/ui/button"; import { useTranslation } from "@/hooks/useTranslation"; -import { normalizeRepoSlug } from "@/lib/repo-slug"; import { cn } from "@/lib/utils"; import type { RepoSummary } from "@/types/temporal"; import { healthIndicator } from "@/types/temporal"; @@ -50,55 +42,14 @@ export function RepoList({ nowMs, }: RepoListProps) { const { t } = useTranslation(); - const { data: session } = useSession(); const [query, setQuery] = useState(""); const [sortByAi, setSortByAi] = useState(false); const [staleOnly, setStaleOnly] = useState(false); - const [hideArchived, setHideArchived] = useState(false); - const [checkingArchived, setCheckingArchived] = useState(false); - const [hasCheckedArchived, setHasCheckedArchived] = useState(false); - // Ephemeral, not persisted: repo-slug -> archived (null = unknown, e.g. - // private repo the session token can't read). Empty until "Check - // archived" runs, and re-fetched fresh every time — never cached across - // page loads. - const [archivedBySlug, setArchivedBySlug] = useState< - Record - >({}); const showDeleteColumn = canDelete && !!organizationId; // nowMs comes from the server component (page.tsx) rather than a // Date.now() call here — calling it directly during a client render // isn't pure. const staleCutoff = nowMs - STALE_MS; - const hasGithubLink = !!( - session?.user as { githubAccessToken?: string } | undefined - )?.githubAccessToken; - - async function handleCheckArchived() { - setCheckingArchived(true); - try { - const res = await fetch("/api/repos/check-archived", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ remoteUrls: repos.map((r) => r.remote_url) }), - }); - if (res.ok) { - const data = (await res.json()) as { - archived: Record; - }; - setArchivedBySlug(data.archived); - setHasCheckedArchived(true); - } - } catch { - // Non-fatal — the button just stays available to retry. - } finally { - setCheckingArchived(false); - } - } - - function isArchived(repo: RepoSummary): boolean | null { - const slug = normalizeRepoSlug(repo.remote_url); - return slug ? (archivedBySlug[slug] ?? null) : null; - } if (repos.length === 0) { return ( @@ -123,10 +74,6 @@ export function RepoList({ ); } - if (hideArchived) { - filtered = filtered.filter((r) => isArchived(r) !== true); - } - // Repos without AI data sort to the end regardless of direction. const sorted = sortByAi ? [...filtered].sort( @@ -170,40 +117,12 @@ export function RepoList({ {t("dashboard.repoList.staleOnly")} - - {hasCheckedArchived && ( - - )} )} {sorted.map((repo) => { const color = healthIndicator(repo.health); const aiPct = repo.ai_detection_coverage_pct; const humanPct = aiPct != null ? 100 - aiPct : null; - const archived = isArchived(repo); return (
-

+

{repo.name} - {archived === true && ( - - {t("dashboard.repoList.archivedTag")} - - )}

{repo.runs_count} runs diff --git a/platform/src/app/api/repos/check-archived/route.ts b/platform/src/app/api/repos/check-archived/route.ts deleted file mode 100644 index 147e6c5..0000000 --- a/platform/src/app/api/repos/check-archived/route.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -import { getServerSession } from "next-auth/next"; - -import { authOptions } from "@/lib/auth"; -import { logError } from "@/lib/debug"; -import { checkArchivedStatus } from "@/lib/github"; - -/** - * On-demand, ephemeral archived-status check for a set of repos — nothing - * here is persisted. Meant to be called for whatever repos are currently - * rendered on a page, not as a full-org background sync. - */ -export async function POST(request: NextRequest) { - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json({ error: "unauthorized" }, { status: 401 }); - } - - const accessToken = (session.user as { githubAccessToken?: string }) - .githubAccessToken; - if (!accessToken) { - return NextResponse.json({ error: "no_github_link" }, { status: 412 }); - } - - const body = await request.json().catch(() => null); - const remoteUrls = Array.isArray(body?.remoteUrls) - ? body.remoteUrls.filter((v: unknown): v is string => typeof v === "string") - : []; - - try { - const archived = await checkArchivedStatus(remoteUrls, accessToken); - return NextResponse.json({ archived }); - } catch (error) { - logError(error, "POST /api/repos/check-archived"); - return NextResponse.json({ error: "github_api_error" }, { status: 502 }); - } -}