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.
|
View all organizations, check billing status, and manually grant plans.
|
||||||
</p>
|
</p>
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,3 +60,18 @@ export async function grantPlan(orgId: string, params: GrantPlanParams): Promise
|
|||||||
body: JSON.stringify(params),
|
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