feat: add filtered traffic page to admin dashboard
Add admin page at /admin/filtered-traffic showing domains blocked by the referrer spam filter with reason badges and date range selector. Helps operators monitor spam filtering and catch false positives.
This commit is contained in:
91
app/admin/filtered-traffic/page.tsx
Normal file
91
app/admin/filtered-traffic/page.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { LoadingOverlay } from '@ciphera-net/ui'
|
||||
import { getFilteredReferrers, FilteredReferrer } from '@/lib/api/admin'
|
||||
|
||||
export default function FilteredTrafficPage() {
|
||||
const [referrers, setReferrers] = useState<FilteredReferrer[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [days, setDays] = useState(30)
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
const endDate = new Date().toISOString().split('T')[0]
|
||||
const startDate = new Date(Date.now() - days * 86400000).toISOString().split('T')[0]
|
||||
getFilteredReferrers(startDate, endDate)
|
||||
.then(setReferrers)
|
||||
.finally(() => setLoading(false))
|
||||
}, [days])
|
||||
|
||||
if (loading) {
|
||||
return <LoadingOverlay logoSrc="/pulse_icon_no_margins.png" title="Loading filtered traffic..." />
|
||||
}
|
||||
|
||||
const totalBlocked = referrers.reduce((sum, r) => sum + r.count, 0)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white">Filtered Traffic</h2>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1">
|
||||
{totalBlocked.toLocaleString()} spam referrers blocked in the last {days} days
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{[7, 30, 90].map((d) => (
|
||||
<button
|
||||
key={d}
|
||||
onClick={() => setDays(d)}
|
||||
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${
|
||||
days === d
|
||||
? 'bg-neutral-900 text-white dark:bg-white dark:text-neutral-900'
|
||||
: 'bg-neutral-100 text-neutral-600 hover:bg-neutral-200 dark:bg-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-700'
|
||||
}`}
|
||||
>
|
||||
{d}d
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 shadow-sm overflow-hidden">
|
||||
{referrers.length === 0 ? (
|
||||
<div className="p-12 text-center text-neutral-500 dark:text-neutral-400">
|
||||
No filtered referrers in this period
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="border-b border-neutral-200 dark:border-neutral-800">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Domain</th>
|
||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400">Reason</th>
|
||||
<th className="px-4 py-3 font-medium text-neutral-500 dark:text-neutral-400 text-right">Blocked</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-neutral-200 dark:divide-neutral-800">
|
||||
{referrers.map((r) => (
|
||||
<tr key={`${r.domain}-${r.reason}`} className="hover:bg-neutral-50 dark:hover:bg-neutral-900/50">
|
||||
<td className="px-4 py-3 text-neutral-900 dark:text-white font-mono text-xs">{r.domain}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
r.reason === 'blocklist'
|
||||
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
||||
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'
|
||||
}`}>
|
||||
{r.reason}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-neutral-900 dark:text-white tabular-nums">
|
||||
{r.count.toLocaleString()}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,16 @@ export default function AdminDashboard() {
|
||||
View all organizations, check billing status, and manually grant plans.
|
||||
</p>
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/filtered-traffic"
|
||||
className="block transition-transform hover:scale-[1.02] rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 p-6 shadow-sm"
|
||||
>
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">Filtered Traffic</h3>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1">Monitor blocked referrer spam</p>
|
||||
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-4">
|
||||
View domains blocked by the spam filter and check for false positives.
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,3 +60,18 @@ export async function grantPlan(orgId: string, params: GrantPlanParams): Promise
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
}
|
||||
|
||||
export interface FilteredReferrer {
|
||||
domain: string
|
||||
reason: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export async function getFilteredReferrers(startDate?: string, endDate?: string): Promise<FilteredReferrer[]> {
|
||||
const params = new URLSearchParams()
|
||||
if (startDate) params.set('start_date', startDate)
|
||||
if (endDate) params.set('end_date', endDate)
|
||||
const query = params.toString() ? `?${params.toString()}` : ''
|
||||
const data = await authFetch<{ filtered_referrers: FilteredReferrer[] }>(`/api/admin/filtered-referrers${query}`)
|
||||
return data.filtered_referrers || []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user