sync: claim removal fix, auto-crawl email, progress bar, search optimization

- Fix claim removal request flow
- Add email notification on auto-crawl skill discovery
- Add progress bar component
- Optimize Meilisearch sync and search indexing
- Improve skill parser and queries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-22 04:58:01 +03:30
parent b05cb21a65
commit 0713607693
15 changed files with 352 additions and 14 deletions

View File

@@ -64,6 +64,8 @@ export default async function ClaimPage({
success: {
title: t('success.title'),
description: t('success.description'),
pendingTitle: t('success.pendingTitle'),
pendingDescription: t('success.pendingDescription'),
viewRequests: t('success.viewRequests'),
},
addSuccess: {

View File

@@ -6,6 +6,7 @@ import { locales, localeDirection, type Locale } from '@/i18n';
import { Providers } from '../providers';
import { Suspense } from 'react';
import { QueryNotification } from '@/components/QueryNotification';
import { ProgressBar } from '@/components/ProgressBar';
import '../globals.css';
export async function generateMetadata({
@@ -70,6 +71,7 @@ export default async function LocaleLayout({
<Suspense fallback={null}>
<QueryNotification />
</Suspense>
<ProgressBar />
{children}
</NextIntlClientProvider>
</Providers>

View File

@@ -1,7 +1,7 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { createDb, addRequestQueries, userQueries } from '@skillhub/db';
import { createDb, addRequestQueries, discoveredRepoQueries, userQueries } from '@skillhub/db';
import { sanitizeReason } from '@/lib/sanitize';
import { sendClaimSubmittedEmail } from '@/lib/email';
@@ -389,6 +389,26 @@ export async function POST(request: NextRequest) {
hasSkillMd: validation.hasSkillMd,
});
// Auto-approve and queue for crawling if SKILL.md found
if (validation.hasSkillMd && validation.skillPaths.length > 0) {
await addRequestQueries.updateStatus(db, requestId, {
status: 'approved',
});
// Queue repo for indexer crawling via discovered_repos
try {
await discoveredRepoQueries.upsert(db, {
id: `${parsed.owner}/${parsed.repo}`,
owner: parsed.owner,
repo: parsed.repo,
discoveredVia: 'add-request',
githubStars: 0,
});
} catch (err) {
console.warn('[Claim] Failed to queue repo for crawling:', err);
}
}
// Build appropriate response message
let message: string;
if (validation.skillPaths.length > 1) {

View File

@@ -124,6 +124,7 @@ export async function POST(request: NextRequest) {
// Check repository ownership using public API (no token needed for public repos)
let isOwner = false;
let repoDeleted = false;
let githubError: string | null = null;
try {
@@ -144,7 +145,7 @@ export async function POST(request: NextRequest) {
isOwner = true;
}
} else if (repoResponse.status === 404) {
githubError = 'Repository not found on GitHub';
repoDeleted = true;
} else if (repoResponse.status === 403) {
githubError = 'GitHub API rate limit exceeded';
}
@@ -160,23 +161,34 @@ export async function POST(request: NextRequest) {
);
}
if (!isOwner) {
if (!isOwner && !repoDeleted) {
return NextResponse.json(
{ error: 'You are not the owner of this repository', code: 'NOT_OWNER' },
{ status: 403 }
);
}
// Create the removal request (auto-approved since owner is verified)
// Create the removal request
const sanitizedReason = reason ? sanitizeReason(reason) : 'No reason provided';
const requestId = await removalRequestQueries.create(db, {
userId: dbUser.id,
skillId,
reason: sanitizedReason,
verifiedOwner: true,
verifiedOwner: !repoDeleted,
});
// Auto-approve: Block the skill from being re-indexed
if (repoDeleted) {
// Repo not found — submit for admin review instead of auto-approving
return NextResponse.json({
success: true,
requestId,
pending: true,
blocked: false,
message: 'Repository not found on GitHub. Your removal request has been submitted for admin review.',
});
}
// Owner verified — auto-approve: Block the skill from being re-indexed
await skillQueries.block(db, skillId);
// Update the request status to approved

View File

@@ -55,6 +55,8 @@ interface ClaimFormProps {
success: {
title: string;
description: string;
pendingTitle: string;
pendingDescription: string;
viewRequests: string;
};
addSuccess: {
@@ -161,6 +163,7 @@ export function ClaimForm({ translations }: ClaimFormProps) {
const [isSubmittingRemove, setIsSubmittingRemove] = useState(false);
const [removeError, setRemoveError] = useState('');
const [removeSuccess, setRemoveSuccess] = useState(false);
const [removePending, setRemovePending] = useState(false);
const [removalRequests, setRemovalRequests] = useState<RemovalRequest[]>([]);
const [loadingRemovalRequests, setLoadingRemovalRequests] = useState(false);
@@ -267,6 +270,7 @@ export function ClaimForm({ translations }: ClaimFormProps) {
}
setRemoveSuccess(true);
setRemovePending(data.pending || false);
setSkillId('');
setRemoveReason('');
fetchRemovalRequests();
@@ -438,11 +442,13 @@ export function ClaimForm({ translations }: ClaimFormProps) {
<div className="card p-8 text-center">
<CheckCircle className="w-12 h-12 mx-auto mb-4 text-success" />
<h2 className="text-xl font-semibold text-text-primary mb-2">
{translations.success.title}
{removePending ? translations.success.pendingTitle : translations.success.title}
</h2>
<p className="text-text-secondary mb-6">{translations.success.description}</p>
<p className="text-text-secondary mb-6">
{removePending ? translations.success.pendingDescription : translations.success.description}
</p>
<button
onClick={() => setRemoveSuccess(false)}
onClick={() => { setRemoveSuccess(false); setRemovePending(false); }}
className="btn-secondary"
>
{translations.success.viewRequests}

View File

@@ -0,0 +1,14 @@
'use client';
import { AppProgressBar } from 'next-nprogress-bar';
export function ProgressBar() {
return (
<AppProgressBar
height="3px"
color="#0284c7"
options={{ showSpinner: false }}
shallowRouting
/>
);
}

View File

@@ -719,6 +719,8 @@
"success": {
"title": "Skill Blocked",
"description": "Your skill has been blocked from indexing. It will no longer appear in SkillHub.",
"pendingTitle": "Request Submitted",
"pendingDescription": "The repository was not found on GitHub. Your removal request has been submitted for admin review.",
"viewRequests": "View My Requests"
},
"addSuccess": {

View File

@@ -719,6 +719,8 @@
"success": {
"title": "مهارت مسدود شد",
"description": "مهارت شما از ایندکس شدن مسدود شد. دیگر در SkillHub نمایش داده نخواهد شد.",
"pendingTitle": "درخواست ثبت شد",
"pendingDescription": "مخزن در GitHub یافت نشد. درخواست حذف شما برای بررسی ادمین ثبت شد.",
"viewRequests": "مشاهده درخواست‌های من"
},
"addSuccess": {

View File

@@ -27,6 +27,7 @@
"next": "^15.0.0",
"next-auth": "5.0.0-beta.30",
"next-intl": "^3.4.0",
"next-nprogress-bar": "^2.4.7",
"next-themes": "^0.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",