From 74cbe2fe1ab909b11368dccb3b4741418eb1bd94 Mon Sep 17 00:00:00 2001 From: pbao9 Date: Wed, 11 Mar 2026 11:22:51 +0700 Subject: [PATCH] feat: Add API endpoints for staff online status and queue monitoring. --- .../Api/Admin/Staff/StaffOnlineController.php | 71 +++++++++ .../Api/QueueMonitorController.php | 149 ++++++++++++++++++ app/Models/User.php | 5 + routes/api.php | 16 ++ 4 files changed, 241 insertions(+) create mode 100644 app/Http/Controllers/Api/Admin/Staff/StaffOnlineController.php create mode 100644 app/Http/Controllers/Api/QueueMonitorController.php diff --git a/app/Http/Controllers/Api/Admin/Staff/StaffOnlineController.php b/app/Http/Controllers/Api/Admin/Staff/StaffOnlineController.php new file mode 100644 index 0000000..fded7aa --- /dev/null +++ b/app/Http/Controllers/Api/Admin/Staff/StaffOnlineController.php @@ -0,0 +1,71 @@ +with('staffOnline')->get(); + + $online = $users->map(fn ($user) => [ + 'user_id' => $user->id, + 'name' => trim($user->firstname . ' ' . $user->lastname), + 'username' => $user->username, + 'email' => $user->email, + 'last_visited' => $user->staffOnline?->last_visited, + ])->values(); + + return response()->json([ + 'error' => false, + 'data' => $online, + 'total' => $online->count(), + ]); + } + + /** + * POST /api/v1/administrator/staff-online/ping + * Heartbeat cho JWT admin — cập nhật trạng thái online. + */ + public function ping(Request $request): JsonResponse + { + $userId = $request->user()?->id; + + if (!$userId) { + return response()->json(['error' => true, 'message' => 'Unauthenticated'], 401); + } + + $record = $this->staffRepository->getFirstBy(['user_id' => $userId]); + + if ($record) { + $record->last_visited = time(); + $record->save(); + } else { + $this->staffRepository->create([ + 'sessions_id' => 'jwt-' . $userId, + 'user_id' => $userId, + 'last_visited' => time(), + ]); + } + + $this->staffRepository->deleteRawTime(self::ONLINE_THRESHOLD_SECONDS); + + return response()->json(['error' => false, 'message' => 'OK']); + } +} diff --git a/app/Http/Controllers/Api/QueueMonitorController.php b/app/Http/Controllers/Api/QueueMonitorController.php new file mode 100644 index 0000000..3b2036c --- /dev/null +++ b/app/Http/Controllers/Api/QueueMonitorController.php @@ -0,0 +1,149 @@ +jobs->getRecent(); + + return response()->json([ + 'success' => true, + 'total' => $this->jobs->countRecent(), + 'data' => $this->formatJobs($jobs, 'completed'), + ]); + } + + /** + * GET /api/queue/jobs/pending + * Jobs currently waiting in the queue (per-queue workload + Horizon pending list). + */ + public function pending(): JsonResponse + { + $jobs = $this->jobs->getPending(); + $queues = $this->workload->get(); + + $workload = collect($queues)->map(fn (array $q) => [ + 'queue' => $q['name'], + 'length' => $q['length'], + 'wait' => $q['wait'], + 'processes' => $q['processes'], + ])->values(); + + return response()->json([ + 'success' => true, + 'total' => $this->jobs->countPending(), + 'workload' => $workload, + 'data' => $this->formatJobs($jobs, 'pending'), + ]); + } + + /** + * GET /api/queue/jobs/failed + * Failed jobs tracked by Horizon. + */ + public function failed(): JsonResponse + { + $jobs = $this->jobs->getFailed(); + + return response()->json([ + 'success' => true, + 'total' => $this->jobs->countFailed(), + 'data' => $this->formatJobs($jobs, 'failed'), + ]); + } + + /** + * GET /api/queue/jobs/metrics + * Queue throughput, runtime, and per-queue/per-job breakdown. + */ + public function metrics(): JsonResponse + { + $measuredQueues = $this->metrics->measuredQueues(); + $measuredJobs = $this->metrics->measuredJobs(); + + $queueMetrics = collect($measuredQueues)->map(fn (string $queue) => [ + 'queue' => $queue, + 'throughput' => $this->metrics->throughputForQueue($queue), + 'runtime_ms' => round($this->metrics->runtimeForQueue($queue), 2), + ])->values(); + + $jobMetrics = collect($measuredJobs)->map(fn (string $job) => [ + 'job' => $job, + 'throughput' => $this->metrics->throughputForJob($job), + 'runtime_ms' => round($this->metrics->runtimeForJob($job), 2), + ])->values(); + + return response()->json([ + 'success' => true, + 'data' => [ + 'throughput' => $this->metrics->throughput(), + 'jobs_processed_per_minute' => $this->metrics->jobsProcessedPerMinute(), + 'counts' => [ + 'recent' => $this->jobs->countRecent(), + 'pending' => $this->jobs->countPending(), + 'failed' => $this->jobs->countFailed(), + 'completed' => $this->jobs->countCompleted(), + 'recently_failed' => $this->jobs->countRecentlyFailed(), + ], + 'queues' => $queueMetrics, + 'jobs' => $jobMetrics, + ], + ]); + } + + /** + * Normalize a collection of Horizon job objects into a consistent array shape. + * + * @param \Illuminate\Support\Collection $jobs + * @param string $defaultStatus + * @return \Illuminate\Support\Collection + */ + private function formatJobs(\Illuminate\Support\Collection $jobs, string $defaultStatus): \Illuminate\Support\Collection + { + return $jobs->map(function (object $job) use ($defaultStatus) { + $payload = isset($job->payload) ? json_decode($job->payload, true) : []; + + return [ + 'id' => $job->id ?? null, + 'name' => $payload['displayName'] ?? $job->name ?? null, + 'queue' => $job->queue ?? null, + 'status' => $job->status ?? $defaultStatus, + 'attempts' => $job->attempts ?? 0, + 'started_at' => $this->formatTimestamp($job->reserved_at ?? null), + 'completed_at' => $this->formatTimestamp($job->completed_at ?? null), + 'failed_at' => $this->formatTimestamp($job->failed_at ?? null), + 'exception' => $job->exception ?? null, + ]; + })->values(); + } + + /** + * Convert a Unix timestamp to ISO-8601 string, or return null. + */ + private function formatTimestamp(int|float|string|null $ts): ?string + { + if ($ts === null || $ts === 0) { + return null; + } + + return \Carbon\Carbon::createFromTimestamp((int) $ts)->toIso8601String(); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 4115424..0293551 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -62,4 +62,9 @@ class User extends Authenticatable implements JWTSubject return []; } + public function staffOnline() + { + return $this->hasOne(\App\Models\Staff::class, 'user_id', 'id'); + } + } diff --git a/routes/api.php b/routes/api.php index 13fb4a1..3d1aa91 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,8 @@ middleware('perm:domain.manager'); }); + // Staff Online + Route::prefix('staff-online')->group(function () { + Route::get('/', [StaffOnlineController::class, 'index']); + Route::post('ping', [StaffOnlineController::class, 'ping']); + }); + + // Queue Monitor (Horizon) + Route::prefix('queue')->group(function () { + Route::get('jobs/recent', [QueueMonitorController::class, 'recent']); + Route::get('jobs/pending', [QueueMonitorController::class, 'pending']); + Route::get('jobs/failed', [QueueMonitorController::class, 'failed']); + Route::get('jobs/metrics', [QueueMonitorController::class, 'metrics']); + }); + // vCenter Manager Power Operations (module: module-vcenter) Route::prefix('vcenter-manager')->group(function () { Route::post('vm/power-on', [App\Http\Controllers\admin\VCenterManagerController::class, 'powerOn'])->middleware('perm:module-vcenter.edit');