sync: malware page, security alerts, review diagnostics, skill detail improvements
- Add malware security advisory page and SecurityAlertBanner component - Add review diagnostics API and reviewed stats page - Improve skill detail page with better scoring display and metadata - Update review API endpoints with enhanced filtering and stats - Add skill file serving improvements and cache enhancements - Remove legacy curation scripts (moved to internal tooling) - Update CLI search with score filtering support Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
149
apps/web/app/[locale]/malware/page.tsx
Normal file
149
apps/web/app/[locale]/malware/page.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import Link from 'next/link';
|
||||
import { Header } from '@/components/Header';
|
||||
import { Footer } from '@/components/Footer';
|
||||
import { createDb, skillReviewQueries } from '@skillhub/db';
|
||||
import { getPageAlternates } from '@/lib/seo';
|
||||
import { getOrSetCache, cacheKeys, cacheTTL } from '@/lib/cache';
|
||||
import { ShieldAlert, Ban, ExternalLink } from 'lucide-react';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
async function getMalwareData(page: number, limit: number) {
|
||||
try {
|
||||
return await getOrSetCache(cacheKeys.malwarePage(page), cacheTTL.malware, async () => {
|
||||
const db = createDb();
|
||||
const offset = (page - 1) * limit;
|
||||
const [skills, total] = await Promise.all([
|
||||
skillReviewQueries.getMaliciousSkills(db, limit, offset),
|
||||
skillReviewQueries.countMaliciousSkills(db),
|
||||
]);
|
||||
return { skills, total };
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching malware skills:', error);
|
||||
return { skills: [], total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
alternates: getPageAlternates(locale, '/malware'),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function MalwarePage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
const searchParamsResolved = await searchParams;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations('malwarePage');
|
||||
const tMalicious = await getTranslations('malicious');
|
||||
|
||||
const limit = 20;
|
||||
const page = parseInt(searchParamsResolved.page || '1');
|
||||
const { skills: malwareSkills } = await getMalwareData(page, limit);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
{/* Header */}
|
||||
<section className="section-header bg-gradient-subtle">
|
||||
<div className="container-main text-center">
|
||||
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-error/10 text-error mb-4">
|
||||
<ShieldAlert className="w-7 h-7" />
|
||||
</div>
|
||||
<h1 className="hero-title mb-4">{t('title')}</h1>
|
||||
<p className="hero-subtitle max-w-2xl mx-auto">{t('subtitle')}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="section bg-surface">
|
||||
<div className="container-main max-w-4xl">
|
||||
{malwareSkills.length === 0 ? (
|
||||
<p className="text-center text-text-muted py-12">{t('noResults')}</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{malwareSkills.map((skill) => (
|
||||
<div
|
||||
key={skill.id}
|
||||
className="card p-5 border-error/20 bg-error/[0.02]"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||
<Link
|
||||
href={`/${locale}/skill/${skill.id}`}
|
||||
className="text-lg font-semibold text-text-primary hover:text-primary-600 transition-colors truncate"
|
||||
>
|
||||
{skill.name || skill.id}
|
||||
</Link>
|
||||
<span className="flex items-center gap-1 px-1.5 py-0.5 text-xs font-bold rounded border text-error bg-error/10 border-error/20 shrink-0">
|
||||
<ShieldAlert className="w-3 h-3" />
|
||||
{tMalicious('badge')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-text-muted mb-2 font-mono">
|
||||
{skill.githubOwner}/{skill.githubRepo}
|
||||
</p>
|
||||
|
||||
{skill.description && (
|
||||
<p className="text-sm text-text-secondary line-clamp-2 mb-3">
|
||||
{skill.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-text-muted flex-wrap">
|
||||
<span className="flex items-center gap-1">
|
||||
<Ban className="w-3.5 h-3.5 text-error" />
|
||||
{t('downloadBlocked')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Ban className="w-3.5 h-3.5 text-error" />
|
||||
{t('installBlocked')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={`https://github.com/${skill.githubOwner}/${skill.githubRepo}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm text-text-muted hover:text-text-primary transition-colors shrink-0"
|
||||
>
|
||||
GitHub <ExternalLink className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Back to stats link */}
|
||||
<div className="text-center mt-8">
|
||||
<Link
|
||||
href={`/${locale}/reviewed/stats`}
|
||||
className="btn-secondary"
|
||||
>
|
||||
← {locale === 'fa' ? 'بازگشت به آمار بررسیها' : 'Back to Review Statistics'}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user