diff --git a/app/Actions/GetOoklaSpeedtestServers.php b/app/Actions/GetOoklaSpeedtestServers.php index fe8e6a6ee..2fa9e2092 100644 --- a/app/Actions/GetOoklaSpeedtestServers.php +++ b/app/Actions/GetOoklaSpeedtestServers.php @@ -2,6 +2,7 @@ namespace App\Actions; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Lorisleiva\Actions\Concerns\AsAction; @@ -11,7 +12,22 @@ class GetOoklaSpeedtestServers { use AsAction; + /** + * For UI: return the ID, Sponsor, and Name to start a manual test + */ public function handle(): array + { + return collect(self::fetch())->mapWithKeys(function (array $item) { + return [ + $item['id'] => ($item['sponsor'] ?? 'Unknown').' ('.($item['name'] ?? 'Unknown').', '.$item['id'].')', + ]; + })->toArray(); + } + + /** + * Fetch the raw Ookla server array from the Ookla API. + */ + public static function fetch(): array { $query = [ 'engine' => 'js', @@ -23,6 +39,8 @@ public function handle(): array $response = Http::retry(3, 250) ->timeout(5) ->get(url: 'https://www.speedtest.net/api/js/servers', query: $query); + + return $response->json(); } catch (Throwable $e) { Log::error('Unable to retrieve Ookla servers.', [$e->getMessage()]); @@ -30,10 +48,28 @@ public function handle(): array '⚠️ Unable to retrieve Ookla servers, check internet connection and see logs.', ]; } + } + + /** + * For API: return array of structured server objects + */ + public static function forApi(): array + { + $servers = self::fetch(); + + // If the first item is not an array, treat as error or empty + if (empty($servers) || ! is_array($servers) || (isset($servers[0]) && ! is_array($servers[0]))) { + // Optionally, you could return an error message here, but to match the controller's behavior, return an empty array + return []; + } - return $response->collect()->mapWithKeys(function (array $item, int $key) { + return collect($servers)->map(function (array $item) { return [ - $item['id'] => $item['sponsor'].' ('.$item['name'].', '.$item['id'].')', + 'id' => $item['id'], + 'host' => Arr::get($item, 'host', 'Unknown'), + 'name' => Arr::get($item, 'sponsor', 'Unknown'), + 'location' => Arr::get($item, 'name', 'Unknown'), + 'country' => Arr::get($item, 'country', 'Unknown'), ]; })->toArray(); } diff --git a/app/Filament/Resources/ApiTokenResource.php b/app/Filament/Resources/ApiTokenResource.php index f5872b048..3d71df012 100644 --- a/app/Filament/Resources/ApiTokenResource.php +++ b/app/Filament/Resources/ApiTokenResource.php @@ -53,9 +53,9 @@ public static function getTokenFormSchema(): array ->required() ->bulkToggleable() ->descriptions([ - 'results:read' => 'Allow this token to read results.', - 'speedtests:run' => 'Allow this token to run speedtests.', - 'ookla:list-servers' => 'Allow this token to list servers.', + 'results:read' => 'Grant this token permission to read results and statistics.', + 'speedtests:run' => 'Grant this token permission to run speedtests.', + 'ookla:list-servers' => 'Grant this token permission to list available servers.', ]), DateTimePicker::make('expires_at') ->label('Expires at') diff --git a/app/Http/Controllers/Api/V1/LatestResult.php b/app/Http/Controllers/Api/V1/LatestResult.php deleted file mode 100644 index 1309c105e..000000000 --- a/app/Http/Controllers/Api/V1/LatestResult.php +++ /dev/null @@ -1,52 +0,0 @@ -latest() - ->firstOr(function () { - self::throw( - e: new NotFoundException('No result found.'), - code: 404, - ); - }); - - return self::sendResponse( - data: new ResultResource($result), - ); - } -} diff --git a/app/Http/Controllers/Api/V1/ListResults.php b/app/Http/Controllers/Api/V1/ListResults.php deleted file mode 100644 index d634496c1..000000000 --- a/app/Http/Controllers/Api/V1/ListResults.php +++ /dev/null @@ -1,86 +0,0 @@ -all(), [ - 'per_page' => 'integer|min:1|max:500', - ]); - - if ($validator->fails()) { - return ApiController::sendResponse( - data: $validator->errors(), - message: 'Validation failed.', - code: 422, - ); - } - - $results = QueryBuilder::for(Result::class) - ->allowedFilters([ - AllowedFilter::operator('ping', FilterOperator::DYNAMIC), - AllowedFilter::operator('download', FilterOperator::DYNAMIC), - AllowedFilter::operator('upload', FilterOperator::DYNAMIC), - AllowedFilter::exact('healthy')->nullable(), - AllowedFilter::exact('status'), - AllowedFilter::exact('scheduled'), - AllowedFilter::operator( - name: 'start_at', - internalName: 'created_at', - filterOperator: FilterOperator::DYNAMIC, - ), - AllowedFilter::operator( - name: 'end_at', - internalName: 'created_at', - filterOperator: FilterOperator::DYNAMIC, - ), - ]) - ->allowedSorts([ - 'ping', - 'download', - 'upload', - 'created_at', - 'updated_at', - ]) - ->jsonPaginate($request->input('per_page', 25)); - - return ResultResource::collection($results); - } -} diff --git a/app/Http/Controllers/Api/V1/ListSpeedtestServers.php b/app/Http/Controllers/Api/V1/ListSpeedtestServers.php deleted file mode 100644 index c67efa648..000000000 --- a/app/Http/Controllers/Api/V1/ListSpeedtestServers.php +++ /dev/null @@ -1,57 +0,0 @@ - 'You do not have permission to view speedtest servers.'] - ) - ), - ] - )] - public function __invoke(Request $request) - { - if ($request->user()->tokenCant('ookla:list-servers')) { - return self::sendResponse( - data: null, - message: 'You do not have permission to view speedtest servers.', - code: Response::HTTP_FORBIDDEN, - ); - } - - $servers = GetOoklaSpeedtestServers::run(); - - return self::sendResponse( - data: $servers, - message: 'Speedtest servers fetched successfully.' - ); - } -} diff --git a/app/Http/Controllers/Api/V1/OoklaController.php b/app/Http/Controllers/Api/V1/OoklaController.php new file mode 100644 index 000000000..992ce94db --- /dev/null +++ b/app/Http/Controllers/Api/V1/OoklaController.php @@ -0,0 +1,32 @@ +user()->tokenCant('ookla:list-servers')) { + return $this->sendResponse( + data: null, + message: 'You do not have permission to view speedtest servers.', + code: Response::HTTP_FORBIDDEN, + ); + } + + $servers = GetOoklaSpeedtestServers::forApi(); + + return $this->sendResponse( + data: $servers, + message: 'Speedtest servers fetched successfully.' + ); + } +} diff --git a/app/Http/Controllers/Api/V1/ResultsController.php b/app/Http/Controllers/Api/V1/ResultsController.php new file mode 100644 index 000000000..9c802fe84 --- /dev/null +++ b/app/Http/Controllers/Api/V1/ResultsController.php @@ -0,0 +1,118 @@ +user()->tokenCant('results:read')) { + return $this->sendResponse( + data: null, + message: 'You do not have permission to view results.', + code: Response::HTTP_FORBIDDEN + ); + } + $validator = Validator::make($request->all(), [ + 'per_page' => 'integer|min:1|max:500', + ]); + + if ($validator->fails()) { + return $this->sendResponse( + data: $validator->errors(), + message: 'Validation failed.', + code: 422 + ); + } + + $results = QueryBuilder::for(Result::class) + ->allowedFilters([ + AllowedFilter::operator('ping', FilterOperator::DYNAMIC), + AllowedFilter::operator('download', FilterOperator::DYNAMIC), + AllowedFilter::operator('upload', FilterOperator::DYNAMIC), + AllowedFilter::exact('healthy')->nullable(), + AllowedFilter::exact('status'), + AllowedFilter::exact('scheduled'), + AllowedFilter::operator( + name: 'start_at', + internalName: 'created_at', + filterOperator: FilterOperator::DYNAMIC, + ), + AllowedFilter::operator( + name: 'end_at', + internalName: 'created_at', + filterOperator: FilterOperator::DYNAMIC, + ), + ]) + ->allowedSorts([ + 'ping', + 'download', + 'upload', + 'created_at', + 'updated_at', + ]) + ->jsonPaginate($request->input('per_page', 25)); + + return ResultResource::collection($results); + } + + /** + * GET /results/{id} + * Fetch a single result by ID. + */ + public function show(Request $request, int $id) + { + if ($request->user()->tokenCant('results:read')) { + return $this->sendResponse( + data: null, + message: 'You do not have permission to view results.', + code: Response::HTTP_FORBIDDEN + ); + } + $result = Result::findOr($id, function () { + self::throw( + e: new NotFoundException('Result not found.'), + code: 404 + ); + }); + + return $this->sendResponse( + data: new ResultResource($result) + ); + } + + /** + * GET /results/latest + * Fetch the single most recent result. + */ + public function latest(Request $request) + { + if ($request->user()->tokenCant('results:read')) { + return $this->sendResponse( + data: null, + message: 'You do not have permission to view results.', + code: Response::HTTP_FORBIDDEN + ); + } + $result = Result::latest() + ->firstOrFail(); + + return $this->sendResponse( + data: new ResultResource($result) + ); + } +} diff --git a/app/Http/Controllers/Api/V1/ShowResult.php b/app/Http/Controllers/Api/V1/ShowResult.php deleted file mode 100644 index d30a8452c..000000000 --- a/app/Http/Controllers/Api/V1/ShowResult.php +++ /dev/null @@ -1,59 +0,0 @@ -user()->tokenCant('speedtests:run')) { + return $this->sendResponse( + data: null, + message: 'You do not have permission to run speedtests.', + code: Response::HTTP_FORBIDDEN, + ); + } + + $validator = Validator::make($request->all(), [ + 'server_id' => 'sometimes|integer', + ]); + + if ($validator->fails()) { + return $this->sendResponse( + data: $validator->errors(), + message: 'Validation failed.', + code: Response::HTTP_UNPROCESSABLE_ENTITY, + ); + } + + $result = RunSpeedtestAction::run( + serverId: $request->input('server_id'), + ); + + return $this->sendResponse( + data: new ResultResource($result), + message: 'Speedtest added to the queue.', + code: Response::HTTP_CREATED, + ); + } +} diff --git a/app/Http/Controllers/Api/V1/Stats.php b/app/Http/Controllers/Api/V1/Stats.php deleted file mode 100644 index fead80892..000000000 --- a/app/Http/Controllers/Api/V1/Stats.php +++ /dev/null @@ -1,83 +0,0 @@ -selectRaw('count(*) as total_results') - ->selectRaw('avg(ping) as avg_ping') - ->selectRaw('avg(download) as avg_download') - ->selectRaw('avg(upload) as avg_upload') - ->selectRaw('min(ping) as min_ping') - ->selectRaw('min(download) as min_download') - ->selectRaw('min(upload) as min_upload') - ->selectRaw('max(ping) as max_ping') - ->selectRaw('max(download) as max_download') - ->selectRaw('max(upload) as max_upload') - ->AllowedFilters([ - AllowedFilter::operator(name: 'start_at', internalName: 'created_at', filterOperator: FilterOperator::DYNAMIC), - AllowedFilter::operator(name: 'end_at', internalName: 'created_at', filterOperator: FilterOperator::DYNAMIC), - ]) - ->first(); - - return self::sendResponse( - data: new StatResource($stats), - filters: $request->input('filter'), - ); - } -} diff --git a/app/Http/Controllers/Api/V1/StatsController.php b/app/Http/Controllers/Api/V1/StatsController.php new file mode 100644 index 000000000..0e283f710 --- /dev/null +++ b/app/Http/Controllers/Api/V1/StatsController.php @@ -0,0 +1,52 @@ +user()->tokenCant('results:read')) { + return $this->sendResponse( + data: null, + message: 'You do not have permission to view statistics.', + code: Response::HTTP_FORBIDDEN + ); + } + + // Build the stats query + $stats = QueryBuilder::for(Result::class) + ->selectRaw('count(*) as total_results') + ->selectRaw('avg(ping) as avg_ping') + ->selectRaw('avg(download) as avg_download') + ->selectRaw('avg(upload) as avg_upload') + ->selectRaw('min(ping) as min_ping') + ->selectRaw('min(download) as min_download') + ->selectRaw('min(upload) as min_upload') + ->selectRaw('max(ping) as max_ping') + ->selectRaw('max(download) as max_download') + ->selectRaw('max(upload) as max_upload') + ->allowedFilters([ + AllowedFilter::operator(name: 'start_at', internalName: 'created_at', filterOperator: FilterOperator::DYNAMIC), + AllowedFilter::operator(name: 'end_at', internalName: 'created_at', filterOperator: FilterOperator::DYNAMIC), + ]) + ->first(); + + // Return wrapped in a resource + return $this->sendResponse( + data: new StatResource($stats) + ); + } +} diff --git a/app/Livewire/Topbar/RunSpeedtestAction.php b/app/Livewire/Topbar/RunSpeedtestAction.php index 648c136d1..326e383c8 100644 --- a/app/Livewire/Topbar/RunSpeedtestAction.php +++ b/app/Livewire/Topbar/RunSpeedtestAction.php @@ -13,6 +13,7 @@ use Filament\Forms\Contracts\HasForms; use Filament\Notifications\Notification; use Filament\Support\Enums\IconPosition; +use Illuminate\Support\Facades\Auth; use Livewire\Component; class RunSpeedtestAction extends Component implements HasActions, HasForms @@ -68,7 +69,7 @@ public function speedtestAction(): Action ->label('Speedtest') ->icon('heroicon-o-rocket-launch') ->iconPosition(IconPosition::Before) - ->hidden(! auth()->user()->is_admin) + ->hidden(! Auth::check() && Auth::user()->is_admin) ->extraAttributes([ 'id' => 'speedtestAction', ]); diff --git a/app/OpenApi/Annotations/V1/OoklaAnnotations.php b/app/OpenApi/Annotations/V1/OoklaAnnotations.php new file mode 100644 index 000000000..9dc612395 --- /dev/null +++ b/app/OpenApi/Annotations/V1/OoklaAnnotations.php @@ -0,0 +1,39 @@ +user()->tokenCant('speedtests:run')) { - return self::sendResponse( - data: null, - message: 'You do not have permission to run speedtests.', - code: Response::HTTP_FORBIDDEN, - ); - } - - $validator = Validator::make($request->all(), [ - 'server_id' => 'sometimes|integer', - ]); - - if ($validator->fails()) { - return ApiController::sendResponse( - data: $validator->errors(), - message: 'Validation failed.', - code: Response::HTTP_UNPROCESSABLE_ENTITY, - ); - } - - $result = RunSpeedtestAction::run( - serverId: $request->input('server_id'), - ); - - return self::sendResponse( - data: new ResultResource($result), - message: 'Speedtest added to the queue.', - code: Response::HTTP_CREATED, - ); + // Annotation placeholder for runSpeedtest } + + #[OA\Get( + path: '/api/v1/speedtests/list-servers', + summary: 'List available Ookla speedtest servers', + operationId: 'listSpeedtestServers', + tags: ['Speedtests'], + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: 'OK', + content: new OA\JsonContent(ref: '#/components/schemas/ServersCollection') + ), + new OA\Response( + response: Response::HTTP_UNAUTHORIZED, + description: 'Unauthenticated', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError') + ), + new OA\Response( + response: Response::HTTP_FORBIDDEN, + description: 'Forbidden', + content: new OA\JsonContent( + ref: '#/components/schemas/ForbiddenError', + example: ['message' => 'You do not have permission to view speedtest servers.'] + ) + ), + ] + )] + public function listServers(): void {} } diff --git a/app/OpenApi/Annotations/V1/StatsAnnotations.php b/app/OpenApi/Annotations/V1/StatsAnnotations.php new file mode 100644 index 000000000..78d8b188a --- /dev/null +++ b/app/OpenApi/Annotations/V1/StatsAnnotations.php @@ -0,0 +1,59 @@ + [ - '12345' => 'Fibernet (New York, 12345)', - ], - ], + type: 'array', + description: 'List of server objects', + items: new OA\Items( + type: 'object', + properties: [ + new OA\Property(property: 'id', type: 'string'), + new OA\Property(property: 'host', type: 'string'), + new OA\Property(property: 'name', type: 'string'), + new OA\Property(property: 'location', type: 'string'), + new OA\Property(property: 'country', type: 'string'), + ] + ) ), new OA\Property( property: 'message', diff --git a/openapi.json b/openapi.json index 3dbd4b26e..f036f9345 100644 --- a/openapi.json +++ b/openapi.json @@ -5,20 +5,34 @@ "version": "1.0.0" }, "paths": { - "/api/v1/results/latest": { + "/api/v1/ookla": { + "description": "Endpoints for retrieving Ookla speedtest servers and related resources." + }, + "/api/v1/results": { + "description": "Endpoints for retrieving speedtest results.", "get": { "tags": [ "Results" ], - "summary": "Fetch the single most recent result", - "operationId": "getLatestResult", + "summary": "List all results", + "operationId": "listResults", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Result" + "$ref": "#/components/schemas/ResultsCollection" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" } } } @@ -33,12 +47,12 @@ } } }, - "404": { - "description": "No result found", + "422": { + "description": "Validation failed", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotFoundError" + "$ref": "#/components/schemas/ValidationError" } } } @@ -46,20 +60,43 @@ } } }, - "/api/v1/results": { + "/api/v1/stats": { + "description": "Endpoints for viewing performance statistics.", "get": { "tags": [ - "Results" + "Stats" + ], + "summary": "Fetch aggregated Speedtest statistics", + "operationId": "getStats", + "parameters": [ + { + "name": "start_at", + "in": "query", + "description": "Filter stats from this date/time (ISO 8601)", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "end_at", + "in": "query", + "description": "Filter stats up to this date/time (ISO 8601)", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + } ], - "summary": "List results", - "operationId": "listResults", "responses": { "200": { - "description": "OK", + "description": "Statistics fetched successfully", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ResultsCollection" + "$ref": "#/components/schemas/Stats" } } } @@ -74,8 +111,18 @@ } } }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, "422": { - "description": "Validation failed", + "description": "Validation error", "content": { "application/json": { "schema": { @@ -93,10 +140,11 @@ "Servers" ], "summary": "List available Ookla speedtest servers", - "operationId": "listSpeedtestServers", + "description": "Returns an array of available Ookla speedtest servers. Requires an API token with `ookla:list-servers` scope.", + "operationId": "listOoklaServers", "responses": { "200": { - "description": "OK", + "description": "Servers retrieved successfully", "content": { "application/json": { "schema": { @@ -121,9 +169,6 @@ "application/json": { "schema": { "$ref": "#/components/schemas/ForbiddenError" - }, - "example": { - "message": "You do not have permission to view speedtest servers." } } } @@ -131,61 +176,61 @@ } } }, - "/api/v1/speedtests/run": { - "post": { + "/api/v1/results/{id}": { + "get": { "tags": [ - "Speedtests" + "Results" ], - "summary": "Run a new Ookla speedtest", - "operationId": "runSpeedtest", + "summary": "Get a single result", + "operationId": "getResult", "parameters": [ { - "name": "server_id", - "in": "query", - "description": "Optional Ookla speedtest server ID", - "required": false, + "name": "id", + "in": "path", + "description": "The ID of the result", + "required": true, "schema": { "type": "integer" } } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SpeedtestRun" + "$ref": "#/components/schemas/ResultResponse" } } } }, - "401": { - "description": "Unauthenticated", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UnauthenticatedError" + "$ref": "#/components/schemas/ForbiddenError" } } } }, - "403": { - "description": "Forbidden", + "401": { + "description": "Unauthenticated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ForbiddenError" + "$ref": "#/components/schemas/UnauthenticatedError" } } } }, - "422": { - "description": "Validation error", + "404": { + "description": "Result not found", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationError" + "$ref": "#/components/schemas/NotFoundError" } } } @@ -193,31 +238,30 @@ } } }, - "/api/v1/results/{id}": { + "/api/v1/results/latest": { "get": { "tags": [ "Results" ], - "summary": "Fetch a single result by ID", - "operationId": "getResult", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "The ID of the result", - "required": true, - "schema": { - "type": "integer" - } - } - ], + "summary": "Get the most recent result", + "operationId": "getLatestResult", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ResultResponse" + "$ref": "#/components/schemas/Result" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" } } } @@ -233,7 +277,7 @@ } }, "404": { - "description": "Result not found", + "description": "No result found", "content": { "application/json": { "schema": { @@ -245,43 +289,82 @@ } } }, - "/api/v1/stats": { - "get": { + "/api/v1/speedtests/run": { + "post": { "tags": [ - "Stats" + "Speedtests" ], - "summary": "Fetch aggregated Speedtest statistics", - "description": "Handle the incoming request.", - "operationId": "getStats", + "summary": "Run a new Ookla speedtest", + "operationId": "runSpeedtest", "parameters": [ { - "name": "start_at", + "name": "server_id", "in": "query", - "description": "ISO‑8601 start datetime filter", + "description": "Optional Ookla speedtest server ID", "required": false, "schema": { - "type": "string", - "format": "date-time" + "type": "integer" + } + } + ], + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SpeedtestRun" + } + } } }, - { - "name": "end_at", - "in": "query", - "description": "ISO‑8601 end datetime filter", - "required": false, - "schema": { - "type": "string", - "format": "date-time" + "401": { + "description": "Unauthenticated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnauthenticatedError" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenError" + } + } + } + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidationError" + } + } } } + } + } + }, + "/api/v1/speedtests/list-servers": { + "get": { + "tags": [ + "Speedtests" ], + "summary": "List available Ookla speedtest servers", + "operationId": "listSpeedtestServers", "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Stats" + "$ref": "#/components/schemas/ServersCollection" } } } @@ -296,12 +379,15 @@ } } }, - "422": { - "description": "Validation failed", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidationError" + "$ref": "#/components/schemas/ForbiddenError" + }, + "example": { + "message": "You do not have permission to view speedtest servers." } } } @@ -664,15 +750,30 @@ "additionalProperties": false }, "ServersCollection": { - "description": "Mapping of server IDs to display names", + "description": "Collection of Ookla speedtest servers", "properties": { "data": { - "description": "Map of server ID to display name", - "type": "object", - "example": { - "data": { - "12345": "Fibernet (New York, 12345)" - } + "description": "List of server objects", + "type": "array", + "items": { + "properties": { + "id": { + "type": "string" + }, + "host": { + "type": "string" + }, + "name": { + "type": "string" + }, + "location": { + "type": "string" + }, + "country": { + "type": "string" + } + }, + "type": "object" } }, "message": { @@ -850,19 +951,19 @@ "tags": [ { "name": "Results", - "description": "Operations related to Speedtest results.\nRequires an API token with scope `results:read`." + "description": "Endpoints for accessing and filtering speedtest results. Requires API token with `results:read` scope." }, { "name": "Speedtests", - "description": "Operations to run speedtests.\nRequires an API token with scope `speedtests:run`." + "description": "Endpoints for running speedtests and listing servers." }, { - "name": "Servers", - "description": "Operations for speedtest servers.\nRequires an API token with scope `ookla:list-servers`." + "name": "Stats", + "description": "Endpoints for retrieving aggregated statistics and performance metrics. Requires `speedtests:read` token scope." }, { - "name": "Stats", - "description": "Operations for statistics.\nRequires an API token with scope `results:read`." + "name": "Servers", + "description": "Servers" } ] } \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 527b71a69..30c477875 100644 --- a/routes/api.php +++ b/routes/api.php @@ -5,8 +5,6 @@ /** * Health check route to ensure the API is up and running. - * - * @deprecated */ Route::get('/healthcheck', function () { return response()->json([ diff --git a/routes/api/v1/routes.php b/routes/api/v1/routes.php index 730d0c11d..0bc2c6bbc 100644 --- a/routes/api/v1/routes.php +++ b/routes/api/v1/routes.php @@ -1,29 +1,27 @@ name('api.v1.')->group(function () { - Route::get('/results', ListResults::class) + Route::get('/results', [ResultsController::class, 'list']) ->name('results.list'); - Route::get('/results/latest', LatestResult::class) + Route::get('/results/latest', [ResultsController::class, 'latest']) ->name('results.latest'); - Route::get('/results/{result}', ShowResult::class) + Route::get('/results/{id}', [ResultsController::class, 'show']) ->name('results.show'); - Route::post('/speedtests/run', RunSpeedtest::class) + Route::post('/speedtests/run', SpeedtestController::class) ->name('speedtests.run'); - Route::get('/ookla/list-servers', ListSpeedtestServers::class) + Route::get('/ookla/list-servers', OoklaController::class) ->name('ookla.list-servers'); - Route::get('/stats', Stats::class) - ->name('stats'); + Route::get('/stats', StatsController::class) + ->name('stats.aggregated'); });