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:
@@ -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',
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user