diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 73ebce0..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(php artisan test:*)", - "Bash(php artisan:*)", - "Bash(./vendor/bin/phpunit:*)", - "Bash(cat:*)", - "Bash(npm run build:*)", - "WebFetch(domain:dev-tools.online)", - "WebSearch", - "Bash(curl:*)", - "Bash(git add:*)", - "Bash(git cherry-pick:*)" - ], - "deny": [], - "ask": [] - } -} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 10c5af9..4bf060d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,35 +4,91 @@ on: release: types: [published] workflow_dispatch: + inputs: + force_full: + description: 'Force full deploy (rebuild and upload vendor + assets)' + type: boolean + default: false jobs: deploy: runs-on: ubuntu-latest name: Deploy via SFTP + env: + # Required at build time only: composer install runs package:discover, + # which boots the visitor-tracker package and trips its dashboard guard + # if no auth method is configured. The runner has no .env, so set this + # here to match prod posture (server's .env handles the actual setting). + VISITOR_TRACKER_ALLOW_UNPROTECTED: true + steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine what changed since last release + id: changes + run: | + set -euo pipefail + + if [[ "${{ github.event.inputs.force_full }}" == "true" ]]; then + echo "Force full deploy requested via workflow_dispatch." + echo "composer_changed=true" >> "$GITHUB_OUTPUT" + echo "assets_changed=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Baseline = previous release tag (skip current HEAD if it IS a tag). + # Falls back to HEAD^ if there's no prior tag in history. + BASE=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || git rev-parse HEAD^) + echo "Diff baseline: $BASE" + + CHANGED=$(git diff --name-only "$BASE" HEAD) + echo "Changed files since $BASE:" + echo "$CHANGED" + + # Vendor needs rebuild + upload only when composer dependencies change. + if echo "$CHANGED" | grep -qE '^composer\.(json|lock)$'; then + echo "composer_changed=true" >> "$GITHUB_OUTPUT" + else + echo "composer_changed=false" >> "$GITHUB_OUTPUT" + fi + + # Compiled assets need rebuild + upload when their source or build + # config changes. public/build/ filenames are content-hashed by Vite, + # so leaving stale ones on the server is harmless. + if echo "$CHANGED" | grep -qE '^(package(-lock)?\.json|vite\.config\.js|resources/(css|js)/)'; then + echo "assets_changed=true" >> "$GITHUB_OUTPUT" + else + echo "assets_changed=false" >> "$GITHUB_OUTPUT" + fi - name: Setup PHP + if: steps.changes.outputs.composer_changed == 'true' uses: shivammathur/setup-php@v2 with: php-version: 8.3 extensions: mbstring, xml, ctype, json - name: Install production dependencies + if: steps.changes.outputs.composer_changed == 'true' run: composer install --no-dev --optimize-autoloader --no-interaction - name: Setup Node.js + if: steps.changes.outputs.assets_changed == 'true' uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install npm dependencies + if: steps.changes.outputs.assets_changed == 'true' run: npm ci - name: Build assets + if: steps.changes.outputs.assets_changed == 'true' run: npm run build - name: Prepare production files diff --git a/app/Http/Controllers/ToolController.php b/app/Http/Controllers/ToolController.php index 8e5e20c..d163904 100644 --- a/app/Http/Controllers/ToolController.php +++ b/app/Http/Controllers/ToolController.php @@ -153,12 +153,6 @@ public function index(): View 'route' => 'tools.sort-lines', 'icon' => 'sort', ], - [ - 'name' => 'Visitor Tracker', - 'description' => 'Server-side visitor analytics for Laravel applications', - 'route' => 'tools.visitor-tracker', - 'icon' => 'chart', - ], ]; return view('home', compact('tools')); @@ -283,31 +277,4 @@ public function sortLines(): View { return view('tools.sort-lines'); } - - public function visitorTracker(): View - { - $stats = app(\Ghdj\VisitorTracker\Services\StatisticsService::class); - $parser = new \Ghdj\VisitorTracker\Services\UserAgentParser(); - $botDetector = new \Ghdj\VisitorTracker\Services\BotDetector(); - - $userAgent = request()->userAgent(); - $parsedUA = $parser->parse($userAgent); - $isBot = $botDetector->isBot($userAgent); - $botName = $isBot ? $botDetector->getBotName($userAgent) : null; - $botCategory = $isBot ? $botDetector->getBotCategory($userAgent) : null; - - return view('tools.visitor-tracker', [ - 'summary' => $stats->summary(), - 'browsers' => $stats->browserStats(5), - 'platforms' => $stats->platformStats(5), - 'devices' => $stats->deviceStats(), - 'topPages' => $stats->mostVisitedPages(5), - 'userAgent' => $userAgent, - 'parsedUA' => $parsedUA, - 'isBot' => $isBot, - 'botName' => $botName, - 'botCategory' => $botCategory, - 'visitorIp' => request()->ip(), - ]); - } } diff --git a/resources/views/tools/visitor-tracker.blade.php b/resources/views/tools/visitor-tracker.blade.php deleted file mode 100644 index 1f15265..0000000 --- a/resources/views/tools/visitor-tracker.blade.php +++ /dev/null @@ -1,299 +0,0 @@ -@extends('layouts.app') - -@section('title', 'Laravel Visitor Tracker - Server-Side Analytics Demo | Dev Tools') -@section('meta_description', 'Laravel Visitor Tracker - A server-side analytics package for Laravel with bot detection, device parsing, and GDPR compliance. Unblockable by ad blockers.') -@section('meta_keywords', 'laravel visitor tracker, laravel analytics, server-side tracking, bot detection, user agent parser, gdpr analytics, laravel package') - -@push('schema') - -@endpush - -@section('content') -
-
-
-

Laravel Visitor Tracker

-

Server-side analytics for Laravel - Unblockable by ad blockers

-
- ← Back -
- - -
- - - GitHub - - - - Packagist - -
- - -
-

Live Statistics from dev-tools.online

-

Real data collected by this package running on this site:

- -
-
-

{{ number_format($summary['total_visitors']) }}

-

Total Visitors

-
-
-

{{ number_format($summary['total_page_views']) }}

-

Page Views

-
-
-

{{ number_format($summary['online_visitors']) }}

-

Online Now

-
-
-

{{ number_format($summary['today_visitors']) }}

-

Today

-
-
-
- -
- -
-

Your Browser Detected

-

This is what the package detects about your current visit:

- -
-
- Browser - {{ $parsedUA['browser'] ?? 'Unknown' }} {{ $parsedUA['browser_version'] ?? '' }} -
-
- Platform - {{ $parsedUA['platform'] ?? 'Unknown' }} {{ $parsedUA['platform_version'] ?? '' }} -
-
- Device Type - {{ $parsedUA['device_type'] ?? 'Unknown' }} -
-
- Is Bot? - - {{ $isBot ? 'Yes' . ($botName ? " ({$botName})" : '') : 'No (Human)' }} - -
- @if($botCategory) -
- Bot Category - {{ str_replace('_', ' ', $botCategory) }} -
- @endif -
- IP Address - {{ $visitorIp }} -
-
- -
-

User Agent:

-

{{ $userAgent }}

-
-
- - -
-
-

Browser Stats

-
- @php $totalBrowsers = $browsers->sum('count'); @endphp - @forelse($browsers as $browser) -
-
- {{ $browser->browser ?? 'Unknown' }} - {{ $totalBrowsers > 0 ? round($browser->count / $totalBrowsers * 100, 1) : 0 }}% -
-
-
-
-
- @empty -

No data yet

- @endforelse -
-
- -
-

Device Types

-
- @php - $totalDevices = $devices->sum('count'); - $deviceColors = ['desktop' => 'bg-purple-600', 'mobile' => 'bg-green-600', 'tablet' => 'bg-yellow-500']; - @endphp - @forelse($devices as $device) -
-
- {{ $device->device_type ?? 'Unknown' }} - {{ number_format($device->count) }} ({{ $totalDevices > 0 ? round($device->count / $totalDevices * 100, 1) : 0 }}%) -
-
-
-
-
- @empty -

No data yet

- @endforelse -
-
-
-
- - - @if($topPages->isNotEmpty()) -
-

Top Pages

-
- - - - - - - - - - @foreach($topPages as $page) - - - - - - @endforeach - -
PathViewsUnique
/{{ $page->path }}{{ number_format($page->visits) }}{{ number_format($page->unique_visitors) }}
-
-
- @endif - - -
-

Package Features

-
-
-
- -
-
-

Unblockable

-

Server-side tracking can't be blocked by ad blockers

-
-
-
-
- -
-
-

Native Detection

-

100+ bot patterns, browser/device parsing - no dependencies

-
-
-
-
- -
-
-

GDPR Compliant

-

GDPR Safe Mode, IP anonymization, DNT support

-
-
-
-
- -
-
-

Zero Dependencies

-

Only uses Laravel's built-in features

-
-
-
-
- -
-
-

Geolocation

-

IP-based location via free APIs (optional)

-
-
-
-
- -
-
-

Built-in Dashboard

-

Tailwind CSS dashboard with token auth

-
-
-
-
- - -
-

Quick Installation

-
-
-

1. Install via Composer:

-
composer require ghdj/laravel-visitor-tracker
-
-
-

2. Publish config and run migrations:

-
php artisan vendor:publish --tag="visitor-tracker-config"
-php artisan migrate
-
-
-

3. Add middleware to your routes:

-
// In bootstrap/app.php (Laravel 11+)
-->withMiddleware(function (Middleware $middleware) {
-    $middleware->web(append: [
-        \Ghdj\VisitorTracker\Middleware\TrackVisitor::class,
-    ]);
-})
-
-
-

4. Use it:

-
use Ghdj\VisitorTracker\Facades\VisitorTracker;
-
-// Get statistics
-$stats = VisitorTracker::stats()->summary();
-$online = VisitorTracker::stats()->onlineVisitors();
-$topPages = VisitorTracker::stats()->mostVisitedPages(10);
-
-
-
- - -
-

Laravel Visitor Tracker v1.0.0 - MIT License

-

- Documentation - · - Packagist -

-
-
-@endsection diff --git a/routes/web.php b/routes/web.php index a55ca79..de71750 100644 --- a/routes/web.php +++ b/routes/web.php @@ -30,7 +30,6 @@ Route::get('/timestamp', [ToolController::class, 'timestamp'])->name('timestamp'); Route::get('/diff', [ToolController::class, 'diff'])->name('diff'); Route::get('/sort-lines', [ToolController::class, 'sortLines'])->name('sort-lines'); - Route::get('/visitor-tracker', [ToolController::class, 'visitorTracker'])->name('visitor-tracker'); }); // Static Pages @@ -65,7 +64,6 @@ ['loc' => route('tools.timestamp'), 'priority' => '0.8', 'changefreq' => 'monthly'], ['loc' => route('tools.diff'), 'priority' => '0.8', 'changefreq' => 'monthly'], ['loc' => route('tools.sort-lines'), 'priority' => '0.8', 'changefreq' => 'monthly'], - ['loc' => route('tools.visitor-tracker'), 'priority' => '0.9', 'changefreq' => 'monthly'], ['loc' => route('about'), 'priority' => '0.5', 'changefreq' => 'monthly'], ['loc' => route('privacy'), 'priority' => '0.3', 'changefreq' => 'yearly'], ];