diff --git a/Xboard/plugins/UserOnlineDevices/Controllers/UserOnlineDevicesController.php b/Xboard/plugins/UserOnlineDevices/Controllers/UserOnlineDevicesController.php index b11dee5..7d390ef 100644 --- a/Xboard/plugins/UserOnlineDevices/Controllers/UserOnlineDevicesController.php +++ b/Xboard/plugins/UserOnlineDevices/Controllers/UserOnlineDevicesController.php @@ -7,6 +7,7 @@ use App\Models\User; use App\Services\DeviceStateService; use Illuminate\Contracts\View\View; use Illuminate\Http\Request; +use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\Facades\URL; @@ -46,11 +47,46 @@ class UserOnlineDevicesController extends PluginController ]); } - public function userStatus(): View + public function adminUsersPage(): View { abort_unless($this->isPluginEnabled(), 404); - return view('UserOnlineDevices::userstatus'); + $securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))); + + return view('UserOnlineDevices::admin-users', [ + 'apiEndpoint' => url('/api/v2/' . $securePath . '/user-online-devices/users'), + 'adminHomeUrl' => url('/' . $securePath), + ]); + } + + public function adminUsers(Request $request) + { + if ($error = $this->beforePluginAction()) { + return $this->fail($error); + } + + $current = max(1, (int) $request->input('current', 1)); + $pageSize = min(100, max(10, (int) $request->input('pageSize', 20))); + $keyword = trim((string) $request->input('keyword', '')); + + $query = User::query() + ->select(['id', 'email', 'last_online_at', 'created_at']); + + if ($keyword !== '') { + $query->where(function ($builder) use ($keyword) { + $builder->where('email', 'like', '%' . $keyword . '%'); + if (is_numeric($keyword)) { + $builder->orWhere('id', (int) $keyword); + } + }); + } + + $users = $query + ->orderByDesc('last_online_at') + ->orderByDesc('id') + ->paginate($pageSize, ['*'], 'page', $current); + + return $this->success($this->buildAdminUsersPayload($users)); } public function panel(Request $request, int $user): View @@ -111,6 +147,43 @@ class UserOnlineDevicesController extends PluginController ]; } + private function buildAdminUsersPayload(LengthAwarePaginator $users): array + { + $items = $users->getCollection(); + $devicesByUser = $this->deviceStateService->getUsersDevices($items->pluck('id')->all()); + + $users->setCollection($items->map(function (User $user) use ($devicesByUser) { + $ips = collect($devicesByUser[$user->id] ?? []) + ->filter(fn ($ip) => is_string($ip) && $ip !== '') + ->unique() + ->values(); + + return [ + 'id' => $user->id, + 'email' => $user->email, + 'online_count' => $ips->count(), + 'online_devices' => $this->formatOnlineDevices($ips), + 'last_online_at' => $user->last_online_at?->timestamp, + 'created_at' => $user->created_at?->timestamp, + ]; + })); + + return [ + 'list' => $users->items(), + 'pagination' => [ + 'current' => $users->currentPage(), + 'pageSize' => $users->perPage(), + 'total' => $users->total(), + 'lastPage' => $users->lastPage(), + ], + 'meta' => [ + 'generated_at' => time(), + 'refresh_interval_seconds' => max(5, (int) $this->getConfig('refresh_interval_seconds', 15)), + 'mask_ip' => (bool) $this->getConfig('mask_ip', false), + ], + ]; + } + private function formatOnlineDevices(Collection $ips): array { return $ips->values()->map(function (string $ip, int $index) { diff --git a/Xboard/plugins/UserOnlineDevices/README.md b/Xboard/plugins/UserOnlineDevices/README.md index f00a5be..4abcf73 100644 --- a/Xboard/plugins/UserOnlineDevices/README.md +++ b/Xboard/plugins/UserOnlineDevices/README.md @@ -1,26 +1,25 @@ # User Online Devices Plugin -This plugin adds a user-facing online device dashboard for Xboard. +This plugin provides online IP monitoring for Xboard users and administrators. ## Features - Shows the current online device count based on Xboard's built-in `DeviceStateService` -- Shows the current online IP list for the logged-in user -- Optionally shows Sanctum login sessions -- Provides an authenticated API endpoint for the frontend to fetch the dashboard URL -- Provides a temporary signed dashboard page that auto-refreshes +- Provides an administrator page to inspect all users' online IP counts +- Provides an administrator API endpoint with paginated user online IP data +- Provides a temporary signed dashboard page for a single user snapshot ## Routes - `GET /api/v1/user-online-devices/summary` - `GET /api/v1/user-online-devices/panel-url` -- `GET /userstatus` -- `GET /user-online-devices/userstatus` +- `GET /api/v2/{secure_path}/user-online-devices/users` +- `GET /{secure_path}/user-online-devices` - `GET /user-online-devices/panel/{user}` (temporary signed URL) ## Notes - This plugin reuses Xboard's existing real-time device state data from Redis. -- In current Xboard releases, plugin development is primarily backend-oriented, so this plugin ships a standalone user-side page instead of patching the compiled SPA bundle directly. +- In current Xboard releases, plugin development is primarily backend-oriented, so this plugin ships standalone pages instead of patching the compiled SPA bundle directly. - The "online device" count is effectively the number of unique online IPs reported by nodes. -- The easiest user entry is now `/userstatus`, which reads the current Xboard login token and shows the current user's own status. +- The administrator entry is `/{secure_path}/user-online-devices`, which reads the current admin login token and shows paginated user online IP data. diff --git a/Xboard/plugins/UserOnlineDevices/resources/views/admin-users.blade.php b/Xboard/plugins/UserOnlineDevices/resources/views/admin-users.blade.php new file mode 100644 index 0000000..5531ad5 --- /dev/null +++ b/Xboard/plugins/UserOnlineDevices/resources/views/admin-users.blade.php @@ -0,0 +1,612 @@ + + + + + + + Admin Online IP Monitor + + + + +
+
+
Xboard Admin Monitor
+

All Users Online IP Count

+
+ This page is intended for administrators. It shows the current online IP quantity of all users reported by Xboard nodes, and supports keyword search by user ID or email. +
+
+ + + +
+
+
Users On Current Page
+
0
+
How many user rows are currently loaded
+
+
+
Users With Online IP
+
0
+
Users on this page whose online IP count is greater than zero
+
+
+
Total Online IP Count
+
0
+
Waiting for data
+
+
+ +
+ + + + + +
+ +
+
+ + + + + + + + + + + + +
IDEmailOnline IP CountOnline IP ListLast OnlineCreated
+
+ + +
+
+ + + + + diff --git a/Xboard/plugins/UserOnlineDevices/routes/api.php b/Xboard/plugins/UserOnlineDevices/routes/api.php index 51f02a3..67dd5d3 100644 --- a/Xboard/plugins/UserOnlineDevices/routes/api.php +++ b/Xboard/plugins/UserOnlineDevices/routes/api.php @@ -3,6 +3,8 @@ use Illuminate\Support\Facades\Route; use Plugin\UserOnlineDevices\Controllers\UserOnlineDevicesController; +$securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))); + Route::group([ 'prefix' => 'api/v1/user-online-devices', 'middleware' => 'user', @@ -10,3 +12,11 @@ Route::group([ Route::get('/summary', [UserOnlineDevicesController::class, 'summary']); Route::get('/panel-url', [UserOnlineDevicesController::class, 'panelUrl']); }); + +Route::group([ + 'prefix' => 'api/v2/' . $securePath . '/user-online-devices', + 'middleware' => 'admin', +], function () { + Route::get('/users', [UserOnlineDevicesController::class, 'adminUsers']) + ->name('user-online-devices.admin-users-data'); +}); diff --git a/Xboard/plugins/UserOnlineDevices/routes/web.php b/Xboard/plugins/UserOnlineDevices/routes/web.php index e4deee4..f51d952 100644 --- a/Xboard/plugins/UserOnlineDevices/routes/web.php +++ b/Xboard/plugins/UserOnlineDevices/routes/web.php @@ -3,22 +3,17 @@ use Illuminate\Support\Facades\Route; use Plugin\UserOnlineDevices\Controllers\UserOnlineDevicesController; -Route::get('/userstatus', [UserOnlineDevicesController::class, 'userStatus']) - ->name('user-online-devices.userstatus.short'); +$securePath = admin_setting('secure_path', admin_setting('frontend_admin_path', hash('crc32b', config('app.key')))); -Route::group([ - 'prefix' => 'user-online-devices', -], function () { - Route::get('/userstatus', [UserOnlineDevicesController::class, 'userStatus']) - ->name('user-online-devices.userstatus'); +Route::get('/' . $securePath . '/user-online-devices', [UserOnlineDevicesController::class, 'adminUsersPage']) + ->name('user-online-devices.admin-users'); - Route::get('/panel/{user}', [UserOnlineDevicesController::class, 'panel']) - ->whereNumber('user') - ->middleware('signed') - ->name('user-online-devices.panel'); +Route::get('/user-online-devices/panel/{user}', [UserOnlineDevicesController::class, 'panel']) + ->whereNumber('user') + ->middleware('signed') + ->name('user-online-devices.panel'); - Route::get('/snapshot/{user}', [UserOnlineDevicesController::class, 'snapshot']) - ->whereNumber('user') - ->middleware('signed') - ->name('user-online-devices.snapshot'); -}); +Route::get('/user-online-devices/snapshot/{user}', [UserOnlineDevicesController::class, 'snapshot']) + ->whereNumber('user') + ->middleware('signed') + ->name('user-online-devices.snapshot');