feat: Add API endpoints for staff online status and queue monitoring.

This commit is contained in:
pbao9
2026-03-11 11:22:51 +07:00
parent 5649674eb4
commit 74cbe2fe1a
4 changed files with 241 additions and 0 deletions
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api\Admin\Staff;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Repositories\Staff\StaffRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class StaffOnlineController extends Controller
{
private const ONLINE_THRESHOLD_SECONDS = 60;
public function __construct(
private readonly StaffRepositoryInterface $staffRepository
) {}
/**
* GET /api/v1/administrator/staff-online
* Trả về danh sách staff đang online (query từ tbl.user).
*/
public function index(): JsonResponse
{
$users = User::whereHas('staffOnline')->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']);
}
}
@@ -0,0 +1,149 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Laravel\Horizon\Contracts\JobRepository;
use Laravel\Horizon\Contracts\MetricsRepository;
use Laravel\Horizon\Contracts\WorkloadRepository;
class QueueMonitorController extends Controller
{
public function __construct(
private readonly JobRepository $jobs,
private readonly MetricsRepository $metrics,
private readonly WorkloadRepository $workload,
) {}
/**
* GET /api/queue/jobs/recent
* Recent processed jobs tracked by Horizon.
*/
public function recent(): JsonResponse
{
$jobs = $this->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();
}
}
+5
View File
@@ -62,4 +62,9 @@ class User extends Authenticatable implements JWTSubject
return [];
}
public function staffOnline()
{
return $this->hasOne(\App\Models\Staff::class, 'user_id', 'id');
}
}
+16
View File
@@ -1,6 +1,8 @@
<?php
use App\Http\Controllers\Api\Admin\Auth\AuthController;
use App\Http\Controllers\Api\QueueMonitorController;
use App\Http\Controllers\Api\Admin\Staff\StaffOnlineController;
use App\Http\Controllers\Api\Admin\Customer\CustomerController;
use App\Http\Controllers\Api\Admin\Employee\EmployeeController;
use App\Http\Controllers\Api\Admin\Role\RolePermissionController;
@@ -192,6 +194,20 @@ Route::prefix('v1')
Route::delete('{id}', [DnsServerController::class, 'destroy'])->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');