Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/app/(app)/maintainer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
getMaintainerPrQueue,
getRepoHealthOverview,
getStaleIssues,
getFlaggedAccounts,
getTopContributors,
type FlaggedAccountRow,
type MaintainerInstall,
type MaintainerPrRow,
type RepoHealthRow,
Expand Down Expand Up @@ -82,6 +84,10 @@ export default async function MaintainerPage({

const contributorsRes = await getTopContributors();
const topContributors: ContributorRow[] = isOk(contributorsRes) ? contributorsRes.data : [];
const flaggedAccountsRes = await getFlaggedAccounts();
const flaggedAccounts: FlaggedAccountRow[] = isOk(flaggedAccountsRes)
? flaggedAccountsRes.data
: [];

return (
<div className="min-h-screen bg-zinc-950 px-6 py-12 text-white">
Expand Down Expand Up @@ -159,6 +165,50 @@ export default async function MaintainerPage({
<p className="mb-4 text-xs text-zinc-500">
{activeInstall.accountLogin} ({activeInstall.permissionLevel.replace('_', ' ')})
</p>
{flaggedAccounts.length > 0 && (
<section className="mb-8 rounded-2xl border border-amber-900/60 bg-amber-950/20 p-5">
<div className="mb-4 flex items-center justify-between gap-3">
<div>
<h2 className="text-sm font-semibold text-amber-100">Suspicious XP Signals</h2>
<p className="mt-1 text-xs text-amber-200/70">
Daily detector output for maintainer review.
</p>
</div>
<span className="rounded-full bg-amber-900/50 px-2 py-1 text-xs text-amber-100">
{flaggedAccounts.length} open
</span>
</div>

<div className="grid gap-3 md:grid-cols-2">
{flaggedAccounts.map((flag) => (
<div key={flag.id} className="rounded-lg border border-amber-900/50 p-3">
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm text-amber-50">@{flag.githubHandle}</p>
<p className="mt-1 text-xs text-amber-200/70">
Level {flag.level} · {flag.xp} XP
</p>
</div>
<span
className={`rounded-full px-2 py-0.5 text-xs ${
flag.severity === 'high'
? 'bg-red-900/50 text-red-200'
: 'bg-amber-900/50 text-amber-100'
}`}
>
{flag.severity}
</span>
</div>
<p className="mt-3 text-sm text-amber-100">{formatFlagReason(flag.reason)}</p>
<p className="mt-1 text-xs text-amber-200/70">{flag.summary}</p>
<p className="mt-2 text-xs text-amber-200/50">
Detected {relativeTime(flag.detectedAt)}
</p>
</div>
))}
</div>
</section>
)}
<div className="mb-8 grid gap-6 lg:grid-cols-3">
<section className="rounded-2xl border border-zinc-800 bg-zinc-900 p-5">
<h2 className="mb-4 text-sm font-semibold text-white">Repository Health</h2>
Expand Down Expand Up @@ -373,6 +423,16 @@ function NoInstalls() {
);
}

function formatFlagReason(reason: string) {
const labels: Record<string, string> = {
daily_xp_event_spike: 'Daily XP event spike',
rapid_merge_spike: 'Rapid merge spike',
reviewer_approval_concentration: 'Reviewer approval concentration',
};

return labels[reason] ?? 'Suspicious activity';
}

function NotConfigured() {
return (
<div className="min-h-screen px-6 py-20 text-white">
Expand Down
109 changes: 109 additions & 0 deletions src/app/actions/maintainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ export type ContributorRow = {
level: number;
};

export type FlaggedAccountRow = {
id: number;
githubHandle: string;
xp: number;
level: number;
reason: string;
severity: 'medium' | 'high';
detectedAt: string;
summary: string;
count: number;
};

const ISSUE_BUCKETS = new Set<IssueTriageBucket>([
'needs-triage',
'in-progress',
Expand Down Expand Up @@ -821,3 +833,100 @@ export async function getTopContributors(): Promise<Result<ContributorRow[]>> {
})),
);
}

export async function getFlaggedAccounts(): Promise<Result<FlaggedAccountRow[]>> {
const sb = getServerSupabase();

if (!sb) {
return err('not_configured', 'auth not configured');
}

const service = getServiceSupabase();

if (!service) {
return err('not_configured', 'service role missing');
}

const {
data: { user },
} = await sb.auth.getUser();

if (!user) {
return err('not_authenticated', 'sign in first');
}

await rateLimit({
namespace: 'maintainer',
key: user.id,
limit: 30,
windowSec: 60,
});

if (!(await isUserMaintainer(user.id))) {
return err('not_authorised', 'not a maintainer');
}

const { data: flags, error } = await service
.from('flagged_accounts')
.select('id, user_id, reason, severity, evidence, detected_at')
.eq('status', 'open')
.order('detected_at', { ascending: false })
.limit(10);

if (error) {
return err('query_failed', error.message);
}

const userIds = Array.from(new Set((flags ?? []).map((flag) => flag.user_id).filter(Boolean)));
const { data: profiles, error: profilesError } =
userIds.length > 0
? await service.from('profiles').select('id, github_handle, xp, level').in('id', userIds)
: { data: [], error: null };

if (profilesError) {
return err('query_failed', profilesError.message);
}

const profilesById = new Map(
(profiles ?? []).map((profile) => [
profile.id,
{
githubHandle: profile.github_handle ?? 'unknown',
xp: profile.xp ?? 0,
level: profile.level ?? 0,
},
]),
);

return ok(
(flags ?? []).map((flag) => {
const profile = profilesById.get(flag.user_id ?? '');
const evidence = readFlagEvidence(flag.evidence);

return {
id: flag.id,
githubHandle: profile?.githubHandle ?? 'unknown',
xp: profile?.xp ?? 0,
level: profile?.level ?? 0,
reason: flag.reason,
severity: flag.severity === 'high' ? 'high' : 'medium',
detectedAt: flag.detected_at,
summary: evidence.summary,
count: evidence.count,
};
}),
);
}

function readFlagEvidence(evidence: unknown) {
if (!evidence || typeof evidence !== 'object') {
return { summary: 'Suspicious activity pattern detected.', count: 0 };
}

const record = evidence as Record<string, unknown>;
return {
summary:
typeof record.summary === 'string' ? record.summary : 'Suspicious activity pattern detected.',
count: typeof record.count === 'number' ? record.count : 0,
};
}
2 changes: 2 additions & 0 deletions src/app/api/inngest/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
streakDetect,
recsExpire,
activityLogCleanup,
flagSuspiciousXpAccounts,
autoUnclaimStale,
} from '@/inngest/functions/maintenance';
import { githubStatsSync } from '@/inngest/functions/github-stats-sync';
Expand All @@ -45,6 +46,7 @@ export const { GET, POST, PUT } = serve({
streakDetect,
recsExpire,
activityLogCleanup,
flagSuspiciousXpAccounts,
autoUnclaimStale,
githubStatsSync,
mentorPostComment,
Expand Down
Loading