feat: Add API endpoints for staff online status and queue monitoring.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -62,4 +62,9 @@ class User extends Authenticatable implements JWTSubject
|
||||
return [];
|
||||
}
|
||||
|
||||
public function staffOnline()
|
||||
{
|
||||
return $this->hasOne(\App\Models\Staff::class, 'user_id', 'id');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user