mad-lawsuit/app/Http/Controllers/Admin/DocumentAccessController.php
friday-bot 04bd71aaaa Add document access statistics to admin dashboard
Records every successful document download and surfaces the counts in the
admin area, so the docket owner can see which filings are being pulled and
how often.

- document_accesses table + DocumentAccess model
- DocumentController@download records each successful download with IP and
  user agent
- documents:import-access-log backfills history from an archived nginx log
- Dashboard gains a 60-day downloads card and a most-requested list
- New Document Access page with a date-range filter and per-day trend

Client IPs were only captured from 2026-07-30, when the reverse proxy chain
started forwarding X-Forwarded-For correctly. Rows imported from the old
nginx log therefore have no IP, and the UI labels those periods as download
counts rather than unique visitors instead of showing a misleading number.
2026-07-30 13:14:01 -06:00

120 lines
4 KiB
PHP

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\DocumentAccess;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
class DocumentAccessController extends Controller
{
/**
* The date real client IPs started being recorded. Before this, the nginx
* log stored only the docker gateway address, so unique-visitor counts are
* not available for earlier periods and must not be implied.
*/
private const IP_DATA_STARTS = '2026-07-30';
/**
* Per-document access counts over a selectable date range.
*/
public function index(Request $request): Response
{
$validated = $request->validate([
'from' => ['nullable', 'date'],
'to' => ['nullable', 'date'],
'include_bots' => ['nullable', 'boolean'],
]);
$from = isset($validated['from'])
? Carbon::parse($validated['from'])->startOfDay()
: Carbon::now()->subDays(60)->startOfDay();
$to = isset($validated['to'])
? Carbon::parse($validated['to'])->endOfDay()
: Carbon::now()->endOfDay();
$includeBots = (bool) ($validated['include_bots'] ?? false);
$documents = $this->documentCounts($from, $to, $includeBots);
return Inertia::render('Admin/DocumentAccess', [
'documents' => $documents,
'daily' => $this->dailyCounts($from, $to, $includeBots),
'filters' => [
'from' => $from->toDateString(),
'to' => $to->toDateString(),
'include_bots' => $includeBots,
],
'totals' => [
'downloads' => $documents->sum('downloads'),
'documents_touched' => $documents->count(),
],
// Drives the caveat shown in the UI.
'ip_data_starts' => self::IP_DATA_STARTS,
'range_predates_ip_data' => $from->lt(Carbon::parse(self::IP_DATA_STARTS)),
]);
}
/**
* Download counts per document, plus unique visitors where the underlying
* rows actually carry an IP.
*/
private function documentCounts(Carbon $from, Carbon $to, bool $includeBots)
{
$query = DocumentAccess::query()
->join('documents', 'documents.id', '=', 'document_accesses.document_id')
->whereBetween('document_accesses.accessed_at', [$from, $to]);
if (! $includeBots) {
$query->excludingBots();
}
return $query
->groupBy('documents.id', 'documents.title')
->orderByDesc('downloads')
->get([
'documents.id',
'documents.title',
DB::raw('count(*) as downloads'),
DB::raw('count(distinct document_accesses.ip_address) as unique_visitors'),
DB::raw('max(document_accesses.accessed_at) as last_accessed'),
])
->map(fn ($row) => [
'id' => $row->id,
'title' => $row->title,
'downloads' => (int) $row->downloads,
'unique_visitors' => (int) $row->unique_visitors,
'last_accessed' => Carbon::parse($row->last_accessed)->format('m/d/Y'),
]);
}
/**
* Downloads per day, for the trend chart.
*/
private function dailyCounts(Carbon $from, Carbon $to, bool $includeBots)
{
$query = DocumentAccess::query()
->whereBetween('accessed_at', [$from, $to]);
if (! $includeBots) {
$query->excludingBots();
}
return $query
->groupBy('day')
->orderBy('day')
->get([
DB::raw('date(accessed_at) as day'),
DB::raw('count(*) as downloads'),
])
->map(fn ($row) => [
'day' => Carbon::parse($row->day)->format('m/d'),
'downloads' => (int) $row->downloads,
]);
}
}