sync: stale skill detection, sentry filtering, claim removal, review improvements

Stale Detection:
- Detect skills removed/moved from GitHub (3 consecutive 404s threshold)
- Hide stale skills from browse/search, show warning banner on detail page
- Serve cached files with isStale flag when GitHub returns 404
- Add stale-check crawler command for batch verification
- CLI shows warning when installing stale skills from cache

Sentry & Error Handling:
- Filter browser extension errors and add denyUrls
- Anti-inflation measures and sentinel recalibration for curation

Claim & Removal:
- Enhanced ClaimForm with repo-level removal support
- Add repo-removal-request API endpoint with tests
- Improved owner page with bilingual content

Review Pipeline:
- Review version and reviewer tracking in submit API
- Source format filter for pending reviews
- Updated review tests

Other:
- Updated i18n strings (en/fa)
- BrowseFilters improvements
- Dockerfile updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-03-12 05:59:26 +03:30
parent 447f9eff59
commit 4212b5ba47
31 changed files with 1590 additions and 58 deletions

View File

@@ -186,7 +186,7 @@ export default async function BrowsePage({ params, searchParams }: BrowsePagePro
const emptyStateTranslations = {
noResults: t('noResults') || 'No skills found',
noResultsWithQuery: t('noResultsWithQuery') || 'No results for "{query}"',
noResultsWithQuery: t('noResultsWithQuery', { query: searchParamsResolved.q || '' }),
tryDifferent: t('emptyState.tryDifferent') || 'Try different search terms or adjust your filters',
clearFilters: t('emptyState.clearFilters') || 'Clear filters',
browseAll: t('emptyState.browseAll') || 'Browse Featured Skills',

View File

@@ -42,6 +42,7 @@ export default async function ClaimPage({
tabs: {
remove: t('tabs.remove'),
add: t('tabs.add'),
removeRepo: t('tabs.removeRepo'),
},
form: {
skillId: t('form.skillId'),
@@ -78,6 +79,27 @@ export default async function ClaimPage({
foundSkillsIn: t('addSuccess.foundSkillsIn'),
root: t('addSuccess.root'),
andMore: t.raw('addSuccess.andMore') as string,
reEnabledTitle: t('addSuccess.reEnabledTitle'),
reEnabledDescription: t('addSuccess.reEnabledDescription'),
},
removeRepoForm: {
repoUrl: t('removeRepoForm.repoUrl'),
repoUrlPlaceholder: t('removeRepoForm.repoUrlPlaceholder'),
repoUrlHelp: t('removeRepoForm.repoUrlHelp'),
reason: t('removeRepoForm.reason'),
reasonPlaceholder: t('removeRepoForm.reasonPlaceholder'),
submit: t('removeRepoForm.submit'),
submitting: t('removeRepoForm.submitting'),
},
removeRepoSuccess: {
title: t('removeRepoSuccess.title'),
description: t.raw('removeRepoSuccess.description') as string,
descriptionZero: t('removeRepoSuccess.descriptionZero'),
},
removeRepoError: {
notOwner: t('removeRepoError.notOwner'),
invalidRepo: t('removeRepoError.invalidRepo'),
parseError: t('removeRepoError.parseError'),
},
error: {
notOwner: t('error.notOwner'),
@@ -90,6 +112,7 @@ export default async function ClaimPage({
rateLimitExceeded: t('error.rateLimitExceeded'),
networkTimeout: t('error.networkTimeout'),
generic: t('error.generic'),
repoBlockedByOwner: t('error.repoBlockedByOwner'),
},
myRequests: {
title: t('myRequests.title'),

View File

@@ -10,6 +10,7 @@ import Link from 'next/link';
import type { Metadata } from 'next';
import { getPageAlternates } from '@/lib/seo';
import { getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
import { auth } from '@/lib/auth';
export const dynamic = 'force-dynamic';
@@ -49,6 +50,8 @@ export default async function OwnerPage({ params, searchParams }: OwnerPageProps
const activeRepo = searchParamsResolved.repo || '';
const db = createDb();
const session = await auth();
const isOwner = session?.user?.username?.toLowerCase() === username.toLowerCase();
// Fetch stats and repo list with caching (30 min TTL), count is dynamic per filter
const [stats, totalSkills, ownerRepos] = await Promise.all([
@@ -256,16 +259,30 @@ export default async function OwnerPage({ params, searchParams }: OwnerPageProps
</div>
</div>
{/* Claim CTA - subtle inline hint */}
<div className="flex items-center gap-2 mb-6 text-xs text-text-muted">
<span>{t('claimCta')}</span>
<Link
href={`/${locale}/claim`}
className="text-primary hover:underline font-medium whitespace-nowrap"
>
{t('claimButton')}
</Link>
</div>
{/* Claim CTA */}
{isOwner ? (
<div className="flex items-center gap-3 mb-6 px-4 py-3 bg-primary/5 border border-primary/20 rounded-lg">
<div className="flex-1 text-sm text-text-secondary">
{t('ownerManageCta')}
</div>
<Link
href={`/${locale}/claim`}
className="text-sm text-primary hover:underline font-medium whitespace-nowrap"
>
{t('ownerManageButton')}
</Link>
</div>
) : (
<div className="flex items-center gap-2 mb-6 text-xs text-text-muted">
<span>{t('claimCta')}</span>
<Link
href={`/${locale}/claim`}
className="text-primary hover:underline font-medium whitespace-nowrap"
>
{t('claimButton')}
</Link>
</div>
)}
{/* Repos + Skills */}
{repos.map((repo) => (

View File

@@ -21,7 +21,7 @@ async function getStats() {
const db = createDb();
// Browse-ready filter: exclude duplicates (matches browseReadyFilter in queries.ts)
const browseReady = sql`${skills.isDuplicate} = false`;
const browseReady = sql`${skills.isDuplicate} = false AND ${skills.isStale} = false`;
// Run all independent count queries in parallel
const [skillsResult, downloadsResult, categories, contributorsResult, totalIndexedResult] = await Promise.all([

View File

@@ -339,6 +339,33 @@ export default async function SkillPage({ params }: SkillPageProps) {
</div>
)}
{/* Stale Skill Warning Banner */}
{dbSkill.isStale && (
<div className="bg-warning/10 border-b border-warning/20">
<div className="container-main py-3">
<div className="flex items-start gap-3 text-sm">
<span className="text-warning text-lg flex-shrink-0"></span>
<div>
<p className="text-warning-foreground dark:text-warning" dir="auto">
{isRTL
? 'این مهارت ممکن است از مخزن GitHub اصلی حذف یا جابجا شده باشد. فایل‌ها از حافظه پنهان SkillHub ارائه می‌شوند و ممکن است قدیمی باشند.'
: 'This skill may have been removed or moved from its GitHub repository. Files are served from the SkillHub cache and may be outdated.'}
</p>
<a
href={`https://github.com/${skill.author}/${skill.repo}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 mt-1 text-primary-600 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 font-medium"
>
{isRTL ? 'بررسی مخزن GitHub' : 'Check GitHub repository'}
<ExternalLink className="w-3 h-3" />
</a>
</div>
</div>
</div>
</div>
)}
{/* Stats Bar */}
<div className="bg-surface-elevated border-b border-border">
<div className="container-main">